22 Commits

Author SHA1 Message Date
cottongin
bc6dc357eb release: ef-0.15.99
All checks were successful
CI / build (push) Successful in 2m22s
Compile Release / build-release (push) Successful in 1m20s
First milestone release of the crosspoint-ef fork.

Key features:
- 14+ major enhancements over upstream 0.16.0
- Comprehensive documentation (crosspoint-ef-features.md, user guide)
- Fixed USB Serial blocking when device booted without USB connected
- Non-blocking Serial handling for ESP32-C3 USB CDC

See docs/crosspoint-ef-features.md for the complete feature list.
2026-01-28 17:49:20 -05:00
cottongin
ffe2aebd7e fix: guard Serial input calls when USB not connected at boot
Fixes device hanging when booted without USB connected. The root cause
was calling Serial.available() and Serial.read() in checkForFlashCommand()
when Serial.begin() was never called (USB not connected at boot).

Changes:
- Add if (!Serial) return guard to checkForFlashCommand()
- Restore upstream while (!Serial) wait loop with 3s timeout
- Remove Serial.setTxTimeoutMs(0) (not in upstream, may cause issues)
- Remove unnecessary if (Serial) guards from EpubReaderActivity.cpp
  (Serial.printf is safe without guards, only input calls need them)

Key insight: Serial.printf() is safe without guards (returns 0 when not
initialized), but Serial.available()/Serial.read() cause undefined
behavior on ESP32-C3 USB CDC when called without Serial.begin().

See: claude_notes/usb-serial-blocking-fix-2026-01-28.md
2026-01-28 17:45:00 -05:00
cottongin
4965e63ad4 chore: remove chat-summaries from git tracking
All checks were successful
CI / build (push) Successful in 2m20s
These files are now gitignored as they contain session-specific notes.
2026-01-28 16:23:34 -05:00
cottongin
4db384edb6 fix: prevent Serial.printf from blocking when USB disconnected
All checks were successful
CI / build (push) Successful in 2m23s
On ESP32-C3 with USB CDC, Serial.printf() blocks indefinitely when USB
is not connected. This caused device freezes when booted without USB.

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

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

Includes documentation of the debugging process and Serial call inventory.

Also applies clang-format to fix pre-existing formatting issues.
2026-01-28 16:16:11 -05:00
cottongin
f3075002c1 style: reduce variable scope in EpubReaderActivity
Some checks failed
CI / build (push) Failing after 2m31s
Move cachedRenderer, cachedMappedInput, and cachedSection declarations
into the DICTIONARY action block where they're actually used, addressing
cppcheck variableScope warnings.
2026-01-28 10:22:01 -05:00
cottongin
3e3be8bd23 fix: correct type names and restore cached variables
Some checks failed
CI / build (push) Failing after 2m4s
2026-01-28 10:13:06 -05:00
cottongin
800b07a2e5 docs: add AI assistance disclaimer
Some checks failed
CI / build (push) Failing after 4m13s
2026-01-28 10:08:13 -05:00
cottongin
2a31559747 docs: add fork notice and documentation
Some checks failed
CI / build (push) Failing after 3m43s
- Add crosspoint-ef fork notice to README with links to upstream
- Include feature overview, user guide, and technical comparison docs
2026-01-28 10:03:45 -05:00
cottongin
c052512b1b chore: point SDK submodule to Gitea fork
Some checks failed
CI / build (push) Failing after 2m45s
Switch from upstream GitHub repo to our Gitea fork which includes:
- SDCardManager.rename() method for archive feature
- EInkDisplay grayscale improvements
2026-01-28 09:58:23 -05:00
cottongin
bd95bfd44d style: fix all cppcheck warnings
Some checks failed
CI / build (push) Failing after 3m20s
- Fix ignored return value in TxtReaderActivity
- Remove unused variables across multiple files
- Add const correctness to DictionaryMargins and EpubReaderActivity
- Replace inefficient substr patterns with resize+append
- Use STL algorithms (find_if, any_of, copy_if, transform) where applicable
- Remove dead sync placeholder code in EpubReaderChapterSelectionActivity
- Add cppcheck suppression for ValueFlow analysis limitation

Resolves all low, medium, and high severity cppcheck defects.
2026-01-28 09:45:42 -05:00
cottongin
fe446d4690 fix(ci): use system Python instead of setup-python action
Some checks failed
CI / build (push) Failing after 3m13s
The setup-python action has hardcoded paths that fail on self-hosted
macOS runners. Use system Python directly instead.

Also simplified clang-format step to use system version if available.
2026-01-28 05:20:19 -05:00
cottongin
23e73312b4 fix(ci): set RUNNER_TOOL_CACHE for setup-python action
Some checks failed
CI / build (push) Failing after 19s
The setup-python action defaults to /Users/runner which doesn't exist
on self-hosted runners. Set RUNNER_TOOL_CACHE to a writable temp path.
2026-01-28 05:17:54 -05:00
cottongin
e8d332e34f ci: add Gitea Actions workflows
Some checks failed
CI / build (push) Failing after 51s
Adapt GitHub Actions workflows for self-hosted Gitea instance:
- CI workflow with PlatformIO build, cppcheck, and clang-format
- PR title format checker using conventional commits
- Release workflow for tagged builds

Keeps original .github/workflows/ for upstream compatibility.
2026-01-28 05:12:49 -05:00
cottongin
54004d5a5b chore: cleanup empty unused file 2026-01-28 03:16:06 -05:00
cottongin
d6e17c09ca typo 2026-01-28 03:14:19 -05:00
cottongin
7288e6499d chore: Stop tracking personal notes file
Add CrossPoint-ef.md to .gitignore to keep fork cleaner
and closer to upstream.
2026-01-28 03:13:19 -05:00
cottongin
5dab3ad5a3 feat: Library improvements - bookmarks, search, and tab navigation
Adds bookmark functionality with persistent storage, quick menu for
in-reader actions, Search tab with character picker, and unified
tab bar navigation across all library tabs.

Includes:
- BookmarkStore and BookmarkListActivity for bookmark management
- QuickMenuActivity for in-reader quick actions
- Reader bookmark integration with visual indicators
- Enhanced tab bar with scrolling, overflow indicators, and cursor
- Search tab with character picker and result navigation
- Consistent tab bar navigation (Up from top enters tab bar mode)
2026-01-28 02:51:51 -05:00
cottongin
82165c1022 feat: Wire up bookmark and quick menu features in main app
Integrates BookmarkStore initialization, QuickMenuActivity, and
BookmarkListActivity into the main application flow.
2026-01-28 02:20:58 -05:00
cottongin
e1fcec7d69 feat: Search tab with character picker and unified tab bar navigation
Adds Search tab to MyLibraryActivity with character picker for building
search queries, result navigation with long press jump-to-end support,
and Bookmarks tab integration. Implements consistent tab bar navigation
across all tabs - pressing Up from top of any list enters tab bar mode
with visible cursor indicators, Left/Right switches tabs, Down enters
list at top, and Up jumps to bottom of list.
2026-01-28 02:20:48 -05:00
cottongin
69a26ccb0e feat: Enhanced tab bar with scrolling, overflow indicators, and cursor
Tab bar now scrolls to keep selected tab visible when content overflows.
Adds triangle overflow indicators and optional bullet cursor indicators
around the active tab for visual focus feedback.
2026-01-28 02:20:38 -05:00
cottongin
245d5a7dd8 feat: Integrate bookmark support into reader activities
Adds bookmark add/remove functionality to EpubReaderActivity and base
ReaderActivity, with visual indicator for bookmarked pages.
2026-01-28 02:20:29 -05:00
cottongin
e991fb10a6 feat: Add QuickMenuActivity for in-reader quick actions
Provides a popup menu accessible during reading for quick access
to bookmarks, settings, and other common actions.
2026-01-28 02:20:19 -05:00
76 changed files with 4329 additions and 1378 deletions

41
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,41 @@
name: CI
'on':
push:
branches: [master, crosspoint-ef]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
run: |
# Use system Python on self-hosted runner
python3 --version
python3 -m pip install --upgrade pip
- name: Install PlatformIO Core
run: python3 -m pip install --upgrade platformio
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
- name: Run clang-format
run: |
# Use system clang-format if available, skip if not
if command -v clang-format &> /dev/null; then
./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
else
echo "clang-format not found, skipping format check"
fi
- name: Generate Dictionary Index
run: |
python3 scripts/generate_dict_index.py --zip dict-en-en.zip --output lib/StarDict/DictPrefixIndex.generated.h
- name: Build CrossPoint
run: pio run

View File

@@ -0,0 +1,40 @@
name: "PR Formatting"
on:
pull_request:
types:
- opened
- reopened
- edited
- synchronize
jobs:
title-check:
name: Title Check
runs-on: ubuntu-latest
steps:
- name: Check PR Title Format
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
echo "Checking PR title: $PR_TITLE"
# Conventional commit pattern: type(scope): description or type: description
# Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_-]+\))?: .+"
if echo "$PR_TITLE" | grep -qE "$PATTERN"; then
echo "✓ PR title follows conventional commit format"
else
echo "✗ PR title does not follow conventional commit format"
echo ""
echo "Expected format: type(scope): description"
echo " or: type: description"
echo ""
echo "Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
echo ""
echo "Examples:"
echo " feat(reader): add bookmark sync feature"
echo " fix: resolve memory leak in epub parser"
echo " docs: update README with new instructions"
exit 1
fi

View File

@@ -0,0 +1,40 @@
name: Compile Release
on:
push:
tags:
- '*'
jobs:
build-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
run: |
# Use system Python on self-hosted runner
python3 --version
python3 -m pip install --upgrade pip
- name: Install PlatformIO Core
run: python3 -m pip install --upgrade platformio
- name: Generate Dictionary Index
run: |
python3 scripts/generate_dict_index.py --zip dict-en-en.zip --output lib/StarDict/DictPrefixIndex.generated.h
- name: Build CrossPoint
run: pio run -e gh_release
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: CrossPoint-${{ github.ref_name }}
path: |
.pio/build/gh_release/bootloader.bin
.pio/build/gh_release/firmware.bin
.pio/build/gh_release/firmware.elf
.pio/build/gh_release/firmware.map
.pio/build/gh_release/partitions.bin

7
.gitignore vendored
View File

@@ -10,4 +10,9 @@ lib/EpdFont/fontsrc
build
**/__pycache__/
test/epubs/
TODO.md
CrossPoint-ef.md
Serial_print.code-search
# Gitea Actions runner config (contains credentials)
.runner
.runner.*

3
.gitmodules vendored
View File

@@ -1,4 +1,5 @@
[submodule "open-x4-sdk"]
path = open-x4-sdk
url = https://github.com/open-x4-epaper/community-sdk.git
url = https://code.cottongin.xyz/cottongin/community-sdk.git
branch = crosspoint-ef
ignore = dirty

View File

@@ -1,11 +0,0 @@
## Feature Requests:
1) search for books/library
2) Bookmarks
3) quick menu
4) crosspoint logo on firmware flashing screen
5) ability to add/remove books from lists on device.
6) hide "system folders" from files view
- dictionaries/
7) sorting options for files view
8) Time spent reading tracking

View File

@@ -1,4 +1,15 @@
# CrossPoint Reader
# CrossPoint Reader (ef fork)
> **Note:** This is **crosspoint-ef**, a heavily customized fork of [CrossPoint Reader](https://github.com/crosspoint-reader/crosspoint-reader) with additional features, UI improvements, and bug fixes. It also uses a [forked community-sdk](https://code.cottongin.xyz/cottongin/community-sdk) with additional hardware support.
>
> **Documentation:**
> - [Feature Overview](./docs/crosspoint-ef-features.md) - What's new in this fork
> - [User Guide](./docs/crosspoint-ef-user-guide.md) - How to use the new features
> - [Technical Comparison](./docs/branch-comparison-summary.md) - Detailed diff from upstream
>
> **Disclaimer:** Much of the code in this fork was developed with assistance from [Claude](https://claude.ai), an AI assistant by Anthropic.
---
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.

View File

@@ -1,537 +0,0 @@
# EPUB Reader Architectural Decisions
**Date:** 2026-01-23 19:47:23
**Status:** Active
**Based on:** EPUB 3.3 Compliance Audit
---
## Purpose
This document captures architectural decisions about which EPUB 3.3 features to implement, intentionally omit, or defer. Each decision includes rationale based on:
- Hardware constraints (ESP32-C3, 800x480 4-level grayscale e-ink)
- Memory limitations (~400KB SRAM, no PSRAM)
- User experience goals
- Implementation complexity
---
## ADR-001: Inline Image Support
**Status:** RECOMMENDED FOR IMPLEMENTATION
### Context
EPUBs frequently contain images for:
- Cover art
- Chapter illustrations
- Diagrams and figures
- Decorative elements
Current implementation displays `[Image: alt_text]` placeholder.
### Decision
**Implement inline image rendering** using existing infrastructure.
### Rationale
1. **Infrastructure Exists:**
- `Bitmap` class handles BMP parsing with grayscale conversion and dithering
- `JpegToBmpConverter` converts JPEG to BMP (most common EPUB image format)
- `GfxRenderer::drawBitmap()` already renders bitmaps to e-ink
- `ZipFile` can extract files from EPUB archive
- Home screen cover rendering demonstrates the pattern works
2. **Memory Management Pattern:**
- Convert and cache images to SD card (like thumbnail generation)
- Load one image at a time during page render
- Use streaming conversion to minimize RAM usage
3. **High User Impact:**
- Many EPUBs contain important visual content
- Technical books rely on diagrams
- Children's books heavily use illustrations
### Implementation Architecture
```
┌────────────────────────────────────────────────────────────────┐
│ Image Processing Pipeline │
├────────────────────────────────────────────────────────────────┤
│ │
│ EPUB ZIP ──► Extract Image ──► Convert to BMP ──► Cache to SD │
│ │ │ │
│ │ ├─ JPEG: JpegToBmpConverter│
│ │ └─ BMP: Direct copy │
│ │ │
│ └─► During page render: │
│ Load cached BMP ──► drawBitmap() │
│ │
└────────────────────────────────────────────────────────────────┘
```
### Page Element Structure
```cpp
// New PageImage element (alongside PageLine)
class PageImage final : public PageElement {
std::string cachedBmpPath; // Path to converted BMP on SD
int16_t width;
int16_t height;
public:
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
static std::unique_ptr<PageImage> deserialize(FsFile& file);
};
```
### Constraints
- **No PNG support** initially (would require adding `pngle` library)
- **Maximum image size:** Scale to viewport width, max 800x480
- **Memory budget:** ~10KB row buffer during conversion
---
## ADR-002: JavaScript/Scripting Support
**Status:** INTENTIONALLY OMITTED
### Context
EPUB 3.3 allows JavaScript in content documents for interactive features.
### Decision
**Do not implement JavaScript execution.**
### Rationale
1. **Security Risk:**
- Untrusted code execution on embedded device
- No sandboxing infrastructure
- Potential for malicious EPUBs
2. **Hardware Limitations:**
- E-ink display unsuitable for interactive content
- Limited RAM for JavaScript engine
- No benefit for static reading experience
3. **Minimal EPUB Use:**
- Most EPUBs don't use JavaScript
- Interactive textbooks target tablets, not e-readers
4. **Implementation Complexity:**
- Would require embedding V8/Duktape/QuickJS
- DOM manipulation engine
- Event handling system
### Alternative
`<script>` elements are silently ignored. Content remains readable.
---
## ADR-003: Fixed Layout (FXL) Support
**Status:** INTENTIONALLY OMITTED
### Context
EPUB Fixed Layout provides pixel-precise page positioning for:
- Comic books
- Children's picture books
- Magazines
- Technical drawings with precise layout
### Decision
**Do not implement Fixed Layout support.**
### Rationale
1. **Display Mismatch:**
- FXL designed for high-resolution color tablets
- 800x480 grayscale e-ink would require heavy downscaling
- Visual quality would be poor
2. **User Experience:**
- FXL EPUBs expect pan/zoom interaction
- E-ink refresh rate makes this impractical
- Text would be too small to read without zoom
3. **Implementation Complexity:**
- Requires full CSS positioning engine
- Viewport meta tag handling
- Coordinate transformation system
### Alternative
FXL EPUBs will open but may display incorrectly. Users should use reflowable EPUBs on this device.
---
## ADR-004: Audio/Video and Media Overlays
**Status:** HARDWARE LIMITED - CANNOT IMPLEMENT
### Context
EPUB 3.3 supports:
- `<audio>` and `<video>` elements
- Media Overlays (SMIL synchronization)
- Text-to-speech hints
### Decision
**Cannot implement due to hardware constraints.**
### Rationale
1. **No Audio Hardware:**
- Device has no speaker or audio DAC
- No audio output jack
2. **No Video Capability:**
- E-ink refresh rate (~1 Hz) incompatible with video
- No video decoding hardware
### Alternative
- Audio/video elements are ignored
- Alt text or fallback content displayed if available
- Media Overlays not processed
---
## ADR-005: Color CSS Properties
**Status:** HARDWARE LIMITED - SIMPLIFIED HANDLING
### Context
EPUBs use CSS colors for:
- Text color (`color`)
- Background color (`background-color`)
- Border colors
### Decision
**Ignore color properties; display in grayscale.**
### Rationale
1. **Hardware Constraint:**
- Display is 4-level grayscale only
- Cannot render colors
2. **Acceptable Degradation:**
- Text remains readable in black
- Background remains white
- Colored elements appear as gray variations
### Implementation
Color CSS properties are parsed but not applied. Default black text on white background used.
---
## ADR-006: Table Rendering
**Status:** DEFERRED - OPTIONAL IMPLEMENTATION
### Context
Tables appear in:
- Technical documentation
- Reference material
- Data presentations
Current implementation shows `[Table omitted]` placeholder.
### Decision
**Implement simple text-based table rendering as an optional enhancement.**
### Rationale
1. **Moderate Impact:**
- Some EPUBs use tables, but not majority
- Technical users would benefit most
2. **Complexity vs. Benefit:**
- Full table layout is complex (colspan, rowspan, sizing)
- Simple tables can be rendered as text columns
### Implementation Approach (if implemented)
```
┌──────────────────────────────────────┐
│ Text-Based Table Rendering │
├──────────────────────────────────────┤
│ │
│ Header1 │ Header2 │ Header3 │
│ ─────────────────────────────────────│
│ Data 1 │ Data 2 │ Data 3 │
│ Data 4 │ Data 5 │ Data 6 │
│ │
└──────────────────────────────────────┘
```
### Constraints
- Equal-width columns (no complex sizing)
- No colspan/rowspan support
- Truncate wide content
- Maximum 4-5 columns before overflow
---
## ADR-007: SVG and MathML
**Status:** INTENTIONALLY OMITTED
### Context
- SVG: Scalable Vector Graphics for illustrations
- MathML: Mathematical notation markup
### Decision
**Do not implement SVG or MathML rendering.**
### Rationale
1. **Implementation Complexity:**
- SVG requires full vector graphics engine
- MathML requires specialized math typesetting
2. **Limited Use:**
- Most EPUBs use raster images, not SVG
- MathML primarily in academic texts
3. **Alternative Exists:**
- Many EPUBs include fallback PNG for SVG
- MathML often has image fallback
### Alternative
Display alt text or fallback image if available.
---
## ADR-008: CSS Selector Support
**Status:** CURRENT IMPLEMENTATION SUFFICIENT
### Context
CSS selectors enable targeting elements for styling. Current support:
- Element selectors (`p`, `div`)
- Class selectors (`.classname`)
- Element.class selectors (`p.intro`)
### Decision
**Maintain current limited selector support; do not expand.**
### Rationale
1. **Sufficient for Most EPUBs:**
- 90%+ of EPUB styling uses simple selectors
- Complex selectors rarely affect core readability
2. **Implementation Complexity:**
- Descendant selectors require DOM tree
- Pseudo-selectors need state tracking
- Specificity calculation is complex
3. **Memory Constraints:**
- DOM tree would consume significant RAM
- Current streaming parser is memory-efficient
### Not Implemented
- Descendant selectors (`div p`)
- Child selectors (`ul > li`)
- Sibling selectors (`h1 + p`)
- Pseudo-classes (`:first-child`, `:hover`)
- Pseudo-elements (`::before`, `::after`)
- Attribute selectors (`[type="text"]`)
---
## ADR-009: Internal Link Navigation
**Status:** RECOMMENDED FOR IMPLEMENTATION (PHASE 2)
### Context
EPUBs use internal links for:
- Footnotes
- Cross-references
- Table of contents
- Index entries
### Decision
**Implement internal link navigation in Phase 2.**
### Rationale
1. **User Value:**
- Footnotes are common in non-fiction
- Reference navigation improves usability
2. **Complexity:**
- Requires anchor parsing and storage
- Needs selection UI for link activation
- Cross-chapter navigation adds complexity
### Implementation Architecture
```
Link Navigation Flow:
1. Parse <a href="#id"> during HTML parsing
2. Store link targets in PageLine metadata
3. Add link highlighting (underline or marker)
4. User selects link via UI
5. Resolve target: same chapter (anchor) or cross-chapter (spine + anchor)
6. Navigate to target page/position
```
### Deferred
- External links (http://) - no network navigation on e-reader
---
## ADR-010: DRM and Encryption
**Status:** INTENTIONALLY OMITTED
### Context
EPUB supports DRM through:
- `encryption.xml` in META-INF
- Adobe DRM
- Various proprietary schemes
### Decision
**Do not implement DRM support.**
### Rationale
1. **Licensing Complexity:**
- Adobe DRM requires licensing agreements
- Proprietary schemes have legal restrictions
2. **User Expectation:**
- Open-source e-reader users expect DRM-free content
- DRM conflicts with device modification philosophy
3. **Implementation Complexity:**
- Each DRM scheme is different
- Secure key storage required
- Regular updates needed for scheme changes
### Alternative
Users should remove DRM from purchased content using legal tools before loading to device.
---
## ADR-011: List Rendering Enhancements
**Status:** RECOMMENDED FOR IMPLEMENTATION (PHASE 1)
### Context
Current implementation:
- `<li>` renders bullet character `•`
- No numbered list support
- No nesting indentation
### Decision
**Enhance list rendering with ordered numbers and nesting.**
### Rationale
1. **Low Complexity:**
- Track list type (`<ol>` vs `<ul>`) in parser state
- Maintain counter for ordered lists
- Apply indentation based on nesting depth
2. **Clear User Benefit:**
- Numbered lists convey sequence
- Indentation shows hierarchy
### Implementation
```cpp
// Parser state additions
int listDepth = 0;
int orderedListCounter = 0;
bool isOrderedList = false;
// On <ol> start
isOrderedList = true;
orderedListCounter = 1;
listDepth++;
// On <li> in ordered list
addWord(std::to_string(orderedListCounter++) + ".", REGULAR);
// Apply indent
textIndent = listDepth * 20; // pixels per level
```
---
## Summary Table
| Feature | Decision | Rationale |
|---------|----------|-----------|
| Inline Images | IMPLEMENT | High impact, infrastructure ready |
| JavaScript | OMIT | Security risk, no benefit for e-ink |
| Fixed Layout | OMIT | Display mismatch, poor UX |
| Audio/Video | CANNOT | No hardware support |
| Color CSS | IGNORE | Grayscale display |
| Tables | DEFER | Moderate impact, high complexity |
| SVG/MathML | OMIT | High complexity, limited use |
| Complex CSS Selectors | OMIT | Memory constraints, limited benefit |
| Internal Links | IMPLEMENT (Phase 2) | User value for references |
| DRM | OMIT | Licensing, philosophy conflict |
| List Enhancements | IMPLEMENT (Phase 1) | Low complexity, clear benefit |
---
## Implementation Priority
### Phase 1 (Quick Wins)
- [x] Basic bullet rendering (already implemented)
- [ ] Ordered list numbering
- [ ] Nested list indentation
- [ ] Line-height CSS support
### Phase 2 (Image Support)
- [ ] Image extraction from EPUB
- [ ] JPEG to BMP conversion for inline images
- [ ] PageImage element integration
- [ ] Image scaling and layout
### Phase 3 (Navigation)
- [ ] Internal link parsing
- [ ] Link selection UI
- [ ] Anchor navigation
### Deferred/Not Planned
- PNG support (would need library addition)
- Table rendering
- Complex CSS selectors
- JavaScript, FXL, DRM

View File

@@ -1,262 +0,0 @@
# EPUB 3.3 Compliance Feature Prioritization
**Date:** 2026-01-23 19:46:02
**Based on:** EPUB 3.3 Compliance Audit
---
## Overview
This document reviews the audit findings from the CrossPoint Reader EPUB 3.3 compliance audit and prioritizes features for implementation based on:
1. **User Impact** - How much the feature improves the reading experience
2. **Implementation Complexity** - Level of effort required
3. **Existing Infrastructure** - Available code that can be leveraged
4. **Hardware Feasibility** - Whether the e-ink display can support it
---
## Priority 1: High Impact, Infrastructure Ready
These features have existing code that can be leveraged and provide significant user value.
### 1.1 Inline Image Rendering
**Current State:** Images show placeholder text `[Image: alt_text]`
**Infrastructure Available:**
- `Bitmap` class (`lib/GfxRenderer/Bitmap.h`) - BMP parsing with grayscale conversion and dithering
- `JpegToBmpConverter` (`lib/JpegToBmpConverter/`) - JPEG to BMP conversion with prescaling
- `GfxRenderer::drawBitmap()` - Already renders bitmaps to e-ink display
- `ZipFile` - Can extract images from EPUB archive
**Implementation Approach:**
1. Extend `ChapterHtmlSlimParser` to extract image `src` attributes
2. Create `PageImage` element (alongside existing `PageLine`)
3. Extract and cache images from EPUB ZIP to SD card as BMP
4. Integrate image blocks into page layout calculations
5. Render images inline with text during page display
**Complexity:** Medium
**User Impact:** High - Many EPUBs have important diagrams, illustrations, and decorative images
**Supported Formats:**
- JPEG (via `picojpeg`) - Most common in EPUBs
- BMP (native support)
- PNG - **Not currently supported** (would need `pngle` or similar library)
---
### 1.2 List Markers (Bullets and Numbers)
**Current State:** List items render without visual markers; `<li>` already adds bullet character
**Infrastructure Available:**
- `ChapterHtmlSlimParser` already handles `<li>` tags (line 248 adds bullet)
- Font system supports Unicode characters
**Implementation Approach:**
1. Track list type (`<ol>` vs `<ul>`) in parser state
2. For `<ol>`: maintain counter, render as "1.", "2.", etc.
3. For `<ul>`: use bullet character (already implemented: `\xe2\x80\xa2`)
4. Apply text indentation for list item content
**Current Code (already adds bullet):**
```cpp
if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
}
```
**Complexity:** Low
**User Impact:** Medium - Improves readability of enumerated content
---
### 1.3 Nested List Indentation
**Current State:** Nested lists are flattened to same indentation level
**Implementation Approach:**
1. Track nesting depth in parser (`listDepth` counter)
2. Apply progressive `text-indent` based on depth (e.g., 20px per level)
3. Store depth in `BlockStyle` for rendering
**Complexity:** Low
**User Impact:** Medium - Important for technical documentation and outlines
---
## Priority 2: Medium Impact, Moderate Complexity
### 2.1 Basic Table Rendering
**Current State:** Tables show `[Table omitted]` placeholder
**Implementation Approach:**
1. Parse table structure (`<table>`, `<tr>`, `<td>`, `<th>`)
2. Calculate column widths based on content or proportional division
3. Render as text rows with column separators (e.g., `|` character)
4. Apply header styling (bold) for `<th>` elements
**Constraints:**
- Fixed-width font may be needed for alignment
- Complex tables (colspan, rowspan) would remain unsupported
- Wide tables may need horizontal truncation
**Complexity:** Medium-High
**User Impact:** Medium - Some EPUBs have data tables, but many don't
---
### 2.2 Internal Link Navigation
**Current State:** Links render as plain text, not interactive
**Implementation Approach:**
1. Parse `<a href="#id">` attributes during HTML parsing
2. Store link targets in page elements
3. Add UI for link selection (highlight/underline links)
4. Implement navigation to target anchor when activated
5. Handle cross-chapter links (resolve to spine + anchor)
**Complexity:** Medium-High (requires UI changes)
**User Impact:** Medium - Important for reference material, footnotes
---
### 2.3 Line Height CSS Support
**Current State:** Line height is fixed based on font metrics
**Implementation Approach:**
1. Add `line-height` property to `CssParser`
2. Store in `CssStyle` struct
3. Apply multiplier to `getLineHeight()` during layout
**Complexity:** Low
**User Impact:** Medium - Improves text density matching to publisher intent
---
## Priority 3: Lower Impact or Higher Complexity
### 3.1 Font Size CSS Support
**Current State:** Font size controlled by reader settings only
**Implementation Approach:**
1. Parse `font-size` in `CssParser` (relative units: em, rem, %)
2. Map to available font sizes (snap to nearest)
3. Apply during text rendering
**Constraints:**
- Limited font size options in bitmap fonts
- Large size changes may cause layout issues
**Complexity:** Medium
**User Impact:** Low-Medium - Most content uses default sizing
---
### 3.2 Page-List Navigation
**Current State:** Only TOC navigation supported
**Implementation Approach:**
1. Parse `<nav epub:type="page-list">` in navigation document
2. Store page reference mappings
3. Add UI to jump to specific page numbers
**Complexity:** Medium
**User Impact:** Low - Useful for academic/reference texts with page citations
---
### 3.3 Landmarks Navigation
**Current State:** Not implemented
**Implementation Approach:**
1. Parse `<nav epub:type="landmarks">`
2. Extract semantic markers (cover, toc, bodymatter, etc.)
3. Add quick-nav UI
**Complexity:** Low-Medium
**User Impact:** Low - Convenience feature
---
## Features NOT Recommended for Implementation
These features are either hardware-limited or provide minimal value for the target use case.
### Hardware Limited (Cannot Implement)
| Feature | Reason |
|---------|--------|
| Color images/CSS | Monochrome 4-level grayscale display |
| Audio playback | No audio hardware |
| Video playback | No video hardware, e-ink refresh too slow |
| Animations | E-ink refresh latency incompatible |
| Media Overlays | Requires audio hardware |
### Not Worth Implementing
| Feature | Reason |
|---------|--------|
| JavaScript | Security concerns, minimal EPUB use, high complexity |
| Fixed Layout (FXL) | Designed for tablets; poor e-ink experience |
| SVG | Complex parser needed, limited use in text EPUBs |
| MathML | Extremely complex, niche use case |
| CSS Grid/Flexbox | Overkill for text layout |
| @font-face | Memory constraints, limited benefit |
| DRM | Licensing complexity, user expectation of DRM-free |
| Forms | Interactive elements unsuitable for e-ink |
---
## Implementation Roadmap
### Phase 1: Quick Wins (Low Effort, High Value)
1. ~~List bullet rendering~~ (already implemented)
2. Ordered list numbering
3. Nested list indentation
4. Line-height CSS support
### Phase 2: Image Support (Medium Effort, High Value)
1. JPEG image extraction and caching
2. BMP image support
3. `PageImage` element integration
4. Image scaling to viewport width
5. (Optional) PNG support via external library
### Phase 3: Enhanced Navigation (Medium Effort, Medium Value)
1. Internal link parsing
2. Link highlighting/selection UI
3. Anchor navigation within chapters
4. Cross-chapter link navigation
### Phase 4: Table Support (High Effort, Medium Value)
1. Basic table parsing
2. Simple column layout
3. Text-based table rendering
4. Header styling
---
## Summary
| Priority | Feature | Complexity | Impact | Dependencies |
|----------|---------|------------|--------|--------------|
| 1.1 | Inline Images | Medium | High | Bitmap, JpegToBmpConverter |
| 1.2 | Ordered List Numbers | Low | Medium | None |
| 1.3 | Nested List Indent | Low | Medium | None |
| 2.1 | Basic Tables | Medium-High | Medium | None |
| 2.2 | Internal Links | Medium-High | Medium | UI changes |
| 2.3 | Line Height CSS | Low | Medium | CssParser |
| 3.1 | Font Size CSS | Medium | Low-Medium | Font system |
| 3.2 | Page-List Nav | Medium | Low | Navigation system |
| 3.3 | Landmarks Nav | Low-Medium | Low | Navigation system |
**Recommended First Implementation:** Start with Phase 1 quick wins (list improvements, line-height), then proceed to Image Support as it has the highest user-visible impact and leverages existing infrastructure.

View File

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

View File

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

View File

@@ -0,0 +1,132 @@
# USB Serial Blocking Issue - Root Cause and Fix
**Date:** 2026-01-28
**Issue:** Device blocking/hanging when USB is not connected at boot
---
## Problem Description
The device would hang or behave unpredictably when booted without USB connected. This was traced to improper Serial handling on ESP32-C3 with USB CDC.
## Root Cause Analysis
### Factor A: `checkForFlashCommand()` Called Without Serial Initialization
The most critical issue was in `checkForFlashCommand()`, which is called at the start of every `loop()` iteration:
```cpp
void loop() {
checkForFlashCommand(); // Called EVERY loop iteration
// ...
}
void checkForFlashCommand() {
while (Serial.available()) { // Called even when Serial.begin() was never called!
char c = Serial.read();
// ...
}
}
```
When USB is not connected at boot, `Serial.begin()` is never called. Then in `loop()`, `checkForFlashCommand()` calls `Serial.available()` and `Serial.read()` on an uninitialized Serial object. On ESP32-C3 with USB CDC, this causes undefined behavior or blocking.
### Factor B: Removed `while (!Serial)` Wait Loop
The upstream 0.16.0 code included a 3-second wait loop after `Serial.begin()`:
```cpp
if (isUsbConnected()) {
Serial.begin(115200);
unsigned long start = millis();
while (!Serial && (millis() - start) < 3000) {
delay(10);
}
}
```
This wait loop was removed in an earlier attempt to fix boot delays, but it may be necessary for proper USB CDC initialization.
### Factor C: `Serial.setTxTimeoutMs(0)` Added Too Early
`Serial.setTxTimeoutMs(0)` was added immediately after `Serial.begin()` to make TX non-blocking. However, calling this before the Serial connection is fully established may interfere with USB CDC initialization.
---
## The Fix
### 1. Guard `checkForFlashCommand()` with Serial Check
```cpp
void checkForFlashCommand() {
if (!Serial) return; // Early exit if Serial not initialized
while (Serial.available()) {
// ... rest unchanged
}
}
```
### 2. Restore Upstream Serial Initialization Pattern
```cpp
void setup() {
t1 = millis();
// Only start serial if USB connected
pinMode(UART0_RXD, INPUT);
if (isUsbConnected()) {
Serial.begin(115200);
// Wait up to 3 seconds for Serial to be ready to catch early logs
unsigned long start = millis();
while (!Serial && (millis() - start) < 3000) {
delay(10);
}
}
// ... rest of setup
}
```
### 3. Remove `Serial.setTxTimeoutMs(0)`
This call was removed entirely as it's not present in upstream and may cause issues.
### 4. Remove Unnecessary `if (Serial)` Guards
The 15 `if (Serial)` guards added to `EpubReaderActivity.cpp` were removed. `Serial.printf()` is safe to call when Serial isn't initialized (it simply returns 0), so guards around output calls are unnecessary.
**Key distinction:**
- `Serial.printf()` / `Serial.println()` - Safe without guards (no-op when not initialized)
- `Serial.available()` / `Serial.read()` - **MUST** be guarded (undefined behavior when not initialized)
---
## Files Changed
| File | Change |
|------|--------|
| `src/main.cpp` | Removed `Serial.setTxTimeoutMs(0)`, restored `while (!Serial)` wait, added guard to `checkForFlashCommand()` |
| `src/activities/reader/EpubReaderActivity.cpp` | Removed all 15 `if (Serial)` guards |
---
## Testing Checklist
After applying fixes, verify:
1. ✅ Boot with USB connected, serial monitor open - should work
2. ✅ Boot with USB connected, NO serial monitor - should work (3s delay then continue)
3. ✅ Boot without USB - should work immediately (no blocking)
4. ✅ Sleep without USB, plug in USB during sleep, wake - should work
5. ✅ Sleep with USB, unplug during sleep, wake - should work
---
## Lessons Learned
1. **Always guard Serial input operations**: `Serial.available()` and `Serial.read()` must be guarded with `if (Serial)` or `if (!Serial) return` when Serial initialization is conditional.
2. **Serial output is safe without guards**: `Serial.printf()` and similar output functions are safe to call even when Serial is not initialized - they simply return 0.
3. **Don't remove initialization waits without understanding why they exist**: The `while (!Serial)` wait loop exists for proper USB CDC initialization and shouldn't be removed without careful testing.
4. **Upstream patterns exist for a reason**: When diverging from upstream behavior, especially around low-level hardware initialization, be extra cautious and test thoroughly.

View File

@@ -0,0 +1,422 @@
# Branch Comparison Summary: crosspoint-ef vs 0.16.0
This document provides a comprehensive comparison between the `crosspoint-ef` branch and the upstream `0.16.0` release for merge planning and implementation decisions.
## Branch History
| Branch | Base | Commits Since Base | Status |
|--------|------|-------------------|--------|
| `crosspoint-ef` | 0.15.0 | 90+ | Active development |
| `0.16.0` | 0.15.0 | 30 | Released |
Both branches diverged from `0.15.0` at commit `3ce11f14`.
---
## Feature Comparison Matrix
### Major Features
| Feature | crosspoint-ef | 0.16.0 | Notes |
|---------|:-------------:|:------:|-------|
| Dictionary Support | Yes | No | StarDict format with word selection |
| Bookmark System | Yes | No | Per-book bookmarks with visual indicator |
| Quick Menu | Yes | No | Power button quick access |
| Library Search | Yes | No | Character picker with weighted search |
| CSS Parsing | Yes | No | Element, class, inline styles |
| Inline Images (PNG/JPEG) | Yes | No | With caching and dithering |
| Custom Fonts | Yes | No | Atkinson Hyperlegible, Fern Micro |
| Enhanced Web Server | Yes | Partial | File ops, MD5 API, mDNS |
| Companion App API | Yes | No | Deep links, WebSocket uploads |
| Reading Lists | Yes | No | With pinning support |
| Tab Bar Enhancements | Yes | No | Scrolling, overflow indicators |
| High Contrast Mode | Yes | No | System-wide |
| Bezel Compensation | Yes | No | Edge defect compensation |
| Sleep Screen Edge Detection | Yes | No | Dominant color fill |
| Recents Improvements | Yes | No | Badges, removal, clearing |
| Progress Bar Status Bar | Yes | Yes | Same feature |
| Spanish Hyphenation | No | Yes | Missing in crosspoint-ef |
| XTC/XTCH Author Extraction | No | Yes | Missing in crosspoint-ef |
| OTA Rework | No | Yes | Different implementation |
| KOReader MD5 Binary Matching | No | Yes | Missing in crosspoint-ef |
| Relative Position on Settings Change | No | Yes | Missing in crosspoint-ef |
| Multi-line Keyboard Entry | No | Yes | Missing in crosspoint-ef |
| Italics on Image Alt | No | Yes | Missing in crosspoint-ef |
| Page Turn on Button Press (UX) | No | Yes | When chapter skip disabled |
### Bug Fixes
| Fix | crosspoint-ef | 0.16.0 | Notes |
|-----|:-------------:|:------:|-------|
| Large EPUB indexing O(n²)→O(n) | Yes | Yes | Same fix |
| Settings validation on read | Yes | Yes | Same fix |
| Line break fixes | Yes | Yes | Similar fixes |
| Rotate origin in drawImage | Yes | Yes | Same fix |
| Short-press power wakeup | Yes | Yes | Same fix |
| TXT books in recent tab | Yes | Yes | Same fix |
| B&W filters for covers | Yes | Yes | Same fix |
| Cover fit artifacts | Yes | Yes | Same fix |
| Grayscale state corruption | Yes | No | Unique to crosspoint-ef |
| Memory graceful degradation | Yes | No | Unique to crosspoint-ef |
| Chapter Selection UI (KOReader) | No | Yes | Missing in crosspoint-ef |
| Front layout in mapLabels() | No | Yes | Missing in crosspoint-ef |
---
## Files Changed Summary
### crosspoint-ef Unique Files (New)
| Category | Files |
|----------|-------|
| Dictionary | `src/activities/dictionary/` (8 files), `lib/StarDict/` (4 files) |
| Bookmarks | `src/BookmarkStore.cpp/.h`, `src/activities/home/BookmarkListActivity.cpp/.h` |
| Quick Menu | `src/activities/util/QuickMenuActivity.cpp/.h` |
| CSS | `lib/Epub/Epub/css/` (3 files) |
| Images | `lib/Epub/Epub/blocks/ImageBlock.cpp/.h`, `lib/Epub/Epub/converters/` (6 files) |
| Custom Fonts | `src/customFonts.cpp`, `src/fontIds.h`, `lib/EpdFont/builtinFonts/custom/` (50+ files) |
| Utils | `src/util/Md5Utils.cpp/.h`, `src/util/StringUtils.cpp/.h` |
| Lists | `src/BookListStore.cpp/.h` |
| Docs | `docs/webserver-api-reference.md`, `docs/companion-app-deep-link-API.md`, `docs/troubleshooting.md` |
### crosspoint-ef Modified Files (Significant Changes)
| File | Changes |
|------|---------|
| `src/network/CrossPointWebServer.cpp` | +1083 lines (file ops, API, WebSocket) |
| `src/activities/home/MyLibraryActivity.cpp` | +700 lines (tabs, search, badges) |
| `src/main.cpp` | +255 lines (feature integration) |
| `lib/GfxRenderer/GfxRenderer.cpp` | +439 lines (contrast, bezel) |
| `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` | +596 lines (CSS integration) |
| `src/CrossPointSettings.cpp/.h` | New settings fields |
| `src/activities/boot_sleep/SleepActivity.cpp` | Edge detection, caching |
| `src/RecentBooksStore.cpp` | Badges, removal, metadata |
| `src/ScreenComponents.cpp` | Tab bar enhancements |
### 0.16.0 Unique Files/Changes
| File | Description |
|------|-------------|
| `lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h` | Spanish hyphenation (removed in ef) |
| `lib/KOReaderSync/` | KOReader credential handling (removed in ef) |
| `src/network/OtaUpdater.cpp` | OTA rework |
---
## Merge Strategy Recommendations
### Phase 1: Cherry-pick 0.16.0 Fixes into crosspoint-ef
**Low Risk - Recommended First:**
1. **Spanish hyphenation support** (#558)
- Add `hyph-es.trie.h` back
- Update `LanguageRegistry.cpp`
2. **Render keyboard entry over multiple lines** (#567)
- Update `KeyboardEntryActivity.cpp`
3. **Correctly render italics on image alt** (#569)
- Minimal change to text rendering
4. **Page turning on button pressed** (#451)
- UX improvement when chapter skip disabled
5. **Missing front layout in mapLabels()** (#564)
- Bug fix for button mapping
**Medium Risk:**
6. **KOReader document MD5 binary matching** (#529)
- May conflict with MD5Utils changes
7. **Chapter Selection UI bugs** (#501)
- Review for conflicts with tab bar changes
8. **Relative position on settings change** (#486)
- Reader state management change
**Higher Risk:**
9. **OTA feature rework** (#509)
- Compare implementations, may need reconciliation
- crosspoint-ef has different OTA changes
10. **Extract author from XTC/XTCH** (#563)
- XTC format was removed in crosspoint-ef
- Evaluate if needed
### Phase 2: Potential Upstream Contributions from crosspoint-ef
**High Value, Moderate Complexity:**
1. **Dictionary Support**
- Self-contained feature
- New files, minimal integration points
- Requires shipping dictionary data
2. **Bookmark System**
- Clean implementation
- New files with reader integration
3. **Quick Menu**
- Simple overlay feature
- Depends on bookmark and dictionary
4. **CSS Parsing**
- Significant EPUB improvement
- Well-isolated in `lib/Epub/Epub/css/`
**High Value, Higher Complexity:**
5. **Inline Image Support**
- Major EPUB enhancement
- Multiple new converters
- Memory management considerations
6. **Library Search**
- Integrated into MyLibraryActivity
- Tab bar changes included
7. **Enhanced Web Server**
- Large changes to CrossPointWebServer
- New API endpoints
- WebSocket uploads
**Medium Value:**
8. **Custom Fonts**
- Large binary additions (font headers)
- Clean integration
9. **Display Enhancements**
- High contrast, bezel compensation
- Settings additions
10. **Reading Lists**
- New feature with web API
---
## Potential Conflicts
### High Conflict Risk
| Area | crosspoint-ef | 0.16.0 | Resolution |
|------|---------------|--------|------------|
| `MyLibraryActivity.cpp` | Major restructure | Minor fixes | Manual merge required |
| `CrossPointWebServer.cpp` | Extensive additions | Minimal changes | crosspoint-ef likely compatible |
| `CrossPointSettings.h` | Many new fields | Few changes | Additive, low conflict |
| `main.cpp` | Feature integration | Minor changes | Review integration points |
| `OtaUpdater.cpp` | Modified | Reworked (#509) | Compare implementations |
### Low Conflict Risk
| Area | Notes |
|------|-------|
| Dictionary files | All new, no conflicts |
| Bookmark files | All new, no conflicts |
| CSS parser files | All new, no conflicts |
| Image converter files | All new, no conflicts |
| Custom font files | All new, no conflicts |
| StarDict library | All new, no conflicts |
---
## Testing Considerations
### Regression Testing Required
After any merge:
1. **EPUB Reading**
- Page navigation
- Chapter selection
- CSS styling
- Image rendering
- Bookmark indicators
2. **Library Functions**
- Tab navigation
- Search functionality
- Recent books display
- List management
3. **Dictionary**
- Word selection
- Lookup accuracy
- Definition display
4. **Web Server**
- File upload/download
- API endpoints
- WebSocket uploads
- mDNS discovery
5. **Settings**
- All new settings persist correctly
- Settings migration from older versions
6. **Display**
- High contrast mode
- Bezel compensation (all orientations)
- Sleep screen variations
### Memory Testing
crosspoint-ef includes memory optimization fixes. After merge:
1. Test with large EPUBs (2000+ chapters)
2. Test opening multiple books in sequence
3. Test anti-aliasing under memory pressure
4. Monitor for ghosting/artifacts
---
## Priority Recommendations
### Immediate (For crosspoint-ef Stability)
1. Cherry-pick Spanish hyphenation (#558)
2. Cherry-pick multi-line keyboard entry (#567)
3. Cherry-pick italics on image alt (#569)
4. Cherry-pick front layout fix (#564)
### Short-term (Feature Completeness)
5. Evaluate OTA rework (#509) - compare implementations
6. Cherry-pick page turn UX (#451)
7. Cherry-pick relative position fix (#486)
### Long-term (Upstream Contribution)
8. Prepare dictionary feature as PR
9. Prepare bookmark system as PR
10. Prepare CSS parsing as PR
11. Evaluate inline image support for upstream
---
## File Inventory for Merge
### Files to Add to 0.16.0 Base (for upstream contribution)
```
src/activities/dictionary/
DictionaryMargins.h
DictionaryMenuActivity.cpp
DictionaryMenuActivity.h
DictionaryResultActivity.cpp
DictionaryResultActivity.h
DictionarySearchActivity.cpp
DictionarySearchActivity.h
EpubWordSelectionActivity.cpp
EpubWordSelectionActivity.h
src/activities/util/
QuickMenuActivity.cpp
QuickMenuActivity.h
src/activities/home/
BookmarkListActivity.cpp
BookmarkListActivity.h
src/
BookmarkStore.cpp
BookmarkStore.h
BookListStore.cpp
BookListStore.h
customFonts.cpp
fontIds.h
BadgeConfig.h
src/util/
Md5Utils.cpp
Md5Utils.h
StringUtils.cpp
StringUtils.h
src/images/
LockIcon.h
lib/StarDict/
StarDict.cpp
StarDict.h
DictHtmlParser.cpp
DictHtmlParser.h
DictPrefixIndex.generated.h
lib/Epub/Epub/css/
CssParser.cpp
CssParser.h
CssStyle.h
lib/Epub/Epub/blocks/
ImageBlock.cpp
ImageBlock.h
BlockStyle.h
lib/Epub/Epub/converters/
FramebufferWriter.cpp
FramebufferWriter.h
ImageDecoderFactory.cpp
ImageDecoderFactory.h
ImageToFramebufferDecoder.cpp
ImageToFramebufferDecoder.h
JpegToFramebufferConverter.cpp
JpegToFramebufferConverter.h
PngToFramebufferConverter.cpp
PngToFramebufferConverter.h
lib/EpdFont/builtinFonts/custom/
[All font header files]
docs/
webserver-api-reference.md
companion-app-deep-link-API.md
troubleshooting.md
crosspoint-ef-features.md
crosspoint-ef-user-guide.md
```
### Files to Merge Carefully
```
src/main.cpp
src/CrossPointSettings.cpp
src/CrossPointSettings.h
src/network/CrossPointWebServer.cpp
src/network/CrossPointWebServer.h
src/network/OtaUpdater.cpp
src/network/OtaUpdater.h
src/activities/home/MyLibraryActivity.cpp
src/activities/home/MyLibraryActivity.h
src/activities/reader/EpubReaderActivity.cpp
src/activities/settings/SettingsActivity.cpp
src/activities/settings/CategorySettingsActivity.cpp
src/ScreenComponents.cpp
src/ScreenComponents.h
src/RecentBooksStore.cpp
src/RecentBooksStore.h
lib/GfxRenderer/GfxRenderer.cpp
lib/GfxRenderer/GfxRenderer.h
lib/GfxRenderer/BitmapHelpers.cpp
lib/GfxRenderer/BitmapHelpers.h
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h
lib/Epub/Epub/Section.cpp
lib/Epub/Epub/Section.h
```
---
## Conclusion
The `crosspoint-ef` branch represents a significant enhancement over the `0.15.0` baseline with 14+ major features. Most features are cleanly isolated in new files, making selective upstream contribution feasible.
**Recommended approach:**
1. First, bring crosspoint-ef up to date with 0.16.0 bug fixes
2. Then, evaluate individual features for upstream PR submission
3. Prioritize dictionary, bookmarks, and CSS parsing as highest-value contributions
The grayscale state corruption fix in crosspoint-ef should also be submitted upstream as a critical bug fix, as it prevents display artifacts under memory pressure.

View File

@@ -0,0 +1,603 @@
# CrossPoint-EF Branch Features
This document describes the features and enhancements unique to the `crosspoint-ef` branch, which diverged from CrossPoint Reader at version `0.15.0`.
## Overview
The `crosspoint-ef` branch introduces significant new functionality including:
- **Dictionary Support** - Offline StarDict dictionary with word selection from reader
- **Bookmark System** - Per-book bookmarks with visual indicators and management
- **Quick Menu** - Fast access to common actions via power button
- **Library Search** - Search across all books by title, author, or filename
- **CSS Support** - Parse and apply CSS styles from EPUB files
- **Inline Images** - PNG and Baseline JPEG image rendering within EPUBs
- **Custom Fonts** - Atkinson Hyperlegible Next and Fern Micro accessibility fonts
- **Enhanced Web Server** - File management, companion app API, mDNS discovery
- **Reading Lists** - Create, manage, and pin custom book lists
- **Display Enhancements** - High contrast mode, bezel compensation, sleep screen improvements
---
## Major Features
### 1. Dictionary Support
Full offline dictionary lookup using the StarDict format with fast prefix-indexed search.
**Features:**
- Word selection directly from EPUB pages
- Manual word entry via on-screen keyboard
- Rich HTML formatting in definitions (bold, italic, lists)
- Multi-page definitions with pagination
- Synonym support
- Case-insensitive search with prefix optimization
**Access Methods:**
- **Quick Menu** → Dictionary
- **Power Button** (when configured to `Dictionary` action)
**Dictionary Format:**
- StarDict format with dictzip compression
- Files located at `/dictionaries/dict-data` on SD card:
- `dict-data.ifo` - Metadata
- `dict-data.idx` - Word index
- `dict-data.dict.dz` - Compressed definitions
- `dict-data.syn` - Synonyms (optional)
**Technical Implementation:**
- Files: `src/activities/dictionary/`, `lib/StarDict/`
- Prefix jump table for near-instant lookups
- On-demand chunk decompression using miniz
- HTML definition parsing with entity decoding
---
### 2. Bookmark System
Per-book bookmark storage with visual indicators and dedicated management interface.
**Features:**
- Add/remove bookmarks from current page
- Visual folded-corner indicator on bookmarked pages
- Bookmarks tab in library showing all books with bookmarks
- Long-press to delete bookmarks
- Auto-generated bookmark names ("Chapter Title - Page X")
- Maximum 100 bookmarks per book
**Storage:**
- Binary file per book: `/.crosspoint/{epub_|txt_}<hash>/bookmarks.bin`
- Stores: name, spine index, content offset, page number, timestamp
**Technical Implementation:**
- Files: `src/BookmarkStore.cpp/.h`, `src/activities/home/BookmarkListActivity.cpp/.h`
- Bookmark identification by `spineIndex + contentOffset` (stable across re-renders)
---
### 3. Quick Menu
In-reader quick access menu for common actions, triggered by short power button press.
**Menu Options:**
1. **Dictionary** - Look up a word
2. **Bookmark** - Add/Remove bookmark (state-aware text)
3. **Clear Cache** - Free up storage space
4. **Settings** - Open settings menu
**Configuration:**
- Settings → Controls → Short Power Button Click → `Quick Menu`
**Technical Implementation:**
- File: `src/activities/util/QuickMenuActivity.cpp/.h`
- Renders overlay with navigation and selection
---
### 4. Library Search with Character Picker
Search across all books using a dynamic character picker interface.
**Features:**
- Character picker with dynamically generated character set from library content
- Weighted search scoring:
- Title match: 100 points (+50 if at start)
- Author match: 80 points (+40 if at start)
- Path match: 30 points
- Results sorted by relevance score
- Special controls: SPC (space), ← (backspace), CLR (clear)
**Navigation:**
- Left/Right: Select character
- Confirm: Add character to query
- Up/Down: Switch between picker and results
**Technical Implementation:**
- Integrated in `src/activities/home/MyLibraryActivity.cpp`
- Search tab accessible from library tab bar
---
### 5. CSS Support for EPUBs
Parse and apply CSS styles from EPUB stylesheets.
**Supported Selectors:**
- Element selectors: `p`, `div`, `h1`, etc.
- Class selectors: `.classname`
- Combined selectors: `element.classname`
- Grouped selectors: `h1, h2, h3`
- Inline styles: `style="..."`
**Supported Properties:**
- `text-align` (left, center, right, justify)
- `font-style` (normal, italic)
- `font-weight` (normal, bold)
- `text-decoration` (underline)
- `text-indent`
- `margin-top`, `margin-bottom`
- `padding-top`, `padding-bottom`
**Cascade Order:**
1. Element styles
2. Class styles
3. Element.class styles
4. Inline styles
**Technical Implementation:**
- Files: `lib/Epub/Epub/css/CssParser.cpp/.h`, `CssStyle.h`
- CSS files parsed during EPUB loading
- Styles applied during HTML parsing via `ChapterHtmlSlimParser`
---
### 6. Inline Image Support (PNG/Baseline JPEG)
Render embedded images within EPUB content.
**Supported Formats:**
- Baseline JPEG (.jpg, .jpeg)
- PNG (.png)
**Features:**
- Images decoded to 2-bit grayscale with dithering
- Image caching as `.pxc` files (2 bits per pixel, packed format)
- Row-by-row rendering to minimize memory usage
- Automatic scaling to fit page width
**Technical Implementation:**
- Files: `lib/Epub/Epub/blocks/ImageBlock.cpp/.h`
- Converters: `JpegToFramebufferConverter`, `PngToFramebufferConverter`
- Factory: `ImageDecoderFactory` routes to appropriate decoder
---
### 7. Custom Fonts
Additional accessibility-focused fonts beyond the standard Bookerly and Noto Sans.
**Available Fonts:**
1. **Atkinson Hyperlegible Next** - Designed for low-vision readers
2. **Fern Micro** - Optimized for small screens
**Font Sizes:**
- 12pt, 14pt, 16pt, 18pt for each font
- Full style support: Regular, Italic, Bold, Bold Italic
**Configuration:**
- Settings → Reader → Font Family → Custom
- Settings → Reader → Custom Font → [Select font]
- Settings → Reader → Fallback Font → [Bookerly/Noto Sans]
**Technical Implementation:**
- Files: `src/customFonts.cpp`, `src/fontIds.h`
- Font headers: `lib/EpdFont/builtinFonts/custom/`
- Conversion scripts: `lib/EpdFont/scripts/convert-builtin-fonts.sh`
---
### 8. Enhanced Web Server
Extended web server with file management operations and companion app support.
**File Operations:**
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/files` | GET | List files with MD5 hashes for EPUBs |
| `/api/status` | GET | Device status (version, IP, heap, uptime) |
| `/api/archived` | GET | List archived books |
| `/api/hash` | GET | Compute/retrieve MD5 hash for sync |
| `/download` | GET | Download files |
| `/upload` | POST | Upload files (multipart) |
| `/delete` | POST | Delete files/folders |
| `/archive` | POST | Archive a book |
| `/unarchive` | POST | Restore archived book |
| `/rename` | POST | Rename files/folders |
| `/copy` | POST | Copy files/folders |
| `/move` | POST | Move files/folders |
| `/mkdir` | POST | Create folders |
| `/list` | GET/POST | Manage reading lists |
**WebSocket Upload (Port 81):**
- Fast binary uploads for large files
- Protocol: `START:<filename>:<size>:<path>``READY` → binary chunks → `DONE`
- Progress updates: `PROGRESS:<received>:<total>`
**mDNS Discovery:**
- Hostname: `crosspoint.local`
- Service: `_http._tcp` on port 80
- UDP discovery on port 8134
**Deep Link Support:**
- URL scheme: `crosspoint://<path>?host=<ip>&port=<port>&wsPort=<wsPort>`
- Paths: `files`, `library`, `lists`, `settings`
**Technical Implementation:**
- Files: `src/network/CrossPointWebServer.cpp/.h`
- MD5 Utils: `src/util/Md5Utils.cpp/.h`
- Docs: `docs/webserver-api-reference.md`, `docs/companion-app-deep-link-API.md`
---
### 9. Reading Lists with Pinning
Create and manage custom book lists with pinning support.
**Features:**
- Create, load, delete lists
- Pin a list to show on home screen
- List contents displayed with book metadata
- Web API for list upload/download (CSV format)
**Storage:**
- Lists stored in `/.lists/` as `.bin` files
- CSV format for API: `order,title,author,path`
**Configuration:**
- Library → Lists tab → Long-press → Pin/Unpin
- Pinned list name shown on home screen Lists button
**Technical Implementation:**
- Files: `src/BookListStore.cpp/.h`
- Pinned list stored in `SETTINGS.pinnedListName`
---
### 10. Display Enhancements
#### High Contrast Mode
System-wide display contrast adjustment for improved readability.
- **Normal mode:** Standard grayscale thresholds
- **High contrast mode:** Pushes mid-grays toward black/white
**Configuration:**
- Settings → Display → High Contrast → On/Off
#### Bezel Compensation
Compensate for physical screen edge defects with configurable margin.
- **Range:** 0-10 pixels
- **Edges:** Bottom, Top, Left, Right
- **Behavior:** Margin rotates with screen orientation
**Configuration:**
- Settings → Display → Bezel Compensation → [0-10]
- Settings → Display → Bezel Edge → [Bottom/Top/Left/Right]
#### Sleep Screen Improvements
Enhanced sleep screen with edge-aware color filling.
- **Edge luminance detection:** Samples edge pixels for dominant color
- **Letterbox fill:** Fills letterbox regions with edge colors for seamless appearance
- **Two-level caching:**
- Per-BMP cache: `.bmp.perim` files store edge luminance
- Book-level cache: `edge.bin` stores cover data
---
### 11. Recents View Improvements
Enhanced recent books display with metadata and management.
**Features:**
- **Badges:** Extension and suffix tags (e.g., "epub", "X4")
- **Metadata display:** Title and author from EPUB
- **Remove from recents:** Long-press → Remove from Recents
- **Clear all:** Long-press → Clear All Recents
**Badge Configuration:**
- Extension badges: `.epub` → "epub", `.txt` → "txt"
- Suffix badges: `-x4` → "X4", `-x4p` → "X4P"
**Technical Implementation:**
- Files: `src/RecentBooksStore.cpp/.h`
- Badge config: `src/BadgeConfig.h`
- String utils: `src/util/StringUtils.cpp/.h`
---
### 12. Enhanced Tab Bar
Unified tab bar with scrolling and overflow indicators.
**Features:**
- Horizontal scrolling when tabs exceed available width
- Overflow indicators (< >) when content extends beyond view
- Selected tab highlighting with underline
- Bullet cursors for focus mode
**Tabs:**
- Recent, Lists, Bookmarks, Search, Files
**Technical Implementation:**
- Files: `src/ScreenComponents.cpp/.h`
- Used in `MyLibraryActivity` for library navigation
---
### 13. Progress Bar Status Bar
Additional status bar option showing visual progress bar.
**Options:**
- `Full w/ Progress Bar` - Full status bar with progress bar
- `Progress Bar` - Only progress bar, no other status info
**Configuration:**
- Settings → Display → Status Bar
---
### 14. Additional Settings
New configuration options unique to crosspoint-ef:
| Setting | Options | Description |
|---------|---------|-------------|
| Short Power Button Click | Ignore, Sleep, Page Turn, Dictionary, Quick Menu | Map power button to action |
| Bezel Compensation | 0-10 pixels | Edge defect compensation |
| Bezel Edge | Bottom, Top, Left, Right | Which edge to compensate |
| High Contrast | Off, On | System-wide contrast boost |
| Custom Font | [Font list] | Select custom font |
| Fallback Font | Bookerly, Noto Sans | Fallback for custom fonts |
---
### 15. OPDS Browser Enhancements
Improved OPDS catalog browsing experience.
**Features:**
- **Navigation history stack** - Back button navigates through visited feeds
- **Page skipping** - Hold Up/Down for 700ms to skip 23 items at once
- **Error retry mechanism** - Retry button on error screens with WiFi status check
- **HTTP Basic Authentication** - Username/password support for protected OPDS servers
**Configuration:**
- Settings → System → Calibre Settings → OPDS Server URL
- Settings → System → Calibre Settings → Username/Password
**Technical Implementation:**
- Files: `src/activities/browser/OpdsBookBrowserActivity.cpp`
- Authentication: `src/network/HttpDownloader.cpp`
---
### 16. Development Tools
Scripts and utilities for firmware development.
**Scripts:**
| Script | Purpose |
|--------|---------|
| `pre_flash.py` | Displays "Flashing firmware..." screen during upload |
| `debugging_monitor.py` | Enhanced serial monitor with memory graphs and color output |
| `pio_helper.py` | Interactive PlatformIO workflow helper with presets |
| `version_hash.py` | Embeds git commit hash in dev builds |
| `build_html.py` | Minifies HTML files for web server |
**Firmware Flashing Screen:**
- Full-screen display during firmware upload
- Shows "Flashing firmware..." with version info
- Lock icon indicates USB port location
- Prevents accidental disconnection
**Debug/Memory Monitoring:**
- `DEBUG_MEMORY` build mode for heap tracking at activity transitions
- Periodic memory logging every 10 seconds (when Serial connected)
- Loop duration warnings when exceeding 50ms
- Detailed heap fragmentation info
**Technical Implementation:**
- Scripts: `scripts/pre_flash.py`, `scripts/debugging_monitor.py`, `scripts/pio_helper.py`
- Flash screen: `src/main.cpp` (lines 138-247)
- Memory monitoring: `src/main.cpp` (lines 549-562)
- Build config: `platformio.ini` (`debug_memory` environment)
---
### 17. Power Management Enhancements
Optimizations for battery life and responsiveness.
**Features:**
- **Auto-sleep prevention** - Background tasks (web server, OTA, downloads) prevent auto-sleep
- **USB connection detection** - Serial only starts when USB is connected (saves power)
- **Skip loop delay** - Activities can request faster loop execution for responsive HTTP handling
- **Power button release wait** - Prevents immediate wake if button is still held
**Technical Implementation:**
- Files: `src/main.cpp`, `src/activities/Activity.h`
- Methods: `preventAutoSleep()`, `skipLoopDelay()`
---
## Bug Fixes
### Unique to crosspoint-ef
1. **Grayscale state corruption fix** - Prevents ghosting and gray filter artifacts when anti-aliasing is enabled under memory pressure
2. **Memory optimization** - Graceful degradation when memory is low (skip anti-aliasing instead of corrupting display)
### Shared with 0.16.0
- Large EPUB indexing optimization (O(n²) → O(n))
- Settings validation on read
- Line break fixes (flush word before `<br/>`)
- Rotate origin in `drawImage()`
- Short-press power button to wakeup
- Add txt books to recent tab
- B&W filters for cover images
---
## Missing or Removed Features from 0.16.0
The following features are present in the upstream `0.16.0` release but are missing or were removed in `crosspoint-ef`:
### Removed Features
#### 1. KOReader Sync Support (Removed)
The entire KOReader sync functionality has been removed.
**What was removed:**
- `lib/KOReaderSync/` library (8 files deleted)
- Progress sync with KOReader sync server (`sync.koreader.rocks`)
- Document MD5 binary matching for progress synchronization
- KOReader credential storage
**Impact:**
- Cannot sync reading progress with KOReader app
- Chapter Selection UI fixes for KOReader sync (#501) not applicable
**Files deleted:**
- `KOReaderSyncClient.cpp/.h`
- `KOReaderCredentialStore.cpp/.h`
- `KOReaderDocumentId.cpp/.h`
- `ProgressMapper.cpp/.h`
---
#### 2. Non-English Hyphenation Patterns (Removed)
Hyphenation pattern files for non-English languages have been removed.
**What was removed:**
- `hyph-es.trie.h` - Spanish hyphenation
- `hyph-de.trie.h` - German hyphenation
- `hyph-fr.trie.h` - French hyphenation
- `hyph-ru.trie.h` - Russian hyphenation
**Impact:**
- Only English hyphenation patterns remain
- Non-English books will not hyphenate correctly
- Spanish hyphenation support (#558) not available
**Note:** These can be restored by copying the trie files from 0.16.0.
---
#### 3. XTC/XTCH File Support (Removed)
Support for the XTC/XTCH proprietary format has been removed.
**What was removed:**
- Author extraction from XTC/XTCH files (#563)
- XTC format handling in file browsers
**Impact:**
- XTC/XTCH files cannot be read
- Author metadata not extracted from these formats
---
### Missing Bug Fixes
The following bug fixes from 0.16.0 have not been applied to crosspoint-ef:
| PR | Description | Impact |
|----|-------------|--------|
| #567 | Multi-line keyboard entry | Long text input truncated with "..." instead of wrapping |
| #569 | Italics on image alt text | Image alt placeholders don't render in italics |
| #564 | Front layout in mapLabels() | Button mapping may be incorrect in some layouts |
| #486 | Relative position on settings change | Reader may jump to different location when settings change |
| #501 | Chapter Selection UI (KOReader) | N/A - KOReader sync removed |
| #529 | KOReader MD5 binary matching | N/A - KOReader sync removed |
---
### Missing UX Enhancements
| PR | Description | Impact |
|----|-------------|--------|
| #451 | Page turn on button press | When long-press chapter skip is disabled, 0.16.0 allows page turn on button press; crosspoint-ef does not |
---
### Different Implementation: OTA Updates
The OTA update mechanism uses a different implementation:
| Aspect | crosspoint-ef | 0.16.0 |
|--------|---------------|--------|
| HTTP Client | Arduino `HTTPClient` | ESP-IDF `esp_http_client` |
| OTA Library | Arduino `Update` | ESP-IDF `esp_https_ota` |
| Memory Management | Standard | Improved with custom buffer handling |
**Impact:**
- Both implementations work, but 0.16.0's ESP-IDF approach may be more memory-efficient
- Consider evaluating 0.16.0's OTA rework (#509) for potential adoption
---
### Recommendation for Missing Features
**High Priority to Cherry-pick:**
1. Multi-line keyboard entry (#567) - Improves UX for long inputs
2. Front layout fix (#564) - Bug fix for button mapping
3. Relative position on settings change (#486) - Improves reader UX
**Medium Priority:**
4. Restore hyphenation patterns for non-English languages
5. Italics on image alt (#569) - Minor visual improvement
6. Page turn on button press (#451) - UX enhancement
**Evaluate:**
7. OTA rework (#509) - Compare implementations for memory benefits
8. KOReader sync - Restore if sync functionality is desired
---
## File Summary
| Feature | Primary Files |
|---------|---------------|
| Dictionary | `src/activities/dictionary/`, `lib/StarDict/` |
| Bookmarks | `src/BookmarkStore.*`, `src/activities/home/BookmarkListActivity.*` |
| Quick Menu | `src/activities/util/QuickMenuActivity.*` |
| Search | `src/activities/home/MyLibraryActivity.cpp` |
| CSS | `lib/Epub/Epub/css/` |
| Images | `lib/Epub/Epub/blocks/ImageBlock.*`, `lib/Epub/Epub/converters/` |
| Custom Fonts | `src/customFonts.cpp`, `lib/EpdFont/builtinFonts/custom/` |
| Web Server | `src/network/CrossPointWebServer.*`, `src/util/Md5Utils.*` |
| Lists | `src/BookListStore.*` |
| Settings | `src/CrossPointSettings.*` |
| Tab Bar | `src/ScreenComponents.*` |
| Recents | `src/RecentBooksStore.*`, `src/BadgeConfig.h` |
| OPDS Browser | `src/activities/browser/OpdsBookBrowserActivity.*` |
| Dev Tools | `scripts/pre_flash.py`, `scripts/debugging_monitor.py`, `scripts/pio_helper.py` |
| Power Management | `src/main.cpp`, `src/activities/Activity.h` |
---
## Version Information
- **Base version:** 0.15.0
- **Branch:** crosspoint-ef
- **Commits since divergence:** 90+
- **Files changed:** 250+

View File

@@ -0,0 +1,555 @@
# CrossPoint-EF User Guide Supplement
This guide covers the additional features available in the `crosspoint-ef` branch. For basic operation, refer to the main [User Guide](../USER_GUIDE.md).
## Table of Contents
- [Dictionary](#dictionary)
- [Bookmarks](#bookmarks)
- [Quick Menu](#quick-menu)
- [Library Search](#library-search)
- [Reading Lists](#reading-lists)
- [Display Settings](#display-settings)
- [Web Server Features](#web-server-features)
- [Custom Fonts](#custom-fonts)
- [Additional Settings](#additional-settings)
---
## Dictionary
The dictionary feature provides offline word lookup while reading.
### Setup
1. Download a StarDict dictionary (English-English dictionary provided as `dict-en-en.zip`)
2. Extract the dictionary files to `/dictionaries/dict-data/` on your SD card
3. You should have these files:
- `dict-data.ifo`
- `dict-data.idx`
- `dict-data.dict.dz`
- `dict-data.syn` (optional, for synonyms)
### Using the Dictionary
#### Method 1: Quick Menu
1. While reading, press the **Power** button briefly (requires Quick Menu to be configured)
2. Select **Dictionary** from the menu
3. Choose **Select from Screen** or **Enter a Word**
#### Method 2: Direct Power Button Access
1. Go to **Settings → Controls → Short Power Button Click**
2. Set to **Dictionary**
3. While reading, press the **Power** button briefly to open the dictionary
### Selecting a Word from the Page
1. Choose **Select from Screen** from the dictionary menu
2. The current page will display with word selection enabled
3. Use **Left/Right** to move between words
4. Use **Up/Down** to jump between lines
5. Press **Confirm** to look up the selected word
6. Press **Back** to cancel
### Viewing Definitions
- Definitions display with rich formatting (bold, italic, lists)
- Use **Left/Right** or **Volume Up/Down** to navigate between pages if the definition is long
- Press **Confirm** to search for another word
- Press **Back** to return to your book
---
## Bookmarks
Create and manage bookmarks within your books.
### Adding a Bookmark
#### Method 1: Quick Menu
1. Press the **Power** button briefly (requires Quick Menu to be configured)
2. Select **Add Bookmark** (or **Remove Bookmark** if already bookmarked)
#### Method 2: Settings Configuration
1. Go to **Settings → Controls → Short Power Button Click**
2. Set to **Quick Menu**
3. Use Quick Menu to toggle bookmarks
### Bookmark Indicator
When a page is bookmarked, a small folded corner triangle appears in the top-right corner of the page.
### Viewing Bookmarks
1. Go to **Home → Library**
2. Select the **Bookmarks** tab
3. You'll see a list of books that have bookmarks
4. Select a book to view its bookmarks
5. Select a bookmark to jump to that location
### Deleting Bookmarks
1. Open a book's bookmark list (from Bookmarks tab)
2. Navigate to the bookmark you want to delete
3. **Long-press Confirm** (hold for about 1 second)
4. Confirm deletion when prompted
### Bookmark Naming
Bookmarks are automatically named based on:
- Chapter title and page number (e.g., "Chapter 3 - Page 42")
- Just page number if no chapter title (e.g., "Page 15")
---
## Quick Menu
Fast access to common actions while reading.
### Enabling Quick Menu
1. Go to **Settings → Controls → Short Power Button Click**
2. Select **Quick Menu**
### Using Quick Menu
1. While reading, press the **Power** button briefly
2. Navigate with **Up/Down** or **Left/Right**
3. Press **Confirm** to select an option
4. Press **Back** to close the menu
### Quick Menu Options
| Option | Description |
|--------|-------------|
| **Dictionary** | Look up a word |
| **Add/Remove Bookmark** | Toggle bookmark on current page |
| **Clear Cache** | Free up storage space |
| **Settings** | Open settings menu |
---
## Library Search
Search your library by title, author, or filename.
### Accessing Search
1. Go to **Home → Library**
2. Select the **Search** tab
3. Or from any tab, scroll to the bottom and select **Search...**
### Using the Character Picker
The search uses a character picker interface:
1. **Left/Right** - Move between characters
2. **Confirm** - Add character to search query
3. **SPC** - Add a space
4. **←** - Delete last character (backspace)
5. **CLR** - Clear entire query
### Navigating Results
1. After entering characters, results appear below
2. Press **Down** to move from character picker to results
3. **Left/Right** to navigate results
4. **Confirm** to open a book
5. **Up** to return to character picker
### Search Scoring
Results are ranked by relevance:
- Title matches rank highest
- Author matches rank second
- Filename matches rank lowest
- Matches at the start of a field rank higher
---
## Reading Lists
Create custom book lists for organizing your library.
### Viewing Lists
1. Go to **Home → Library**
2. Select the **Lists** tab
3. Available lists are displayed
### Opening a List
1. Navigate to a list name
2. Press **Confirm** to view the list contents
3. Select a book to start reading
### Pinning a List
Pin a list to quickly access it from the home screen:
1. In the Lists tab, navigate to a list
2. **Long-press Confirm** to open the action menu
3. Select **Pin List**
The pinned list name will appear on the Lists button on the home screen.
### Unpinning a List
1. Navigate to the pinned list
2. **Long-press Confirm**
3. Select **Unpin List**
### Deleting a List
1. Navigate to a list
2. **Long-press Confirm**
3. Select **Delete List**
4. Confirm deletion
### Creating Lists via Web Server
Lists can be created and uploaded via the web server API. See [Web Server Features](#web-server-features).
---
## Display Settings
### High Contrast Mode
Increases contrast across the entire UI for better readability.
1. Go to **Settings → Display → High Contrast**
2. Set to **On** or **Off**
When enabled, mid-gray tones are pushed toward black or white.
### Bezel Compensation
Compensate for physical screen edge defects (common on some devices).
1. Go to **Settings → Display → Bezel Compensation**
2. Set value from **0** (disabled) to **10** pixels
3. If compensation is enabled, select **Bezel Edge**:
- **Bottom** - Default, compensates bottom edge
- **Top** - Compensates top edge
- **Left** - Compensates left edge
- **Right** - Compensates right edge
The compensation margin automatically rotates with screen orientation.
### Status Bar Options
Additional status bar display options:
| Option | Description |
|--------|-------------|
| None | No status bar |
| No Progress | Status bar without reading progress |
| Full w/ Percentage | Status bar with percentage progress |
| Full w/ Progress Bar | Status bar with visual progress bar |
| Progress Bar | Only progress bar, no other info |
Configure at **Settings → Display → Status Bar**.
### Sleep Screen Cover Filter
When using book cover as sleep screen:
| Filter | Effect |
|--------|--------|
| None | Grayscale image as-is |
| Contrast | Black and white only (no grays) |
| Inverted | Inverted black and white |
Configure at **Settings → Display → Sleep Screen Cover Filter**.
---
## Web Server Features
The web server provides extended file management and companion app support.
### Starting the Web Server
1. Go to **Home → File Transfer**
2. Select a WiFi network or create a hotspot
3. The web server URL will be displayed
### File Management
Access the file manager at `http://<device-ip>/files`
**Available Operations:**
- **Upload** - Upload files via drag-and-drop or file picker
- **Download** - Download files to your computer
- **Delete** - Remove files and folders
- **Rename** - Rename files and folders
- **Create Folder** - Create new directories
- **Archive/Unarchive** - Archive books (preserves reading progress)
- **Copy/Move** - Copy or move files and folders
### API Access
The web server provides a JSON API for programmatic access:
| Endpoint | Description |
|----------|-------------|
| `GET /api/status` | Device status |
| `GET /api/files?path=/` | List files |
| `GET /api/archived` | List archived books |
| `GET /api/hash?path=/book.epub` | Get MD5 hash |
### mDNS Discovery
The device advertises itself as `crosspoint.local` on your network.
### Companion App Support
The web server supports the CrossPoint Companion Android app:
1. **QR Code** - Scan the QR code displayed on the web server screen
2. **Deep Links** - URLs like `crosspoint://files?host=192.168.1.100` open the app directly
### Managing Reading Lists via API
**Get all lists:**
```
GET /list
```
**Get specific list:**
```
GET /list?name=MyList
```
**Upload a list:**
```
POST /list?action=upload&name=MyList
Content-Type: text/plain
1,Book Title,Author Name,/path/to/book.epub
2,Another Book,Another Author,/path/to/another.epub
```
**Delete a list:**
```
POST /list?action=delete&name=MyList
```
---
## Custom Fonts
Two additional accessibility-focused fonts are available.
### Available Custom Fonts
1. **Atkinson Hyperlegible Next** - Designed for low-vision readers with high character differentiation
2. **Fern Micro** - Optimized for small screens
### Enabling Custom Fonts
1. Go to **Settings → Reader → Font Family**
2. Select **Custom**
3. Go to **Settings → Reader → Custom Font**
4. Select your preferred font
### Fallback Font
When using custom fonts, set a fallback for missing glyphs:
1. Go to **Settings → Reader → Fallback Font**
2. Choose **Bookerly** or **Noto Sans**
---
## Additional Settings
### Short Power Button Actions
Configure what happens when you briefly press the Power button:
| Option | Action |
|--------|--------|
| Ignore | No action (default) |
| Sleep | Put device to sleep |
| Page Turn | Turn to next page |
| Dictionary | Open dictionary |
| Quick Menu | Open quick menu |
Configure at **Settings → Controls → Short Power Button Click**.
### Long-press Chapter Skip
Control side button long-press behavior:
- **On** (default) - Long-press Volume buttons to skip chapters
- **Off** - Long-press scrolls a page instead
Configure at **Settings → Controls → Long-press Chapter Skip**.
### Hyphenation
Enable word hyphenation for justified text:
1. Go to **Settings → Reader → Hyphenation**
2. Set to **On**
Hyphenation patterns are available for multiple languages (English, German, French, Spanish, Russian, etc.).
---
## Recents View Enhancements
### Badges
Books in the Recent tab display badges showing:
- **File extension** (epub, txt, md)
- **Suffix tags** (X4, X4P for files with `-x4` or `-x4p` suffixes)
### Removing from Recents
1. Navigate to a book in the Recent tab
2. **Long-press Confirm**
3. Select **Remove from Recents**
### Clearing All Recents
1. Navigate to any book in the Recent tab
2. **Long-press Confirm**
3. Select **Clear All Recents**
4. Confirm the action
---
## Tab Navigation
The library uses a unified tab bar for navigation.
### Tabs Available
| Tab | Contents |
|-----|----------|
| Recent | Recently opened books |
| Lists | Custom reading lists |
| Bookmarks | Books with bookmarks |
| Search | Search all books |
| Files | File browser |
### Navigating Tabs
When the tab bar is focused:
- **Left/Right** - Switch between tabs
- **Down** - Enter the selected tab's content
- **Confirm** - Same as Down
### Tab Overflow
When tabs don't fit on screen:
- **<** indicator appears on left when more tabs exist to the left
- **>** indicator appears on right when more tabs exist to the right
- Scroll continues automatically when navigating past visible tabs
---
## Inline Images
EPUBs with embedded images now display them inline with text.
### Supported Formats
- JPEG (.jpg, .jpeg)
- PNG (.png)
### Image Display
- Images are automatically scaled to fit the page width
- Images are converted to 4-level grayscale with dithering
- First load may be slower as images are processed
- Subsequent loads use cached versions
### Image Cache
Processed images are cached as `.pxc` files in the book's cache directory for faster loading.
---
## Troubleshooting
### Dictionary Not Working
1. Verify dictionary files are in `/dictionaries/dict-data/`
2. Check that all required files exist (.ifo, .idx, .dict.dz)
3. File names must match exactly (case-sensitive)
### Bookmarks Not Saving
1. Ensure SD card is not write-protected
2. Check available storage space
3. Bookmarks are saved per-book in `/.crosspoint/`
### Search Not Finding Books
1. Search only indexes books in the library
2. Ensure books have proper EPUB metadata
3. Try searching by filename if metadata is missing
### Images Not Displaying
1. Only PNG and JPEG formats are supported
2. Very large images may fail to load due to memory constraints
3. Check for sufficient free memory (multiple large books open may exhaust memory)
### Web Server Connection Issues
1. Ensure device and computer are on the same network
2. Try accessing via IP address instead of `crosspoint.local`
3. Check that firewall isn't blocking port 80
---
## Keyboard Shortcuts Summary
### In Reader
| Button | Action |
|--------|--------|
| Left/Volume Up | Previous page |
| Right/Volume Down | Next page |
| Left (hold) | Previous chapter |
| Right (hold) | Next chapter |
| Back | Return to library |
| Back (hold) | Return to home |
| Confirm | Open chapter selection |
| Power (brief) | Configured action (Quick Menu/Dictionary/Sleep/Page Turn) |
### In Quick Menu
| Button | Action |
|--------|--------|
| Up/Down/Left/Right | Navigate options |
| Confirm | Select option |
| Back | Close menu |
### In Word Selection
| Button | Action |
|--------|--------|
| Left/Right | Move between words |
| Up/Down | Move between lines |
| Confirm | Look up word |
| Back | Cancel |
### In Library Tabs
| Button | Action |
|--------|--------|
| Left/Right | Switch tabs (when tab bar focused) |
| Up/Down | Navigate within tab |
| Confirm | Select item / Enter tab |
| Confirm (hold) | Action menu |
| Back | Go back / Exit to home |

0
ef.md
View File

View File

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

View File

@@ -57,12 +57,12 @@ class Page {
public:
// the list of block index and line numbers on this page
std::vector<std::shared_ptr<PageElement>> elements;
// Byte offset in source HTML where this page's content begins
// Used for restoring reading position after re-indexing due to font/setting changes
// This is stored in the Section file's LUT, not in Page serialization
uint32_t firstContentOffset = 0;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool serialize(FsFile& file) const;
static std::unique_ptr<Page> deserialize(FsFile& file);

View File

@@ -186,7 +186,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
}
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
viewportHeight, hyphenationEnabled);
// LUT entries: { filePosition, contentOffset } pairs
struct LutEntry {
uint32_t filePos;
@@ -202,8 +202,8 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
const uint32_t contentOffset = page->firstContentOffset;
const uint32_t filePos = this->onPageComplete(std::move(page));
lut.push_back({filePos, contentOffset});
}, progressFn,
epub->getCssParser());
},
progressFn, epub->getCssParser());
Hyphenator::setPreferredLanguage(epub->getLanguage());
success = visitor.parseAndBuildPages();
@@ -217,7 +217,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
// Add placeholder to LUT
const uint32_t filePos = this->onPageComplete(std::move(placeholderPage));
lut.push_back({filePos, 0});
// If we still have no pages, the placeholder creation failed
if (pageCount == 0) {
Serial.printf("[%lu] [SCT] Failed to create placeholder page\n", millis());
@@ -262,13 +262,13 @@ std::unique_ptr<Page> Section::loadPageFromSectionFile() {
file.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t lutOffset;
serialization::readPod(file, lutOffset);
// LUT entries are now 8 bytes each: { filePos (4), contentOffset (4) }
file.seek(lutOffset + LUT_ENTRY_SIZE * currentPage);
uint32_t pagePos;
serialization::readPod(file, pagePos);
// Skip contentOffset for now - we don't need it when just loading the page
file.seek(pagePos);
auto page = Page::deserialize(file);
@@ -300,15 +300,15 @@ int Section::findPageForContentOffset(uint32_t targetOffset) const {
while (left <= right) {
const int mid = left + (right - left) / 2;
// Read content offset for page 'mid'
// LUT entry format: { filePos (4), contentOffset (4) }
f.seek(lutOffset + LUT_ENTRY_SIZE * mid + sizeof(uint32_t)); // Skip filePos
uint32_t midOffset;
serialization::readPod(f, midOffset);
if (midOffset <= targetOffset) {
result = mid; // This page could be the answer
result = mid; // This page could be the answer
left = mid + 1; // Look for a later page that might also qualify
} else {
right = mid - 1; // Look for an earlier page
@@ -322,7 +322,7 @@ int Section::findPageForContentOffset(uint32_t targetOffset) const {
f.seek(lutOffset + LUT_ENTRY_SIZE * result + sizeof(uint32_t));
uint32_t resultOffset;
serialization::readPod(f, resultOffset);
while (result > 0) {
f.seek(lutOffset + LUT_ENTRY_SIZE * (result - 1) + sizeof(uint32_t));
uint32_t prevOffset;

View File

@@ -36,7 +36,7 @@ class Section {
const std::function<void()>& progressSetupFn = nullptr,
const std::function<void(int)>& progressFn = nullptr);
std::unique_ptr<Page> loadPageFromSectionFile();
// Methods for content offset-based position tracking
// Used to restore reading position after re-indexing due to font/setting changes
int findPageForContentOffset(uint32_t targetOffset) const;

View File

@@ -83,7 +83,7 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
// Flush the contents of partWordBuffer to currentTextBlock
void ChapterHtmlSlimParser::flushPartWordBuffer() {
if (partWordBufferIndex == 0) return;
// Determine font style using effective styles
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
if (effectiveBold && effectiveItalic) {
@@ -93,7 +93,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
} else if (effectiveItalic) {
fontStyle = EpdFontFamily::ITALIC;
}
// Flush the buffer
partWordBuffer[partWordBufferIndex] = '\0';
currentTextBlock->addWord(partWordBuffer, fontStyle, effectiveUnderline);
@@ -290,7 +290,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} else {
placeholder = "[Image unavailable]";
}
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
self->depth += 1;
@@ -478,7 +478,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
if (self->skipUntilDepth < self->depth) {
return;
}
// Capture byte offset of this character data for page position tracking
if (self->xmlParser) {
self->lastCharDataOffset = XML_GetCurrentByteIndex(self->xmlParser);
@@ -647,7 +647,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
const size_t totalSize = file.size();
size_t bytesRead = 0;
int lastProgress = -1;
// Initialize offset tracking - first page starts at offset 0
currentPageStartOffset = 0;
lastCharDataOffset = 0;
@@ -739,7 +739,7 @@ void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
currentPage->firstContentOffset = static_cast<uint32_t>(currentPageStartOffset);
}
completePageFn(std::move(currentPage));
// Start new page - offset will be set when first content is added
currentPage.reset(new Page());
currentPageStartOffset = lastCharDataOffset; // Use offset from when content was parsed

View File

@@ -58,11 +58,11 @@ class ChapterHtmlSlimParser {
bool effectiveBold = false;
bool effectiveItalic = false;
bool effectiveUnderline = false;
// Byte offset tracking for position restoration after re-indexing
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
size_t currentPageStartOffset = 0; // Byte offset when current page was started
size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing)
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
size_t currentPageStartOffset = 0; // Byte offset when current page was started
size_t lastCharDataOffset = 0; // Byte offset of last character data (captured during parsing)
void updateEffectiveInlineStyle();
void startNewTextBlock(TextBlock::Style style);

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ class GfxRenderer;
/**
* DictHtmlParser parses HTML dictionary definitions into ParsedText.
*
*
* Supports:
* - Bold: <b>, <strong>
* - Italic: <i>, <em>
@@ -25,7 +25,7 @@ class DictHtmlParser {
/**
* Parse HTML definition and populate ParsedText with styled words.
* Each paragraph/block creates a separate ParsedText via the callback.
*
*
* @param html The HTML definition text
* @param fontId Font ID for text width calculations
* @param renderer Reference to renderer for layout

View File

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

View File

@@ -32,7 +32,7 @@ class StarDict {
struct DictzipInfo {
uint32_t chunkLength = 0; // Uncompressed chunk size (usually 58315)
uint16_t chunkCount = 0;
uint32_t headerSize = 0; // Total header size to skip
uint32_t headerSize = 0; // Total header size to skip
uint16_t* chunkSizes = nullptr; // Array of compressed chunk sizes
bool loaded = false;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,11 +20,10 @@ constexpr int MAX_BOOKMARKS_PER_BOOK = 100;
// Get cache directory path for a book (same logic as BookManager)
std::string getCacheDir(const std::string& bookPath) {
const size_t hash = std::hash<std::string>{}(bookPath);
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
return "/.crosspoint/epub_" + std::to_string(hash);
} else if (StringUtils::checkFileExtension(bookPath, ".txt") ||
StringUtils::checkFileExtension(bookPath, ".TXT") ||
} else if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".TXT") ||
StringUtils::checkFileExtension(bookPath, ".md")) {
return "/.crosspoint/txt_" + std::to_string(hash);
}
@@ -47,21 +46,21 @@ std::vector<Bookmark> BookmarkStore::getBookmarks(const std::string& bookPath) {
bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) {
std::vector<Bookmark> bookmarks;
loadBookmarks(bookPath, bookmarks);
// Check if bookmark already exists at this location
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
return b.spineIndex == bookmark.spineIndex && b.contentOffset == bookmark.contentOffset;
});
if (it != bookmarks.end()) {
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n",
millis(), bookmark.spineIndex, bookmark.contentOffset);
Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n", millis(), bookmark.spineIndex,
bookmark.contentOffset);
return false;
}
// Add new bookmark
bookmarks.push_back(bookmark);
// Trim to max size (remove oldest)
if (bookmarks.size() > MAX_BOOKMARKS_PER_BOOK) {
// Sort by timestamp and remove oldest
@@ -70,100 +69,99 @@ bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& boo
});
bookmarks.resize(MAX_BOOKMARKS_PER_BOOK);
}
return saveBookmarks(bookPath, bookmarks);
}
bool BookmarkStore::removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
std::vector<Bookmark> bookmarks;
loadBookmarks(bookPath, bookmarks);
auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
});
if (it == bookmarks.end()) {
return false;
}
bookmarks.erase(it);
Serial.printf("[%lu] [BMS] Removed bookmark at spine %u, offset %u\n", millis(), spineIndex, contentOffset);
return saveBookmarks(bookPath, bookmarks);
}
bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) {
std::vector<Bookmark> bookmarks;
loadBookmarks(bookPath, bookmarks);
return std::any_of(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) {
return b.spineIndex == spineIndex && b.contentOffset == contentOffset;
});
return std::any_of(bookmarks.begin(), bookmarks.end(),
[&](const Bookmark& b) { return b.spineIndex == spineIndex && b.contentOffset == contentOffset; });
}
int BookmarkStore::getBookmarkCount(const std::string& bookPath) {
const std::string filePath = getBookmarksFilePath(bookPath);
if (filePath.empty()) return 0;
FsFile inputFile;
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
return 0;
}
uint8_t version;
serialization::readPod(inputFile, version);
if (version != BOOKMARKS_FILE_VERSION) {
inputFile.close();
return 0;
}
uint8_t count;
serialization::readPod(inputFile, count);
inputFile.close();
return count;
}
std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
std::vector<BookmarkedBook> result;
// Scan /.crosspoint/ directory for cache folders with bookmarks
auto crosspoint = SdMan.open("/.crosspoint");
if (!crosspoint || !crosspoint.isDirectory()) {
if (crosspoint) crosspoint.close();
return result;
}
crosspoint.rewindDirectory();
char name[256];
for (auto entry = crosspoint.openNextFile(); entry; entry = crosspoint.openNextFile()) {
entry.getName(name, sizeof(name));
if (!entry.isDirectory()) {
entry.close();
continue;
}
// Check if this directory has a bookmarks file
std::string dirPath = "/.crosspoint/";
dirPath += name;
std::string bookmarksPath = dirPath + "/" + BOOKMARKS_FILENAME;
if (SdMan.exists(bookmarksPath.c_str())) {
// Read the bookmarks file to get count and book info
FsFile bookmarksFile;
if (SdMan.openFileForRead("BMS", bookmarksPath, bookmarksFile)) {
uint8_t version;
serialization::readPod(bookmarksFile, version);
if (version == BOOKMARKS_FILE_VERSION) {
uint8_t count;
serialization::readPod(bookmarksFile, count);
// Read book metadata (stored at end of file)
std::string bookPath, bookTitle, bookAuthor;
// Skip bookmark entries to get to metadata
for (uint8_t i = 0; i < count; i++) {
std::string tempName;
@@ -176,12 +174,12 @@ std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
serialization::readPod(bookmarksFile, tempPage);
serialization::readPod(bookmarksFile, tempTimestamp);
}
// Read book metadata
serialization::readString(bookmarksFile, bookPath);
serialization::readString(bookmarksFile, bookTitle);
serialization::readString(bookmarksFile, bookAuthor);
if (!bookPath.empty() && count > 0) {
BookmarkedBook book;
book.path = bookPath;
@@ -197,19 +195,18 @@ std::vector<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
entry.close();
}
crosspoint.close();
// Sort by title
std::sort(result.begin(), result.end(), [](const BookmarkedBook& a, const BookmarkedBook& b) {
return a.title < b.title;
});
std::sort(result.begin(), result.end(),
[](const BookmarkedBook& a, const BookmarkedBook& b) { return a.title < b.title; });
return result;
}
void BookmarkStore::clearBookmarks(const std::string& bookPath) {
const std::string filePath = getBookmarksFilePath(bookPath);
if (filePath.empty()) return;
SdMan.remove(filePath.c_str());
Serial.printf("[%lu] [BMS] Cleared all bookmarks for %s\n", millis(), bookPath.c_str());
}
@@ -217,21 +214,21 @@ void BookmarkStore::clearBookmarks(const std::string& bookPath) {
bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks) {
const std::string cacheDir = getCacheDir(bookPath);
if (cacheDir.empty()) return false;
// Make sure the directory exists
SdMan.mkdir(cacheDir.c_str());
const std::string filePath = cacheDir + "/" + BOOKMARKS_FILENAME;
FsFile outputFile;
if (!SdMan.openFileForWrite("BMS", filePath, outputFile)) {
return false;
}
serialization::writePod(outputFile, BOOKMARKS_FILE_VERSION);
const uint8_t count = static_cast<uint8_t>(std::min(bookmarks.size(), static_cast<size_t>(255)));
serialization::writePod(outputFile, count);
for (size_t i = 0; i < count; i++) {
const auto& bookmark = bookmarks[i];
serialization::writeString(outputFile, bookmark.name);
@@ -240,7 +237,7 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector
serialization::writePod(outputFile, bookmark.pageNumber);
serialization::writePod(outputFile, bookmark.timestamp);
}
// Store book metadata at end (for getBooksWithBookmarks to read)
// Extract title from path if we don't have it
std::string title = bookPath;
@@ -252,11 +249,11 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector
if (dot != std::string::npos) {
title.resize(dot);
}
serialization::writeString(outputFile, bookPath);
serialization::writeString(outputFile, title);
serialization::writeString(outputFile, ""); // Author (not always available)
outputFile.close();
Serial.printf("[%lu] [BMS] Bookmarks saved for %s (%d entries)\n", millis(), bookPath.c_str(), count);
return true;
@@ -264,15 +261,15 @@ bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector
bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks) {
bookmarks.clear();
const std::string filePath = getBookmarksFilePath(bookPath);
if (filePath.empty()) return false;
FsFile inputFile;
if (!SdMan.openFileForRead("BMS", filePath, inputFile)) {
return false;
}
uint8_t version;
serialization::readPod(inputFile, version);
if (version != BOOKMARKS_FILE_VERSION) {
@@ -280,11 +277,11 @@ bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookm
inputFile.close();
return false;
}
uint8_t count;
serialization::readPod(inputFile, count);
bookmarks.reserve(count);
for (uint8_t i = 0; i < count; i++) {
Bookmark bookmark;
serialization::readString(inputFile, bookmark.name);
@@ -294,7 +291,7 @@ bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector<Bookm
serialization::readPod(inputFile, bookmark.timestamp);
bookmarks.push_back(bookmark);
}
inputFile.close();
Serial.printf("[%lu] [BMS] Bookmarks loaded for %s (%d entries)\n", millis(), bookPath.c_str(), count);
return true;

View File

@@ -7,12 +7,12 @@ struct BookmarkedBook;
// A single bookmark within a book
struct Bookmark {
std::string name; // Display name (e.g., "Chapter 1 - Page 42")
uint16_t spineIndex = 0; // For EPUB: which spine item
uint32_t contentOffset = 0; // Content offset for stable positioning
uint16_t pageNumber = 0; // Page number at time of bookmark (for display)
uint32_t timestamp = 0; // Unix timestamp when created
std::string name; // Display name (e.g., "Chapter 1 - Page 42")
uint16_t spineIndex = 0; // For EPUB: which spine item
uint32_t contentOffset = 0; // Content offset for stable positioning
uint16_t pageNumber = 0; // Page number at time of bookmark (for display)
uint32_t timestamp = 0; // Unix timestamp when created
bool operator==(const Bookmark& other) const {
return spineIndex == other.spineIndex && contentOffset == other.contentOffset;
}
@@ -22,7 +22,7 @@ struct Bookmark {
* BookmarkStore manages bookmarks for books.
* Bookmarks are stored per-book in the book's cache directory:
* /.crosspoint/{epub_|txt_}<hash>/bookmarks.bin
*
*
* This is a static utility class, not a singleton, since bookmarks
* are loaded/saved on demand for specific books.
*/
@@ -30,34 +30,34 @@ class BookmarkStore {
public:
// Get all bookmarks for a book
static std::vector<Bookmark> getBookmarks(const std::string& bookPath);
// Add a bookmark to a book
// Returns true if added, false if bookmark already exists at that location
static bool addBookmark(const std::string& bookPath, const Bookmark& bookmark);
// Remove a bookmark from a book by content offset
// Returns true if removed, false if not found
static bool removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
// Check if a specific page is bookmarked
static bool isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset);
// Get count of bookmarks for a book (without loading all data)
static int getBookmarkCount(const std::string& bookPath);
// Get all books that have bookmarks (for Bookmarks tab)
static std::vector<BookmarkedBook> getBooksWithBookmarks();
// Delete all bookmarks for a book
static void clearBookmarks(const std::string& bookPath);
private:
// Get the bookmarks file path for a book
static std::string getBookmarksFilePath(const std::string& bookPath);
// Save bookmarks to file
static bool saveBookmarks(const std::string& bookPath, const std::vector<Bookmark>& bookmarks);
// Load bookmarks from file
static bool loadBookmarks(const std::string& bookPath, std::vector<Bookmark>& bookmarks);
};

View File

@@ -193,7 +193,7 @@ bool CrossPointSettings::loadFromFile() {
float CrossPointSettings::getReaderLineCompression() const {
// For custom fonts, use the fallback font's line compression
const uint8_t effectiveFamily = (fontFamily == CUSTOM_FONT) ? fallbackFontFamily : fontFamily;
switch (effectiveFamily) {
case BOOKERLY:
default:

View File

@@ -26,7 +26,14 @@ class CrossPointSettings {
};
// Status bar display type enum
enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2, FULL_WITH_PROGRESS_BAR = 3, ONLY_PROGRESS_BAR = 4, STATUS_BAR_MODE_COUNT };
enum STATUS_BAR_MODE {
NONE = 0,
NO_PROGRESS = 1,
FULL = 2,
FULL_WITH_PROGRESS_BAR = 3,
ONLY_PROGRESS_BAR = 4,
STATUS_BAR_MODE_COUNT
};
enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default)
@@ -86,7 +93,7 @@ class CrossPointSettings {
};
// Short power button press actions
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, DICTIONARY = 3, SHORT_PWRBTN_COUNT };
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, DICTIONARY = 3, QUICK_MENU = 4, SHORT_PWRBTN_COUNT };
// Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
@@ -116,7 +123,7 @@ class CrossPointSettings {
uint8_t sideButtonLayout = PREV_NEXT;
// Reader font settings
uint8_t fontFamily = BOOKERLY;
uint8_t customFontIndex = 0; // Which custom font to use (0 to CUSTOM_FONT_COUNT-1)
uint8_t customFontIndex = 0; // Which custom font to use (0 to CUSTOM_FONT_COUNT-1)
uint8_t fallbackFontFamily = BOOKERLY; // Fallback for missing glyphs/weights in custom fonts
uint8_t fontSize = MEDIUM;
uint8_t lineSpacing = NORMAL;

View File

@@ -90,33 +90,156 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
}
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) {
constexpr int tabPadding = 20; // Horizontal padding between tabs
constexpr int leftMargin = 20; // Left margin for first tab
constexpr int underlineHeight = 2; // Height of selection underline
constexpr int underlineGap = 4; // Gap between text and underline
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs,
int selectedIndex, bool showCursor) {
constexpr int tabPadding = 20; // Horizontal padding between tabs
constexpr int leftMargin = 20; // Left margin for first tab
constexpr int rightMargin = 20; // Right margin
constexpr int underlineHeight = 2; // Height of selection underline
constexpr int underlineGap = 4; // Gap between text and underline
constexpr int cursorPadding = 4; // Space between bullet cursor and tab text
constexpr int overflowIndicatorWidth = 16; // Space reserved for < > indicators
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int tabBarHeight = lineHeight + underlineGap + underlineHeight;
const int screenWidth = renderer.getScreenWidth();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int availableWidth = screenWidth - bezelLeft - bezelRight - leftMargin - rightMargin;
int currentX = leftMargin;
// Find selected index if not provided
if (selectedIndex < 0) {
for (size_t i = 0; i < tabs.size(); i++) {
if (tabs[i].selected) {
selectedIndex = static_cast<int>(i);
break;
}
}
}
// Calculate total width of all tabs and individual tab widths
std::vector<int> tabWidths;
int totalWidth = 0;
for (const auto& tab : tabs) {
const int textWidth =
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
tabWidths.push_back(textWidth);
totalWidth += textWidth;
}
totalWidth += static_cast<int>(tabs.size() - 1) * tabPadding; // Add padding between tabs
// Draw tab label
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
// Calculate scroll offset to keep selected tab visible
int scrollOffset = 0;
if (totalWidth > availableWidth && selectedIndex >= 0) {
// Calculate position of selected tab
int selectedStart = 0;
for (int i = 0; i < selectedIndex; i++) {
selectedStart += tabWidths[i] + tabPadding;
}
int selectedEnd = selectedStart + tabWidths[selectedIndex];
// Draw underline for selected tab
if (tab.selected) {
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
// If selected tab would be cut off on the right, scroll left
if (selectedEnd > availableWidth) {
scrollOffset = selectedEnd - availableWidth + tabPadding;
}
// If selected tab would be cut off on the left (after scrolling), adjust
if (selectedStart - scrollOffset < 0) {
scrollOffset = selectedStart;
}
}
int currentX = leftMargin + bezelLeft - scrollOffset;
// Bullet cursor settings
constexpr int bulletRadius = 3;
const int bulletCenterY = y + lineHeight / 2;
// Calculate visible area boundaries (leave room for overflow indicators)
const bool hasLeftOverflow = scrollOffset > 0;
const bool hasRightOverflow = totalWidth > availableWidth && scrollOffset < totalWidth - availableWidth;
const int visibleLeft = bezelLeft + (hasLeftOverflow ? overflowIndicatorWidth : 0);
const int visibleRight = screenWidth - bezelRight - (hasRightOverflow ? overflowIndicatorWidth : 0);
for (size_t i = 0; i < tabs.size(); i++) {
const auto& tab = tabs[i];
const int textWidth = tabWidths[i];
// Only draw if at least partially visible (accounting for overflow indicator space)
if (currentX + textWidth > visibleLeft && currentX < visibleRight) {
// Draw bullet cursor before selected tab when showCursor is true
if (showCursor && tab.selected) {
// Draw filled circle using distance-squared check
const int bulletCenterX = currentX - cursorPadding - bulletRadius;
const int radiusSq = bulletRadius * bulletRadius;
for (int dy = -bulletRadius; dy <= bulletRadius; ++dy) {
for (int dx = -bulletRadius; dx <= bulletRadius; ++dx) {
if (dx * dx + dy * dy <= radiusSq) {
renderer.drawPixel(bulletCenterX + dx, bulletCenterY + dy, true);
}
}
}
}
// Draw tab label
renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true,
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
// Draw bullet cursor after selected tab when showCursor is true
if (showCursor && tab.selected) {
// Draw filled circle using distance-squared check
const int bulletCenterX = currentX + textWidth + cursorPadding + bulletRadius;
const int radiusSq = bulletRadius * bulletRadius;
for (int dy = -bulletRadius; dy <= bulletRadius; ++dy) {
for (int dx = -bulletRadius; dx <= bulletRadius; ++dx) {
if (dx * dx + dy * dy <= radiusSq) {
renderer.drawPixel(bulletCenterX + dx, bulletCenterY + dy, true);
}
}
}
}
// Draw underline for selected tab
if (tab.selected) {
renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight);
}
}
currentX += textWidth + tabPadding;
}
// Draw overflow indicators if content extends beyond visible area
if (totalWidth > availableWidth) {
constexpr int triangleHeight = 12; // Height of the triangle (vertical)
constexpr int triangleWidth = 6; // Width of the triangle (horizontal) - thin/elongated
const int triangleCenterY = y + lineHeight / 2;
// Left overflow indicator (more content to the left) - thin triangle pointing left
if (scrollOffset > 0) {
// Clear background behind indicator to hide any overlapping text
renderer.fillRect(bezelLeft, y - 2, overflowIndicatorWidth, lineHeight + 4, false);
// Draw left-pointing triangle: point on left, base on right
const int tipX = bezelLeft + 2;
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (0 at tip, full height at base)
const int lineHalfHeight = (triangleHeight * i) / (triangleWidth * 2);
renderer.drawLine(tipX + i, triangleCenterY - lineHalfHeight, tipX + i, triangleCenterY + lineHalfHeight);
}
}
// Right overflow indicator (more content to the right) - thin triangle pointing right
if (scrollOffset < totalWidth - availableWidth) {
// Clear background behind indicator to hide any overlapping text
renderer.fillRect(screenWidth - bezelRight - overflowIndicatorWidth, y - 2, overflowIndicatorWidth,
lineHeight + 4, false);
// Draw right-pointing triangle: base on left, point on right
const int baseX = screenWidth - bezelRight - 2 - triangleWidth;
for (int i = 0; i < triangleWidth; ++i) {
// Scale height based on position (full height at base, 0 at tip)
const int lineHalfHeight = (triangleHeight * (triangleWidth - 1 - i)) / (triangleWidth * 2);
renderer.drawLine(baseX + i, triangleCenterY - lineHalfHeight, baseX + i, triangleCenterY + lineHalfHeight);
}
}
}
return tabBarHeight;
}

View File

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

View File

@@ -12,13 +12,13 @@ class GfxRenderer;
// Helper macro to log stack high-water mark for a task
// Usage: LOG_STACK_WATERMARK("ActivityName", taskHandle);
#define LOG_STACK_WATERMARK(name, handle) \
do { \
if (handle) { \
UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle); \
#define LOG_STACK_WATERMARK(name, handle) \
do { \
if (handle) { \
UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle); \
Serial.printf("[%lu] [STACK] %s: %u bytes remaining\n", millis(), name, remaining * sizeof(StackType_t)); \
} \
} while(0)
} \
} while (0)
class Activity {
protected:

View File

@@ -164,8 +164,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
float cropX = 0, cropY = 0;
int drawWidth = pageWidth;
int drawHeight = pageHeight;
int fillWidth = pageWidth; // Actual area the image will occupy
int fillHeight = pageHeight;
int fillWidth, fillHeight; // Actual area the image will occupy (set per mode)
Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
pageWidth, pageHeight);
@@ -183,9 +182,10 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
// Don't constrain to screen dimensions - drawBitmap will clip
drawWidth = 0;
drawHeight = 0;
fillWidth = bitmap.getWidth();
fillHeight = bitmap.getHeight();
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d\n", millis(), x, y);
fillWidth = static_cast<int>(bitmap.getWidth());
fillHeight = static_cast<int>(bitmap.getHeight());
Serial.printf("[%lu] [SLP] ACTUAL mode: centering at %d, %d (fill: %dx%d)\n", millis(), x, y, fillWidth,
fillHeight);
} else if (coverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
// CROP mode: Scale to fill screen completely (may crop edges)
// Calculate crop values to fill the screen while maintaining aspect ratio
@@ -222,8 +222,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
// Center the scaled image
x = (pageWidth - fillWidth) / 2;
y = (pageHeight - fillHeight) / 2;
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale,
fillWidth, fillHeight, x, y);
Serial.printf("[%lu] [SLP] FIT mode: scale %f, scaled size %d x %d, position %d, %d\n", millis(), scale, fillWidth,
fillHeight, x, y);
}
// Get edge luminance values (from cache or calculate)
@@ -233,9 +233,8 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
const uint8_t leftGray = quantizeGray(edges.left);
const uint8_t rightGray = quantizeGray(edges.right);
Serial.printf("[%lu] [SLP] Edge luminance: T=%d B=%d L=%d R=%d -> gray levels T=%d B=%d L=%d R=%d\n",
millis(), edges.top, edges.bottom, edges.left, edges.right,
topGray, bottomGray, leftGray, rightGray);
Serial.printf("[%lu] [SLP] Edge luminance: T=%d B=%d L=%d R=%d -> gray levels T=%d B=%d L=%d R=%d\n", millis(),
edges.top, edges.bottom, edges.left, edges.right, topGray, bottomGray, leftGray, rightGray);
// Check if greyscale pass should be used (PR #476: skip if filter is applied)
const bool hasGreyscale = bitmap.hasGreyscale() &&
@@ -419,10 +418,10 @@ std::string SleepActivity::getEdgeCachePath(const std::string& bmpPath) {
uint8_t SleepActivity::quantizeGray(uint8_t lum) {
// Quantize luminance (0-255) to 4-level grayscale (0-3)
// Thresholds tuned for X4 display gray levels
if (lum < 43) return 0; // black
if (lum < 128) return 1; // dark gray
if (lum < 213) return 2; // light gray
return 3; // white
if (lum < 43) return 0; // black
if (lum < 128) return 1; // dark gray
if (lum < 213) return 2; // light gray
return 3; // white
}
EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::string& bmpPath) const {
@@ -435,8 +434,7 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
uint8_t cacheData[EDGE_CACHE_SIZE];
if (cacheFile.read(cacheData, EDGE_CACHE_SIZE) == EDGE_CACHE_SIZE) {
// Extract cached file size
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
const uint32_t cachedSize = static_cast<uint32_t>(cacheData[0]) | (static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
@@ -449,8 +447,8 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
result.bottom = cacheData[5];
result.left = cacheData[6];
result.right = cacheData[7];
Serial.printf("[%lu] [SLP] Edge cache hit for %s: T=%d B=%d L=%d R=%d\n", millis(), bmpPath.c_str(),
result.top, result.bottom, result.left, result.right);
Serial.printf("[%lu] [SLP] Edge cache hit for %s: T=%d B=%d L=%d R=%d\n", millis(), bmpPath.c_str(), result.top,
result.bottom, result.left, result.right);
cacheFile.close();
return result;
}
@@ -463,8 +461,8 @@ EdgeLuminance SleepActivity::getEdgeLuminance(const Bitmap& bitmap, const std::s
// Cache miss - calculate edge luminance
Serial.printf("[%lu] [SLP] Calculating edge luminance for %s\n", millis(), bmpPath.c_str());
result = bitmap.detectEdgeLuminance(2); // Sample 2 pixels deep for stability
Serial.printf("[%lu] [SLP] Edge luminance detected: T=%d B=%d L=%d R=%d\n", millis(),
result.top, result.bottom, result.left, result.right);
Serial.printf("[%lu] [SLP] Edge luminance detected: T=%d B=%d L=%d R=%d\n", millis(), result.top, result.bottom,
result.left, result.right);
// Get BMP file size from already-opened bitmap for cache
const uint32_t fileSize = bitmap.getFileSize();
@@ -535,8 +533,7 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
cacheFile.close();
// Extract cached values
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) |
(static_cast<uint32_t>(cacheData[1]) << 8) |
const uint32_t cachedBmpSize = static_cast<uint32_t>(cacheData[0]) | (static_cast<uint32_t>(cacheData[1]) << 8) |
(static_cast<uint32_t>(cacheData[2]) << 16) |
(static_cast<uint32_t>(cacheData[3]) << 24);
EdgeLuminance cachedEdges;
@@ -549,8 +546,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
// Check if cover mode matches (for EPUB)
const uint8_t currentCoverMode = cropped ? 1 : 0;
if (StringUtils::checkFileExtension(bookPath, ".epub") && cachedCoverMode != currentCoverMode) {
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n",
cachedCoverMode, currentCoverMode);
Serial.printf("[SLP] Cover mode changed (cached=%d, current=%d), invalidating cache\n", cachedCoverMode,
currentCoverMode);
return false;
}
@@ -572,8 +569,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
// Check if BMP file size matches cache
const uint32_t currentBmpSize = bmpFile.size();
if (currentBmpSize != cachedBmpSize || currentBmpSize == 0) {
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n",
static_cast<unsigned long>(cachedBmpSize), static_cast<unsigned long>(currentBmpSize));
Serial.printf("[SLP] BMP size mismatch (cached=%lu, current=%lu)\n", static_cast<unsigned long>(cachedBmpSize),
static_cast<unsigned long>(currentBmpSize));
bmpFile.close();
return false;
}
@@ -586,8 +583,8 @@ bool SleepActivity::tryRenderCachedCoverSleep(const std::string& bookPath, bool
return false;
}
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(),
coverBmpPath.c_str(), cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
Serial.printf("[%lu] [SLP] Using cached cover sleep: %s (T=%d B=%d L=%d R=%d)\n", millis(), coverBmpPath.c_str(),
cachedEdges.top, cachedEdges.bottom, cachedEdges.left, cachedEdges.right);
// Render the bitmap with cached edge values
// We call renderBitmapSleepScreen which will use getEdgeLuminance internally,

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
* - PortraitInverted: Front=TOP, Side=LEFT
* - LandscapeCCW: Front=RIGHT, Side=TOP
*/
inline void getDictionaryContentMargins(GfxRenderer& renderer, int* outTop, int* outRight, int* outBottom,
inline void getDictionaryContentMargins(const GfxRenderer& renderer, int* outTop, int* outRight, int* outBottom,
int* outLeft) {
// Start with same base margins as reader (getOrientedViewableTRBL + screenMargin)
renderer.getOrientedViewableTRBL(outTop, outRight, outBottom, outLeft);

View File

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

View File

@@ -93,8 +93,8 @@ void DictionaryResultActivity::paginateDefinition() {
const auto pageHeight = renderer.getScreenHeight();
// Calculate available area for text (must match render() layout)
constexpr int headerHeight = 80; // Space for word and header (relative to marginTop)
constexpr int footerHeight = 30; // Space for page indicator
constexpr int headerHeight = 80; // Space for word and header (relative to marginTop)
constexpr int footerHeight = 30; // Space for page indicator
const int textMargin = marginLeft + 10;
const int textWidth = pageWidth - textMargin - marginRight - 10;
const int textHeight = pageHeight - marginTop - marginBottom - headerHeight - footerHeight;
@@ -152,7 +152,6 @@ void DictionaryResultActivity::render() const {
int marginTop, marginRight, marginBottom, marginLeft;
getDictionaryContentMargins(renderer, &marginTop, &marginRight, &marginBottom, &marginLeft);
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header with top margin

View File

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

View File

@@ -77,13 +77,8 @@ void EpubWordSelectionActivity::buildWordList() {
while (wordIt != words.end() && xPosIt != xPositions.end() && styleIt != styles.end()) {
// Skip whitespace-only words
const std::string& wordText = *wordIt;
bool hasAlpha = false;
for (char c : wordText) {
if (std::isalpha(static_cast<unsigned char>(c))) {
hasAlpha = true;
break;
}
}
const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(),
[](char c) { return std::isalpha(static_cast<unsigned char>(c)); });
if (hasAlpha) {
WordInfo info;
@@ -106,7 +101,6 @@ void EpubWordSelectionActivity::buildWordList() {
int EpubWordSelectionActivity::findLineForWordIndex(int wordIndex) const {
if (wordIndex < 0 || wordIndex >= static_cast<int>(allWords.size())) return 0;
const int targetY = allWords[wordIndex].y;
int lineIdx = 0;
int lastY = -1;
@@ -254,7 +248,8 @@ void EpubWordSelectionActivity::render() const {
// Draw instruction text - position it just above the front button area
const auto screenHeight = renderer.getScreenHeight();
renderer.drawCenteredText(SMALL_FONT_ID, screenHeight - marginBottom - 10, "Navigate with arrows, select with confirm");
renderer.drawCenteredText(SMALL_FONT_ID, screenHeight - marginBottom - 10,
"Navigate with arrows, select with confirm");
// Draw button hints
const auto labels = mappedInput.mapLabels("\xc2\xab Cancel", "Select", "< >", "");

View File

@@ -39,9 +39,7 @@ int BookmarkListActivity::getCurrentPage() const {
return selectorIndex / pageItems + 1;
}
void BookmarkListActivity::loadBookmarks() {
bookmarks = BookmarkStore::getBookmarks(bookPath);
}
void BookmarkListActivity::loadBookmarks() { bookmarks = BookmarkStore::getBookmarks(bookPath); }
void BookmarkListActivity::taskTrampoline(void* param) {
auto* self = static_cast<BookmarkListActivity*>(param);
@@ -95,7 +93,7 @@ void BookmarkListActivity::loop() {
const auto& bm = bookmarks[selectorIndex];
BookmarkStore::removeBookmark(bookPath, bm.spineIndex, bm.contentOffset);
loadBookmarks();
// Adjust selector if needed
if (selectorIndex >= static_cast<int>(bookmarks.size()) && !bookmarks.empty()) {
selectorIndex = static_cast<int>(bookmarks.size()) - 1;
@@ -113,12 +111,10 @@ void BookmarkListActivity::loop() {
// Normal state handling
const int itemCount = static_cast<int>(bookmarks.size());
const int pageItems = getPageItems();
// Long press Confirm to delete bookmark
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
mappedInput.getHeldTime() >= ACTION_MENU_MS && !bookmarks.empty() &&
selectorIndex < itemCount) {
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= ACTION_MENU_MS &&
!bookmarks.empty() && selectorIndex < itemCount) {
uiState = UIState::Confirming;
updateRequired = true;
return;
@@ -129,7 +125,7 @@ void BookmarkListActivity::loop() {
if (mappedInput.getHeldTime() >= ACTION_MENU_MS) {
return; // Was a long press
}
if (!bookmarks.empty() && selectorIndex < itemCount) {
const auto& bm = bookmarks[selectorIndex];
onSelectBookmark(bm.spineIndex, bm.contentOffset);
@@ -191,12 +187,14 @@ void BookmarkListActivity::render() const {
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
// Draw title
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, bookTitle.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, BASE_TAB_BAR_Y + bezelTop, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
auto truncatedTitle =
renderer.truncatedText(UI_12_FONT_ID, bookTitle.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, BASE_TAB_BAR_Y + bezelTop, truncatedTitle.c_str(), true,
EpdFontFamily::BOLD);
if (itemCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No bookmarks");
const auto labels = mappedInput.mapLabels("\xc2\xab Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();

View File

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

View File

@@ -157,7 +157,7 @@ bool HomeActivity::storeCoverBuffer() {
}
const size_t bufferSize = GfxRenderer::getBufferSize();
// Reuse existing buffer if already allocated (avoids fragmentation from free+malloc)
if (!coverBuffer) {
coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
@@ -270,7 +270,7 @@ bool HomeActivity::preloadCoverBuffer() {
cachedCoverPath = thumbPath;
coverBufferStored = false; // Will be set true after actual render in HomeActivity
coverRendered = false; // Will trigger load from disk in render()
Serial.printf("[%lu] [HOME] [MEM] Cover buffer pre-allocated for: %s\n", millis(), thumbPath.c_str());
return true;
}
@@ -374,8 +374,7 @@ void HomeActivity::render() {
constexpr int menuSpacing = 8;
const int halfTileWidth = (menuTileWidth - menuSpacing) / 2; // Account for spacing between halves
// 1 row for split buttons + full-width rows
const int totalMenuHeight =
menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
const int totalMenuHeight = menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
// Anchor menu to bottom of screen
const int menuStartY = pageHeight - bottomMargin - totalMenuHeight;
@@ -581,8 +580,7 @@ void HomeActivity::render() {
// Still have words left, so add ellipsis to last line
lines.back().append("...");
while (!lines.back().empty() &&
renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
// Remove "..." first, then remove one UTF-8 char, then add "..." back
lines.back().resize(lines.back().size() - 3); // Remove "..."
StringUtils::utf8RemoveLastChar(lines.back());
@@ -690,8 +688,10 @@ void HomeActivity::render() {
// Truncate lists label if needed
std::string truncatedLabel = listsLabel;
const int maxLabelWidth = halfTileWidth - 16; // Padding
while (renderer.getTextWidth(UI_10_FONT_ID, truncatedLabel.c_str()) > maxLabelWidth && truncatedLabel.length() > 3) {
truncatedLabel = truncatedLabel.substr(0, truncatedLabel.length() - 4) + "...";
while (renderer.getTextWidth(UI_10_FONT_ID, truncatedLabel.c_str()) > maxLabelWidth &&
truncatedLabel.length() > 3) {
truncatedLabel.resize(truncatedLabel.length() - 4);
truncatedLabel += "...";
}
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedLabel.c_str());
@@ -749,7 +749,7 @@ void HomeActivity::render() {
// Draw battery in bottom-left where the back button hint would normally be
const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
constexpr int batteryX = 25; // Align with first button hint position
constexpr int batteryX = 25; // Align with first button hint position
const int batteryY = pageHeight - 34; // Vertically centered in button hint area
ScreenComponents::drawBatteryLarge(renderer, batteryX, batteryY, showBatteryPercentage);

View File

@@ -17,9 +17,9 @@ class HomeActivity final : public Activity {
bool hasCoverImage = false;
// Static cover buffer - persists across activity changes to avoid reloading from SD
static bool coverRendered; // Track if cover has been rendered once
static bool coverBufferStored; // Track if cover buffer is stored
static uint8_t* coverBuffer; // HomeActivity's own buffer for cover image
static bool coverRendered; // Track if cover has been rendered once
static bool coverBufferStored; // Track if cover buffer is stored
static uint8_t* coverBuffer; // HomeActivity's own buffer for cover image
static std::string cachedCoverPath; // Path of the cached cover (to detect book changes)
std::string lastBookTitle;
@@ -43,15 +43,14 @@ class HomeActivity final : public Activity {
public:
// Free cover buffer from external activities (e.g., when entering reader to reclaim memory)
static void freeCoverBufferIfAllocated();
// Preload cover buffer from external activities (e.g., MyLibraryActivity) for instant Home screen
// Returns true if cover was successfully preloaded or already cached
static bool preloadCoverBuffer();
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onContinueReading, const std::function<void()>& onListsOpen,
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onSettingsOpen,
const std::function<void()>& onFileTransferOpen,
const std::function<void()>& onOpdsBrowserOpen)
const std::function<void()>& onFileTransferOpen, const std::function<void()>& onOpdsBrowserOpen)
: Activity("Home", renderer, mappedInput),
onContinueReading(onContinueReading),
onListsOpen(onListsOpen),

View File

@@ -202,8 +202,8 @@ void ListViewActivity::render() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN - bezelLeft, LINE_HEIGHT);
// Calculate available text width
const int textMaxWidth = pageWidth - LEFT_MARGIN - RIGHT_MARGIN - MICRO_THUMB_WIDTH - 10;
@@ -262,8 +262,8 @@ void ListViewActivity::render() const {
}
// Extract tags for badges (only if we'll show them - when NOT selected)
constexpr int badgeSpacing = 4; // Gap between badges
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
constexpr int badgeSpacing = 4; // Gap between badges
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
int totalBadgeWidth = 0;
BookTags tags;
@@ -302,8 +302,8 @@ void ListViewActivity::render() const {
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
if (!tags.extensionTag.empty()) {
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
SMALL_FONT_ID, false);
int badgeWidth =
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(), SMALL_FONT_ID, false);
badgeX += badgeWidth + badgeSpacing;
}
if (!tags.suffixTag.empty()) {

File diff suppressed because it is too large Load Diff

View File

@@ -13,16 +13,39 @@
// Cached thumbnail existence info for Recent tab
struct ThumbExistsCache {
std::string bookPath; // Book path this cache entry belongs to
std::string thumbPath; // Path to micro-thumbnail (if exists)
bool checked = false; // Whether we've checked for this book
bool exists = false; // Whether thumbnail exists
std::string bookPath; // Book path this cache entry belongs to
std::string thumbPath; // Path to micro-thumbnail (if exists)
bool checked = false; // Whether we've checked for this book
bool exists = false; // Whether thumbnail exists
};
// Search result for the Search tab
struct SearchResult {
std::string path;
std::string title;
std::string author;
int matchScore = 0; // Higher = better match
};
// Book with bookmarks info for the Bookmarks tab
struct BookmarkedBook {
std::string path;
std::string title;
std::string author;
int bookmarkCount = 0;
};
class MyLibraryActivity final : public Activity {
public:
enum class Tab { Recent, Lists, Files };
enum class UIState { Normal, ActionMenu, Confirming, ListActionMenu, ListConfirmingDelete, ClearAllRecentsConfirming };
enum class Tab { Recent, Lists, Bookmarks, Search, Files };
enum class UIState {
Normal,
ActionMenu,
Confirming,
ListActionMenu,
ListConfirmingDelete,
ClearAllRecentsConfirming
};
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
private:
@@ -32,13 +55,14 @@ class MyLibraryActivity final : public Activity {
Tab currentTab = Tab::Recent;
int selectorIndex = 0;
bool updateRequired = false;
bool inTabBar = false; // true = focus on tab bar for switching tabs (all tabs)
// Action menu state
UIState uiState = UIState::Normal;
ActionType selectedAction = ActionType::Archive;
std::string actionTargetPath;
std::string actionTargetName;
int menuSelection = 0; // 0 = Archive, 1 = Delete
int menuSelection = 0; // 0 = Archive, 1 = Delete
bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu
// Recent tab state
@@ -47,13 +71,12 @@ class MyLibraryActivity final : public Activity {
// Static thumbnail existence cache - persists across activity enter/exit
static constexpr int MAX_THUMB_CACHE = 10;
static ThumbExistsCache thumbExistsCache[MAX_THUMB_CACHE];
public:
// Clear the thumbnail existence cache (call when disk cache is cleared)
static void clearThumbExistsCache();
private:
private:
// Lists tab state
std::vector<std::string> lists;
@@ -61,6 +84,17 @@ class MyLibraryActivity final : public Activity {
int listMenuSelection = 0; // 0 = Pin/Unpin, 1 = Delete
std::string listActionTargetName;
// Bookmarks tab state
std::vector<BookmarkedBook> bookmarkedBooks;
// Search tab state
std::string searchQuery;
std::vector<SearchResult> searchResults;
std::vector<SearchResult> allBooks; // Cached index of all books
std::vector<char> searchCharacters; // Dynamic character set from library
int searchCharIndex = 0; // Current position in character picker
bool searchInResults = false; // true = navigating results, false = in character picker
// Files tab state (from FileSelectionActivity)
std::string basepath = "/";
std::vector<std::string> files;
@@ -69,6 +103,7 @@ class MyLibraryActivity final : public Activity {
const std::function<void()> onGoHome;
const std::function<void(const std::string& path, Tab fromTab)> onSelectBook;
const std::function<void(const std::string& listName)> onSelectList;
const std::function<void(const std::string& path, const std::string& title)> onSelectBookmarkedBook;
// Number of items that fit on a page
int getPageItems() const;
@@ -79,6 +114,9 @@ class MyLibraryActivity final : public Activity {
// Data loading
void loadRecentBooks();
void loadLists();
void loadBookmarkedBooks();
void loadAllBooks();
void updateSearchResults();
void loadFiles();
size_t findEntry(const std::string& name) const;
@@ -88,10 +126,16 @@ class MyLibraryActivity final : public Activity {
void render() const;
void renderRecentTab() const;
void renderListsTab() const;
void renderBookmarksTab() const;
void renderSearchTab() const;
void renderFilesTab() const;
void renderActionMenu() const;
void renderConfirmation() const;
// Search character picker helpers
void buildSearchCharacters();
void renderCharacterPicker(int y) const;
// Action handling
void openActionMenu();
void executeAction();
@@ -110,17 +154,19 @@ class MyLibraryActivity final : public Activity {
void renderClearAllRecentsConfirmation() const;
public:
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoHome,
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
const std::function<void(const std::string& listName)>& onSelectList,
Tab initialTab = Tab::Recent, std::string initialPath = "/")
explicit MyLibraryActivity(
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoHome,
const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook,
const std::function<void(const std::string& listName)>& onSelectList,
const std::function<void(const std::string& path, const std::string& title)>& onSelectBookmarkedBook = nullptr,
Tab initialTab = Tab::Recent, std::string initialPath = "/")
: Activity("MyLibrary", renderer, mappedInput),
currentTab(initialTab),
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
onGoHome(onGoHome),
onSelectBook(onSelectBook),
onSelectList(onSelectList) {}
onSelectList(onSelectList),
onSelectBookmarkedBook(onSelectBookmarkedBook) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@@ -600,9 +600,9 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const {
// Landscape layout (800x480): QR on left, text on right
constexpr int QR_X = 15;
constexpr int QR_Y = 15;
constexpr int QR_PX = 7; // pixels per QR module
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
constexpr int QR_PX = 7; // pixels per QR module
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
constexpr int LINE_SPACING = 32;
// Draw title on right side
@@ -640,7 +640,8 @@ void CrossPointWebServerActivity::renderWebBrowserScreen() const {
std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 35) {
ssidInfo = ssidInfo.substr(0, 32) + "...";
ssidInfo.resize(32);
ssidInfo += "...";
}
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
textY += LINE_SPACING;
@@ -666,9 +667,9 @@ void CrossPointWebServerActivity::renderCompanionAppScreen() const {
// Landscape layout (800x480): QR on left, text on right
constexpr int QR_X = 15;
constexpr int QR_Y = 15;
constexpr int QR_PX = 7; // pixels per QR module
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
constexpr int QR_PX = 7; // pixels per QR module
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
constexpr int LINE_SPACING = 32;
// Draw title on right side
@@ -679,7 +680,8 @@ void CrossPointWebServerActivity::renderCompanionAppScreen() const {
// Show network info
std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 35) {
ssidInfo = ssidInfo.substr(0, 32) + "...";
ssidInfo.resize(32);
ssidInfo += "...";
}
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
textY += LINE_SPACING;
@@ -715,9 +717,9 @@ void CrossPointWebServerActivity::renderCompanionAppLibraryScreen() const {
// Landscape layout (800x480): QR on left, text on right
constexpr int QR_X = 15;
constexpr int QR_Y = 15;
constexpr int QR_PX = 7; // pixels per QR module
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
constexpr int QR_PX = 7; // pixels per QR module
constexpr int QR_SIZE = 33 * QR_PX; // QR version 4 = 33 modules = 231px
constexpr int TEXT_X = QR_X + QR_SIZE + 35; // Text starts after QR + margin
constexpr int LINE_SPACING = 32;
// Draw title on right side
@@ -728,7 +730,8 @@ void CrossPointWebServerActivity::renderCompanionAppLibraryScreen() const {
// Show network info
std::string ssidInfo = "Network: " + connectedSSID;
if (ssidInfo.length() > 35) {
ssidInfo = ssidInfo.substr(0, 32) + "...";
ssidInfo.resize(32);
ssidInfo += "...";
}
renderer.drawText(NOTOSANS_12_FONT_ID, TEXT_X, textY, ssidInfo.c_str());
textY += LINE_SPACING;

View File

@@ -8,6 +8,7 @@
#include <Serialization.h>
#include "BookManager.h"
#include "BookmarkStore.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "EpubReaderChapterSelectionActivity.h"
@@ -17,6 +18,7 @@
#include "activities/dictionary/DictionaryMenuActivity.h"
#include "activities/dictionary/DictionarySearchActivity.h"
#include "activities/dictionary/EpubWordSelectionActivity.h"
#include "activities/util/QuickMenuActivity.h"
#include "fontIds.h"
namespace {
@@ -109,24 +111,24 @@ void EpubReaderActivity::onEnter() {
FsFile f;
if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
const size_t fileSize = f.size();
if (fileSize >= 9) {
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
uint8_t version;
serialization::readPod(f, version);
if (version == EPUB_PROGRESS_VERSION) {
uint16_t spineIndex, pageNumber;
serialization::readPod(f, spineIndex);
serialization::readPod(f, pageNumber);
serialization::readPod(f, savedContentOffset);
currentSpineIndex = spineIndex;
nextPageNumber = pageNumber;
hasContentOffset = true;
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n",
millis(), currentSpineIndex, nextPageNumber, savedContentOffset);
Serial.printf("[%lu] [ERS] Loaded progress v1: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
nextPageNumber, savedContentOffset);
} else {
// Unknown version, try legacy format
f.seek(0);
@@ -135,8 +137,8 @@ void EpubReaderActivity::onEnter() {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
hasContentOffset = false;
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n",
millis(), version, currentSpineIndex, nextPageNumber);
Serial.printf("[%lu] [ERS] Loaded legacy progress (unknown version %d): spine %d, page %d\n", millis(),
version, currentSpineIndex, nextPageNumber);
}
}
} else if (fileSize >= 4) {
@@ -146,12 +148,13 @@ void EpubReaderActivity::onEnter() {
currentSpineIndex = data[0] + (data[1] << 8);
nextPageNumber = data[2] + (data[3] << 8);
hasContentOffset = false;
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n",
millis(), currentSpineIndex, nextPageNumber);
Serial.printf("[%lu] [ERS] Loaded legacy progress: spine %d, page %d\n", millis(), currentSpineIndex,
nextPageNumber);
}
}
f.close();
}
// We may want a better condition to detect if we are opening for the first time.
// This will trigger if the book is re-opened at Chapter 0.
if (currentSpineIndex == 0) {
@@ -298,19 +301,20 @@ void EpubReaderActivity::loop() {
Section* cachedSection = section.get();
SemaphoreHandle_t cachedMutex = renderingMutex;
EpubReaderActivity* self = this;
// Handle dictionary mode selection - exitActivity deletes DictionaryMenuActivity
exitActivity();
if (mode == DictionaryMode::ENTER_WORD) {
// Enter word mode - show keyboard and search
self->enterNewActivity(new DictionarySearchActivity(cachedRenderer, cachedMappedInput,
[self]() {
// On back from dictionary
self->exitActivity();
self->updateRequired = true;
},
"")); // Empty string = show keyboard
self->enterNewActivity(new DictionarySearchActivity(
cachedRenderer, cachedMappedInput,
[self]() {
// On back from dictionary
self->exitActivity();
self->updateRequired = true;
},
"")); // Empty string = show keyboard
} else {
// Select from screen mode - show word selection on current page
if (cachedSection) {
@@ -320,7 +324,7 @@ void EpubReaderActivity::loop() {
// Get margins for word selection positioning
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
cachedRenderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
&orientedMarginLeft);
&orientedMarginLeft);
orientedMarginTop += SETTINGS.screenMargin;
orientedMarginLeft += SETTINGS.screenMargin;
@@ -333,12 +337,13 @@ void EpubReaderActivity::loop() {
[self](const std::string& selectedWord) {
// Word selected - look it up
self->exitActivity();
self->enterNewActivity(new DictionarySearchActivity(self->renderer, self->mappedInput,
[self]() {
self->exitActivity();
self->updateRequired = true;
},
selectedWord));
self->enterNewActivity(new DictionarySearchActivity(
self->renderer, self->mappedInput,
[self]() {
self->exitActivity();
self->updateRequired = true;
},
selectedWord));
},
[self]() {
// Cancelled word selection
@@ -366,6 +371,152 @@ void EpubReaderActivity::loop() {
return;
}
// Quick Menu power button press
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::QUICK_MENU &&
mappedInput.wasReleased(MappedInputManager::Button::Power)) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// Check if current page is bookmarked
bool isBookmarked = false;
if (section) {
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
isBookmarked = BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset);
}
exitActivity();
enterNewActivity(new QuickMenuActivity(
renderer, mappedInput,
[this](QuickMenuAction action) {
// Cache values before exitActivity
EpubReaderActivity* self = this;
SemaphoreHandle_t cachedMutex = renderingMutex;
exitActivity();
if (action == QuickMenuAction::DICTIONARY) {
// Open dictionary menu - cache renderer/input for this scope
GfxRenderer& cachedRenderer = self->renderer;
MappedInputManager& cachedMappedInput = self->mappedInput;
const Section* cachedSection = self->section.get();
self->enterNewActivity(new DictionaryMenuActivity(
cachedRenderer, cachedMappedInput,
[self](DictionaryMode mode) {
GfxRenderer& r = self->renderer;
MappedInputManager& m = self->mappedInput;
Section* s = self->section.get();
SemaphoreHandle_t mtx = self->renderingMutex;
self->exitActivity();
if (mode == DictionaryMode::ENTER_WORD) {
self->enterNewActivity(new DictionarySearchActivity(
r, m,
[self]() {
self->exitActivity();
self->updateRequired = true;
},
""));
} else if (s) {
xSemaphoreTake(mtx, portMAX_DELAY);
auto page = s->loadPageFromSectionFile();
if (page) {
int mt, mr, mb, ml;
r.getOrientedViewableTRBL(&mt, &mr, &mb, &ml);
mt += SETTINGS.screenMargin;
ml += SETTINGS.screenMargin;
const int fontId = SETTINGS.getReaderFontId();
self->enterNewActivity(new EpubWordSelectionActivity(
r, m, std::move(page), fontId, ml, mt,
[self](const std::string& word) {
self->exitActivity();
self->enterNewActivity(new DictionarySearchActivity(
self->renderer, self->mappedInput,
[self]() {
self->exitActivity();
self->updateRequired = true;
},
word));
},
[self]() {
self->exitActivity();
self->updateRequired = true;
}));
xSemaphoreGive(mtx);
} else {
xSemaphoreGive(mtx);
self->updateRequired = true;
}
} else {
self->updateRequired = true;
}
},
[self]() {
self->exitActivity();
self->updateRequired = true;
},
self->section != nullptr));
} else if (action == QuickMenuAction::ADD_BOOKMARK) {
// Toggle bookmark on current page
if (self->section) {
const uint32_t contentOffset = self->section->getContentOffsetForPage(self->section->currentPage);
const std::string& bookPath = self->epub->getPath();
if (BookmarkStore::isPageBookmarked(bookPath, self->currentSpineIndex, contentOffset)) {
// Remove bookmark
BookmarkStore::removeBookmark(bookPath, self->currentSpineIndex, contentOffset);
} else {
// Add bookmark with auto-generated name
Bookmark bm;
bm.spineIndex = self->currentSpineIndex;
bm.contentOffset = contentOffset;
bm.pageNumber = self->section->currentPage;
bm.timestamp = millis() / 1000; // Approximate timestamp
// Generate name: "Chapter - Page X" or fallback
std::string chapterTitle;
const int tocIndex = self->epub->getTocIndexForSpineIndex(self->currentSpineIndex);
if (tocIndex >= 0) {
chapterTitle = self->epub->getTocItem(tocIndex).title;
}
if (!chapterTitle.empty()) {
bm.name = chapterTitle + " - Page " + std::to_string(self->section->currentPage + 1);
} else {
bm.name = "Page " + std::to_string(self->section->currentPage + 1);
}
BookmarkStore::addBookmark(bookPath, bm);
}
}
self->updateRequired = true;
} else if (action == QuickMenuAction::CLEAR_CACHE) {
// Navigate to Clear Cache activity
if (self->onGoToClearCache) {
xSemaphoreGive(cachedMutex);
self->onGoToClearCache();
return;
}
self->updateRequired = true;
} else if (action == QuickMenuAction::GO_TO_SETTINGS) {
// Navigate to Settings activity
if (self->onGoToSettings) {
xSemaphoreGive(cachedMutex);
self->onGoToSettings();
return;
}
self->updateRequired = true;
}
},
[this]() {
EpubReaderActivity* self = this;
exitActivity();
self->updateRequired = true;
},
isBookmarked));
xSemaphoreGive(renderingMutex);
return;
}
const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Left);
const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
@@ -491,7 +642,7 @@ void EpubReaderActivity::renderScreen() {
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
bool sectionWasReIndexed = false;
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled)) {
@@ -538,7 +689,7 @@ void EpubReaderActivity::renderScreen() {
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
};
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) {
Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis());
@@ -558,8 +709,8 @@ void EpubReaderActivity::renderScreen() {
// Use the offset to find the correct page
const int restoredPage = section->findPageForContentOffset(savedContentOffset);
section->currentPage = restoredPage;
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n",
millis(), savedContentOffset, restoredPage, nextPageNumber);
Serial.printf("[%lu] [ERS] Restored position via offset: %u -> page %d (was page %d)\n", millis(),
savedContentOffset, restoredPage, nextPageNumber);
// Clear the offset flag since we've used it
hasContentOffset = false;
} else {
@@ -579,7 +730,8 @@ void EpubReaderActivity::renderScreen() {
}
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage,
section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
@@ -594,7 +746,7 @@ void EpubReaderActivity::renderScreen() {
section.reset();
return renderScreen();
}
// Handle empty pages (e.g., from malformed chapters that couldn't be parsed)
if (p->elements.empty()) {
Serial.printf("[%lu] [ERS] Page has no content (possibly malformed chapter)\n", millis());
@@ -604,7 +756,7 @@ void EpubReaderActivity::renderScreen() {
renderer.displayBuffer();
return;
}
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start);
@@ -615,16 +767,16 @@ void EpubReaderActivity::renderScreen() {
if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) {
// Get content offset for current page
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
// New format: version (1) + spineIndex (2) + pageNumber (2) + contentOffset (4) = 9 bytes
serialization::writePod(f, EPUB_PROGRESS_VERSION);
serialization::writePod(f, static_cast<uint16_t>(currentSpineIndex));
serialization::writePod(f, static_cast<uint16_t>(section->currentPage));
serialization::writePod(f, contentOffset);
f.close();
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n",
millis(), currentSpineIndex, section->currentPage, contentOffset);
Serial.printf("[%lu] [ERS] Saved progress: spine %d, page %d, offset %u\n", millis(), currentSpineIndex,
section->currentPage, contentOffset);
}
}
@@ -632,6 +784,24 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) {
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
// Draw bookmark indicator (folded corner) if this page is bookmarked
if (section) {
const uint32_t contentOffset = section->getContentOffsetForPage(section->currentPage);
if (BookmarkStore::isPageBookmarked(epub->getPath(), currentSpineIndex, contentOffset)) {
// Draw folded corner in top-right
const int screenWidth = renderer.getScreenWidth();
constexpr int cornerSize = 20;
const int cornerX = screenWidth - orientedMarginRight - cornerSize;
const int cornerY = orientedMarginTop;
// Draw triangle (folded corner effect)
const int xPoints[3] = {cornerX, cornerX + cornerSize, cornerX + cornerSize};
const int yPoints[3] = {cornerY, cornerY, cornerY + cornerSize};
renderer.fillPolygon(xPoints, yPoints, 3, true); // Black triangle
}
}
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
@@ -775,7 +945,8 @@ void EpubReaderActivity::renderEndOfBookPrompt() {
// Book title (truncated if needed)
std::string bookTitle = epub->getTitle();
if (bookTitle.length() > 30) {
bookTitle = bookTitle.substr(0, 27) + "...";
bookTitle.resize(27);
bookTitle += "...";
}
renderer.drawCenteredText(UI_10_FONT_ID, 120, bookTitle.c_str());

View File

@@ -18,11 +18,13 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;
const std::function<void()> onGoToClearCache;
const std::function<void()> onGoToSettings;
// End-of-book prompt state
bool showingEndOfBookPrompt = false;
int endOfBookSelection = 2; // 0=Archive, 1=Delete, 2=Keep (default to safe option)
// Content offset for position restoration after re-indexing
uint32_t savedContentOffset = 0;
bool hasContentOffset = false; // True if we have a valid content offset to use
@@ -38,11 +40,15 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
public:
explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome)
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome,
const std::function<void()>& onGoToClearCache = nullptr,
const std::function<void()>& onGoToSettings = nullptr)
: ActivityWithSubactivity("EpubReader", renderer, mappedInput),
epub(std::move(epub)),
onGoBack(onGoBack),
onGoHome(onGoHome) {}
onGoHome(onGoHome),
onGoToClearCache(onGoToClearCache),
onGoToSettings(onGoToSettings) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

@@ -10,7 +10,12 @@ namespace {
constexpr int SKIP_PAGE_MS = 700;
} // namespace
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return false; }
// Sync feature is currently disabled - will be enabled when implemented
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
bool EpubReaderChapterSelectionActivity::hasSyncOption() const {
// TODO: Return true when sync credentials are configured
return false;
}
int EpubReaderChapterSelectionActivity::getTotalItems() const {
// Add 2 for sync options (top and bottom) if credentials are configured
@@ -18,12 +23,6 @@ int EpubReaderChapterSelectionActivity::getTotalItems() const {
return epub->getTocItemsCount() + syncCount;
}
bool EpubReaderChapterSelectionActivity::isSyncItem(int index) const {
if (!hasSyncOption()) return false;
// First item and last item are sync options
return index == 0 || index == getTotalItems() - 1;
}
int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) const {
// Account for the sync option at the top
const int offset = hasSyncOption() ? 1 : 0;
@@ -94,10 +93,6 @@ void EpubReaderChapterSelectionActivity::onExit() {
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
// KOReader sync functionality removed
}
void EpubReaderChapterSelectionActivity::loop() {
if (subActivity) {
subActivity->loop();
@@ -114,13 +109,7 @@ void EpubReaderChapterSelectionActivity::loop() {
const int totalItems = getTotalItems();
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Check if sync option is selected (first or last item)
if (isSyncItem(selectorIndex)) {
launchSyncActivity();
return;
}
// Get TOC index (account for top sync offset)
// Get TOC index (account for top sync offset if enabled)
const int tocIndex = tocIndexFromItemIndex(selectorIndex);
const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex);
if (newSpineIndex == -1) {
@@ -171,30 +160,25 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const std::string title =
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
const std::string title = renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(),
pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, title.c_str(), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2,
pageWidth - 1 - bezelLeft - bezelRight, 30);
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
const int displayY = 60 + bezelTop + (itemIndex % pageItems) * 30;
const bool isSelected = (itemIndex == selectorIndex);
if (isSyncItem(itemIndex)) {
// Draw sync option (at top or bottom)
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, displayY, ">> Sync Progress", !isSelected);
} else {
// Draw TOC item (account for top sync offset)
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
renderer.drawText(UI_10_FONT_ID, indentSize, 60 + bezelTop + (tocIndex % pageItems) * 30, chapterName.c_str(),
tocIndex != selectorIndex);
}
// Draw TOC item
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
const std::string chapterName = renderer.truncatedText(
UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
}
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");

View File

@@ -30,18 +30,15 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
int getTotalItems() const;
// Check if sync option is available (credentials configured)
// Note: Currently always returns false - placeholder for future sync feature
bool hasSyncOption() const;
// Check if given item index is a sync option (first or last)
bool isSyncItem(int index) const;
// Convert item index to TOC index (accounting for top sync option offset)
int tocIndexFromItemIndex(int itemIndex) const;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void launchSyncActivity();
public:
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,

View File

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

View File

@@ -13,6 +13,8 @@ class ReaderActivity final : public ActivityWithSubactivity {
MyLibraryActivity::Tab libraryTab; // Track which tab to return to
const std::function<void()> onGoBack;
const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
const std::function<void()> onGoToClearCache;
const std::function<void()> onGoToSettings;
static std::unique_ptr<Epub> loadEpub(const std::string& path);
static std::unique_ptr<Txt> loadTxt(const std::string& path);
static bool isTxtFile(const std::string& path);
@@ -25,11 +27,15 @@ class ReaderActivity final : public ActivityWithSubactivity {
public:
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack,
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary)
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary,
const std::function<void()>& onGoToClearCache = nullptr,
const std::function<void()>& onGoToSettings = nullptr)
: ActivityWithSubactivity("Reader", renderer, mappedInput),
initialBookPath(std::move(initialBookPath)),
libraryTab(libraryTab),
onGoBack(onGoBack),
onGoToLibrary(onGoToLibrary) {}
onGoToLibrary(onGoToLibrary),
onGoToClearCache(onGoToClearCache),
onGoToSettings(onGoToSettings) {}
void onEnter() override;
};

View File

@@ -88,7 +88,7 @@ void TxtReaderActivity::onEnter() {
renderer.displayBuffer(EInkDisplay::FAST_REFRESH);
// Generate covers with progress callback
txt->generateAllCovers([&](int percent) {
(void)txt->generateAllCovers([&](int percent) {
const unsigned long now = millis();
if ((now - lastUpdate) >= 3000) {
lastUpdate = now;
@@ -658,15 +658,15 @@ void TxtReaderActivity::saveProgress() const {
if (SdMan.openFileForWrite("TRS", txt->getCachePath() + "/progress.bin", f)) {
// New format: version + byte offset + page number (for backwards compatibility debugging)
serialization::writePod(f, PROGRESS_VERSION);
// Store byte offset - this is stable across font/setting changes
const size_t byteOffset = (currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size()))
? pageOffsets[currentPage] : 0;
const size_t byteOffset =
(currentPage >= 0 && currentPage < static_cast<int>(pageOffsets.size())) ? pageOffsets[currentPage] : 0;
serialization::writePod(f, static_cast<uint32_t>(byteOffset));
// Also store page number for debugging/logging purposes
serialization::writePod(f, static_cast<uint16_t>(currentPage));
f.close();
Serial.printf("[%lu] [TRS] Saved progress: page %d, offset %zu\n", millis(), currentPage, byteOffset);
}
@@ -677,24 +677,24 @@ void TxtReaderActivity::loadProgress() {
if (SdMan.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) {
// Check file size to determine format
const size_t fileSize = f.size();
if (fileSize >= 7) {
// New format: version (1) + byte offset (4) + page number (2) = 7 bytes
uint8_t version;
serialization::readPod(f, version);
if (version == PROGRESS_VERSION) {
uint32_t savedOffset;
serialization::readPod(f, savedOffset);
uint16_t savedPage;
serialization::readPod(f, savedPage);
// Use byte offset to find the correct page (works even if re-indexed)
currentPage = findPageForOffset(savedOffset);
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n",
millis(), savedOffset, currentPage, totalPages, savedPage);
Serial.printf("[%lu] [TRS] Loaded progress: offset %u -> page %d/%d (was page %d)\n", millis(), savedOffset,
currentPage, totalPages, savedPage);
} else {
// Unknown version, fall back to legacy behavior
Serial.printf("[%lu] [TRS] Unknown progress version %d, ignoring\n", millis(), version);
@@ -708,7 +708,7 @@ void TxtReaderActivity::loadProgress() {
Serial.printf("[%lu] [TRS] Loaded legacy progress: page %d/%d\n", millis(), currentPage, totalPages);
}
}
// Bounds check
if (currentPage >= totalPages) {
currentPage = totalPages - 1;
@@ -716,7 +716,7 @@ void TxtReaderActivity::loadProgress() {
if (currentPage < 0) {
currentPage = 0;
}
f.close();
}
}
@@ -725,16 +725,16 @@ int TxtReaderActivity::findPageForOffset(size_t targetOffset) const {
if (pageOffsets.empty()) {
return 0;
}
// Binary search: find the largest offset that is <= targetOffset
// This finds the page that contains or starts at the target offset
auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), targetOffset);
if (it == pageOffsets.begin()) {
// Target is before the first page, return page 0
return 0;
}
// upper_bound returns iterator to first element > targetOffset
// So we need the element before it (which is <= targetOffset)
return static_cast<int>(std::distance(pageOffsets.begin(), it) - 1);
@@ -888,7 +888,8 @@ void TxtReaderActivity::renderEndOfBookPrompt() {
filename = filename.substr(lastSlash + 1);
}
if (filename.length() > 30) {
filename = filename.substr(0, 27) + "...";
filename.resize(27);
filename += "...";
}
renderer.drawCenteredText(UI_10_FONT_ID, 120, filename.c_str());

View File

@@ -20,6 +20,8 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;
const std::function<void()> onGoToClearCache;
const std::function<void()> onGoToSettings;
// End-of-book prompt state
bool showingEndOfBookPrompt = false;
@@ -56,11 +58,15 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
public:
explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt,
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome)
const std::function<void()>& onGoBack, const std::function<void()>& onGoHome,
const std::function<void()>& onGoToClearCache = nullptr,
const std::function<void()>& onGoToSettings = nullptr)
: ActivityWithSubactivity("TxtReader", renderer, mappedInput),
txt(std::move(txt)),
onGoBack(onGoBack),
onGoHome(onGoHome) {}
onGoHome(onGoHome),
onGoToClearCache(onGoToClearCache),
onGoToSettings(onGoToSettings) {}
void onEnter() override;
void onExit() override;
void loop() override;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,165 @@
#include "QuickMenuActivity.h"
#include <GfxRenderer.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"};
} // namespace
void QuickMenuActivity::taskTrampoline(void* param) {
auto* self = static_cast<QuickMenuActivity*>(param);
self->displayTaskLoop();
}
void QuickMenuActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Reset selection
selectedIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&QuickMenuActivity::taskTrampoline, "QuickMenuTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void QuickMenuActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void QuickMenuActivity::loop() {
// Handle back button - cancel
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onCancel();
return;
}
// Handle confirm button - select current option
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
QuickMenuAction action;
switch (selectedIndex) {
case 0:
action = QuickMenuAction::DICTIONARY;
break;
case 1:
action = QuickMenuAction::ADD_BOOKMARK;
break;
case 2:
action = QuickMenuAction::CLEAR_CACHE;
break;
case 3:
default:
action = QuickMenuAction::GO_TO_SETTINGS;
break;
}
onActionSelected(action);
return;
}
// Handle navigation
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 = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT;
updateRequired = true;
} else if (nextPressed) {
selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT;
updateRequired = true;
}
}
void QuickMenuActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void QuickMenuActivity::render() const {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Get bezel offsets
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
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
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, marginTop, "Quick Menu", true, EpdFontFamily::BOLD);
// Select descriptions based on bookmark state
const char* const* descriptions = isPageBookmarked ? MENU_DESCRIPTIONS_REMOVE : MENU_DESCRIPTIONS_ADD;
// 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;
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
const int itemY = startY + i * itemHeight;
const bool isSelected = (i == selectedIndex);
// 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";
}
renderer.drawText(UI_10_FONT_ID, marginLeft + 20, itemY, itemText, !isSelected);
renderer.drawText(SMALL_FONT_ID, marginLeft + 20, itemY + 22, descriptions[i], !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);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,46 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "../Activity.h"
// Enum for quick menu selection
enum class QuickMenuAction { DICTIONARY, ADD_BOOKMARK, CLEAR_CACHE, GO_TO_SETTINGS };
/**
* QuickMenuActivity presents a quick access menu triggered by short power button press.
* Options:
* - "Dictionary" - Look up a word
* - "Add/Remove Bookmark" - Toggle bookmark on current page
*
* The onActionSelected callback is called with the user's choice.
* The onCancel callback is called if the user presses back.
*/
class QuickMenuActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectedIndex = 0;
bool updateRequired = false;
const std::function<void(QuickMenuAction)> onActionSelected;
const std::function<void()> onCancel;
const bool isPageBookmarked; // True if current page already has a bookmark
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public:
explicit QuickMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void(QuickMenuAction)>& onActionSelected,
const std::function<void()>& onCancel, bool isPageBookmarked = false)
: Activity("QuickMenu", renderer, mappedInput),
onActionSelected(onActionSelected),
onCancel(onCancel),
isPageBookmarked(isPageBookmarked) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
#include <Arduino.h>
#include <BitmapHelpers.h>
#include <EInkDisplay.h>
#include <Epub.h>
#include <GfxRenderer.h>
@@ -10,8 +11,6 @@
#include <cstring>
#include <BitmapHelpers.h>
#include "Battery.h"
#include "BookListStore.h"
#include "CrossPointSettings.h"
@@ -21,11 +20,13 @@
#include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h"
#include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/home/BookmarkListActivity.h"
#include "activities/home/HomeActivity.h"
#include "activities/home/ListViewActivity.h"
#include "activities/home/MyLibraryActivity.h"
#include "activities/network/CrossPointWebServerActivity.h"
#include "activities/reader/ReaderActivity.h"
#include "activities/settings/ClearCacheActivity.h"
#include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "fontIds.h"
@@ -121,11 +122,8 @@ unsigned long t2 = 0;
// Memory debugging helper - logs heap state for tracking leaks
#ifdef DEBUG_MEMORY
void logMemoryState(const char* tag, const char* context) {
Serial.printf("[%lu] [%s] [MEM] %s - Free: %d, Largest: %d, MinFree: %d\n",
millis(), tag, context,
ESP.getFreeHeap(),
ESP.getMaxAllocHeap(),
ESP.getMinFreeHeap());
Serial.printf("[%lu] [%s] [MEM] %s - Free: %d, Largest: %d, MinFree: %d\n", millis(), tag, context, ESP.getFreeHeap(),
ESP.getMaxAllocHeap(), ESP.getMinFreeHeap());
}
#else
// No-op when not in debug mode
@@ -136,6 +134,7 @@ void logMemoryState(const char* tag, const char* context) {
static String flashCmdBuffer;
void checkForFlashCommand() {
if (!Serial) return; // Early exit if Serial not initialized
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
@@ -194,39 +193,31 @@ void checkForFlashCommand() {
// USB port locations: Portrait=bottom-left, PortraitInverted=top-right,
// LandscapeCW=top-left, LandscapeCCW=bottom-right
// Position offsets: edge margin + half-width offset to center on USB port
constexpr int edgeMargin = 28; // Distance from screen edge
constexpr int edgeMargin = 28; // Distance from screen edge
constexpr int halfWidth = LOCK_ICON_WIDTH / 2; // 16px offset for centering
int iconX, iconY;
GfxRenderer::ImageRotation rotation;
int outW, outH; // Note: 90/270 rotation swaps output dimensions
// Note: 90/270 rotation swaps output dimensions (W<->H)
switch (renderer.getOrientation()) {
case GfxRenderer::Portrait: // USB at bottom-left, shackle points right
rotation = GfxRenderer::ROTATE_90;
outW = LOCK_ICON_HEIGHT;
outH = LOCK_ICON_WIDTH;
iconX = edgeMargin;
iconY = screenH - outH - edgeMargin - halfWidth;
iconY = screenH - LOCK_ICON_WIDTH - edgeMargin - halfWidth;
break;
case GfxRenderer::PortraitInverted: // USB at top-right, shackle points left
rotation = GfxRenderer::ROTATE_270;
outW = LOCK_ICON_HEIGHT;
outH = LOCK_ICON_WIDTH;
iconX = screenW - outW - edgeMargin;
iconX = screenW - LOCK_ICON_HEIGHT - edgeMargin;
iconY = edgeMargin + halfWidth;
break;
case GfxRenderer::LandscapeClockwise: // USB at top-left, shackle points down
rotation = GfxRenderer::ROTATE_180;
outW = LOCK_ICON_WIDTH;
outH = LOCK_ICON_HEIGHT;
iconX = edgeMargin + halfWidth;
iconY = edgeMargin;
break;
case GfxRenderer::LandscapeCounterClockwise: // USB at bottom-right, shackle points up
rotation = GfxRenderer::ROTATE_0;
outW = LOCK_ICON_WIDTH;
outH = LOCK_ICON_HEIGHT;
iconX = screenW - outW - edgeMargin - halfWidth;
iconY = screenH - outH - edgeMargin;
iconX = screenW - LOCK_ICON_WIDTH - edgeMargin - halfWidth;
iconY = screenH - LOCK_ICON_HEIGHT - edgeMargin;
break;
}
renderer.drawImageRotated(LockIcon, iconX, iconY, LOCK_ICON_WIDTH, LOCK_ICON_HEIGHT, rotation);
@@ -336,10 +327,12 @@ void enterDeepSleep() {
void onGoHome();
void onGoToMyLibrary();
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab);
void onGoToClearCache();
void onGoToSettings();
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
exitActivity();
enterNewActivity(
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab));
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome,
onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
}
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
@@ -348,14 +341,28 @@ void onGoToReaderFromList(const std::string& bookPath) {
exitActivity();
// When opening from a list, treat it like opening from Recent (will return to list view via back)
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, MyLibraryActivity::Tab::Recent, onGoHome,
onGoToMyLibraryWithTab));
onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
}
// View a specific list
void onGoToListView(const std::string& listName) {
exitActivity();
enterNewActivity(
new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
enterNewActivity(new ListViewActivity(renderer, mappedInputManager, listName, onGoToMyLibrary, onGoToReaderFromList));
}
// View bookmarks for a specific book
void onGoToBookmarkList(const std::string& bookPath, const std::string& bookTitle) {
exitActivity();
enterNewActivity(new BookmarkListActivity(
renderer, mappedInputManager, bookPath, bookTitle,
onGoToMyLibrary, // On back, return to library
[bookPath](uint16_t spineIndex, uint32_t contentOffset) {
// Navigate to bookmark location in the book
// For now, just open the book (TODO: pass bookmark location to reader)
exitActivity();
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, MyLibraryActivity::Tab::Bookmarks,
onGoHome, onGoToMyLibraryWithTab, onGoToClearCache, onGoToSettings));
}));
}
// Go to pinned list (if exists) or Lists tab
@@ -368,7 +375,7 @@ void onGoToListsOrPinned() {
} else {
// Go to Lists tab in My Library
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
MyLibraryActivity::Tab::Lists));
onGoToBookmarkList, MyLibraryActivity::Tab::Lists));
}
}
@@ -382,14 +389,21 @@ void onGoToSettings() {
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
}
void onGoToClearCache() {
exitActivity();
enterNewActivity(new ClearCacheActivity(renderer, mappedInputManager, onGoHome));
}
void onGoToMyLibrary() {
exitActivity();
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView));
enterNewActivity(
new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
}
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
exitActivity();
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, tab, path));
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
onGoToBookmarkList, tab, path));
}
void onGoToBrowser() {
@@ -524,14 +538,13 @@ void loop() {
// Basic heap info
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
ESP.getHeapSize(), ESP.getMinFreeHeap());
// Detailed fragmentation info using ESP-IDF heap caps API
multi_heap_info_t info;
heap_caps_get_info(&info, MALLOC_CAP_8BIT);
Serial.printf("[%lu] [HEAP] Largest: %d, Allocated: %d, Blocks: %d, Free blocks: %d\n", millis(),
info.largest_free_block, info.total_allocated_bytes,
info.allocated_blocks, info.free_blocks);
info.largest_free_block, info.total_allocated_bytes, info.allocated_blocks, info.free_blocks);
lastMemPrint = millis();
}
@@ -566,7 +579,7 @@ void loop() {
const unsigned long loopDuration = millis() - loopStartTime;
if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration;
if (maxLoopDuration > 50) {
if (Serial && maxLoopDuration > 50) {
Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration,
activityDuration);
}