Compare commits
22 Commits
4080184b27
...
ef-0.15.99
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc6dc357eb | ||
|
|
ffe2aebd7e | ||
|
|
4965e63ad4 | ||
|
|
4db384edb6 | ||
|
|
f3075002c1 | ||
|
|
3e3be8bd23 | ||
|
|
800b07a2e5 | ||
|
|
2a31559747 | ||
|
|
c052512b1b | ||
|
|
bd95bfd44d | ||
|
|
fe446d4690 | ||
|
|
23e73312b4 | ||
|
|
e8d332e34f | ||
|
|
54004d5a5b | ||
|
|
d6e17c09ca | ||
|
|
7288e6499d | ||
|
|
5dab3ad5a3 | ||
|
|
82165c1022 | ||
|
|
e1fcec7d69 | ||
|
|
69a26ccb0e | ||
|
|
245d5a7dd8 | ||
|
|
e991fb10a6 |
41
.gitea/workflows/ci.yml
Normal file
41
.gitea/workflows/ci.yml
Normal 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
|
||||
40
.gitea/workflows/pr-formatting-check.yml
Normal file
40
.gitea/workflows/pr-formatting-check.yml
Normal 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
|
||||
40
.gitea/workflows/release.yml
Normal file
40
.gitea/workflows/release.yml
Normal 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
7
.gitignore
vendored
@@ -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
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
13
README.md
13
README.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
70
claude_notes/missing-serial-guards-2026-01-28.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Serial.printf Calls Without `if (Serial)` Guards
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Status:** Informational (not blocking issues)
|
||||
|
||||
## Summary
|
||||
|
||||
The codebase contains **408 Serial print calls** across 27 files in `src/`. Of these, only **16 calls** (in 2 files) have explicit `if (Serial)` guards.
|
||||
|
||||
**This is not a problem** because `Serial.setTxTimeoutMs(0)` is called in `setup()` before any activity code runs, making all Serial output non-blocking globally.
|
||||
|
||||
## Protection Mechanism
|
||||
|
||||
In `src/main.cpp` (lines 467-468):
|
||||
```cpp
|
||||
Serial.begin(115200);
|
||||
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||
```
|
||||
|
||||
This ensures that even without `if (Serial)` guards, Serial.printf calls will return immediately when USB is disconnected instead of blocking indefinitely.
|
||||
|
||||
## Files with `if (Serial)` Guards (16 calls)
|
||||
|
||||
| File | Protected Calls |
|
||||
|------|-----------------|
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | 15 |
|
||||
| `src/main.cpp` | 1 |
|
||||
|
||||
## Files Without Guards (392 calls)
|
||||
|
||||
These calls are protected by `Serial.setTxTimeoutMs(0)` but don't have explicit guards:
|
||||
|
||||
| File | Unguarded Calls |
|
||||
|------|-----------------|
|
||||
| `src/network/CrossPointWebServer.cpp` | 106 |
|
||||
| `src/activities/network/CrossPointWebServerActivity.cpp` | 49 |
|
||||
| `src/activities/boot_sleep/SleepActivity.cpp` | 33 |
|
||||
| `src/BookManager.cpp` | 25 |
|
||||
| `src/activities/reader/TxtReaderActivity.cpp` | 20 |
|
||||
| `src/activities/home/HomeActivity.cpp` | 16 |
|
||||
| `src/network/OtaUpdater.cpp` | 16 |
|
||||
| `src/util/Md5Utils.cpp` | 15 |
|
||||
| `src/main.cpp` | 13 (plus 1 guarded) |
|
||||
| `src/WifiCredentialStore.cpp` | 12 |
|
||||
| `src/network/HttpDownloader.cpp` | 12 |
|
||||
| `src/BookListStore.cpp` | 11 |
|
||||
| `src/activities/network/WifiSelectionActivity.cpp` | 11 |
|
||||
| `src/activities/settings/OtaUpdateActivity.cpp` | 9 |
|
||||
| `src/activities/browser/OpdsBookBrowserActivity.cpp` | 9 |
|
||||
| `src/activities/settings/ClearCacheActivity.cpp` | 7 |
|
||||
| `src/BookmarkStore.cpp` | 6 |
|
||||
| `src/RecentBooksStore.cpp` | 5 |
|
||||
| `src/activities/reader/ReaderActivity.cpp` | 4 |
|
||||
| `src/activities/Activity.h` | 3 |
|
||||
| `src/CrossPointSettings.cpp` | 3 |
|
||||
| `src/activities/network/CalibreConnectActivity.cpp` | 2 |
|
||||
| `src/activities/home/ListViewActivity.cpp` | 2 |
|
||||
| `src/activities/home/MyLibraryActivity.cpp` | 1 |
|
||||
| `src/activities/dictionary/DictionarySearchActivity.cpp` | 1 |
|
||||
| `src/CrossPointState.cpp` | 1 |
|
||||
|
||||
## Recommendation
|
||||
|
||||
No immediate action required. The global `Serial.setTxTimeoutMs(0)` protection is sufficient.
|
||||
|
||||
If desired, `if (Serial)` guards could be added to high-frequency logging paths for minor performance optimization (skipping format string processing), but this is low priority.
|
||||
|
||||
## Note on open-x4-sdk
|
||||
|
||||
The `open-x4-sdk` submodule also contains Serial calls (in `EInkDisplay.cpp`, `SDCardManager.cpp`). These are also protected by the global timeout setting since `Serial.begin()` and `setTxTimeoutMs()` are called before any SDK code executes.
|
||||
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
125
claude_notes/serial-blocking-debug-2026-01-28.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Serial Blocking Debug Session Summary
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Issue:** Device freezes when booted without USB connected
|
||||
**Resolution:** `Serial.setTxTimeoutMs(0)` - make Serial TX non-blocking
|
||||
|
||||
## Problem Description
|
||||
|
||||
During release preparation for ef-0.15.9, the device was discovered to freeze completely when:
|
||||
1. Unplugged from USB
|
||||
2. Powered on via power button
|
||||
3. Book page displays, then device becomes unresponsive
|
||||
4. No button presses register
|
||||
|
||||
The device worked perfectly when USB was connected.
|
||||
|
||||
## Investigation Process
|
||||
|
||||
### Initial Hypotheses Tested
|
||||
|
||||
Multiple hypotheses were systematically investigated:
|
||||
|
||||
1. **Hypothesis A-D:** Display/rendering mutex issues
|
||||
- Added mutex logging to SD card
|
||||
- Mutex operations completed successfully
|
||||
- Ruled out as root cause
|
||||
|
||||
2. **Hypothesis E:** FreeRTOS task creation issues
|
||||
- Task created and ran successfully
|
||||
- First render completed normally
|
||||
- Ruled out
|
||||
|
||||
3. **Hypothesis F-G:** Main loop execution
|
||||
- Added loop counter logging to SD card
|
||||
- **Key finding:** Main loop never started logging
|
||||
- Setup() completed but loop() never executed meaningful work
|
||||
|
||||
4. **Hypothesis H-J:** Various timing and initialization issues
|
||||
- Tested different delays and initialization orders
|
||||
- No improvement
|
||||
|
||||
### Root Cause Discovery
|
||||
|
||||
The breakthrough came from analyzing the boot sequence:
|
||||
|
||||
1. `setup()` completes successfully
|
||||
2. `EpubReaderActivity::onEnter()` runs and calls `Serial.printf()` to log progress
|
||||
3. **Device hangs at Serial.printf() call**
|
||||
|
||||
On ESP32-C3 with USB CDC (USB serial), `Serial.printf()` blocks indefinitely waiting for the TX buffer to drain when USB is not connected. The default behavior expects a host to read the data.
|
||||
|
||||
### Evidence
|
||||
|
||||
- When USB connected: `Serial.printf()` returns immediately (data sent to host)
|
||||
- When USB disconnected: `Serial.printf()` blocks forever waiting for TX buffer space
|
||||
- The hang occurred specifically in `EpubReaderActivity.cpp` during progress logging
|
||||
|
||||
## Solution
|
||||
|
||||
### Primary Fix
|
||||
|
||||
Configure Serial to be non-blocking in `src/main.cpp`:
|
||||
|
||||
```cpp
|
||||
// Always initialize Serial but make it non-blocking
|
||||
Serial.begin(115200);
|
||||
Serial.setTxTimeoutMs(0); // Non-blocking TX - critical for USB disconnect handling
|
||||
```
|
||||
|
||||
`Serial.setTxTimeoutMs(0)` tells the ESP32 Arduino core to return immediately from Serial write operations if the buffer is full, rather than blocking.
|
||||
|
||||
### Secondary Protection (Belt and Suspenders)
|
||||
|
||||
Added `if (Serial)` guards to high-traffic Serial calls in `EpubReaderActivity.cpp`:
|
||||
|
||||
```cpp
|
||||
if (Serial) Serial.printf("[%lu] [ERS] Loaded progress...\n", millis());
|
||||
```
|
||||
|
||||
This provides an additional check before attempting to print, though it's not strictly necessary with the timeout set to 0.
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/main.cpp` | Added `Serial.setTxTimeoutMs(0)` after `Serial.begin()` |
|
||||
| `src/main.cpp` | Added `if (Serial)` guard to auto-sleep log |
|
||||
| `src/main.cpp` | Added `if (Serial)` guard to max loop duration log |
|
||||
| `src/activities/reader/EpubReaderActivity.cpp` | Added 16 `if (Serial)` guards |
|
||||
|
||||
## Verification
|
||||
|
||||
After applying the fix:
|
||||
1. Device boots successfully when unplugged from USB
|
||||
2. Book pages render correctly
|
||||
3. Button presses register normally
|
||||
4. Sleep/wake cycle works
|
||||
5. No functionality lost when USB is connected
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **ESP32-C3 USB CDC behavior:** Serial output can block indefinitely without a connected host
|
||||
2. **Always set non-blocking:** `Serial.setTxTimeoutMs(0)` should be standard for battery-powered devices
|
||||
3. **Debug logging location matters:** When debugging hangs, SD card logging proved essential since Serial was the problem
|
||||
4. **Systematic hypothesis testing:** Ruled out many red herrings (mutex, task, rendering) before finding the true cause
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Why This Affects ESP32-C3 Specifically
|
||||
|
||||
The ESP32-C3 uses native USB CDC for serial communication (no external USB-UART chip). The Arduino core's default behavior is to wait for TX buffer space, which requires an active USB host connection.
|
||||
|
||||
### Alternative Approaches Considered
|
||||
|
||||
1. **Only initialize Serial when USB connected:** Partially implemented, but insufficient because USB can be disconnected after boot
|
||||
2. **Add `if (Serial)` guards everywhere:** Too invasive (400+ calls)
|
||||
3. **Disable Serial entirely:** Would lose debug output when USB connected
|
||||
|
||||
The chosen solution (`setTxTimeoutMs(0)`) provides the best balance: debug output works when USB is connected, device operates normally when disconnected.
|
||||
|
||||
## References
|
||||
|
||||
- ESP32 Arduino Core Serial documentation
|
||||
- ESP-IDF USB CDC documentation
|
||||
- FreeRTOS queue behavior (initial red herring investigation)
|
||||
132
claude_notes/usb-serial-blocking-fix-2026-01-28.md
Normal file
132
claude_notes/usb-serial-blocking-fix-2026-01-28.md
Normal 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.
|
||||
422
docs/branch-comparison-summary.md
Normal file
422
docs/branch-comparison-summary.md
Normal 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.
|
||||
603
docs/crosspoint-ef-features.md
Normal file
603
docs/crosspoint-ef-features.md
Normal 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+
|
||||
555
docs/crosspoint-ef-user-guide.md
Normal file
555
docs/crosspoint-ef-user-guide.md
Normal 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 |
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -588,8 +588,12 @@ static std::string decodeHtmlEntity(const std::string& html, size_t& i) {
|
||||
const char* replacement;
|
||||
};
|
||||
static const EntityMapping entities[] = {
|
||||
{" ", " "}, {"<", "<"}, {">", ">"},
|
||||
{"&", "&"}, {""", "\""}, {"'", "'"},
|
||||
{" ", " "},
|
||||
{"<", "<"},
|
||||
{">", ">"},
|
||||
{"&", "&"},
|
||||
{""", "\""},
|
||||
{"'", "'"},
|
||||
{"—", "\xe2\x80\x94"}, // —
|
||||
{"–", "\xe2\x80\x93"}, // –
|
||||
{"…", "\xe2\x80\xa6"}, // …
|
||||
@@ -688,8 +692,8 @@ std::string StarDict::stripHtml(const std::string& html) {
|
||||
|
||||
// Extract tag name
|
||||
size_t tagEnd = tagStart;
|
||||
while (tagEnd < html.length() && !std::isspace(static_cast<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++;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Submodule open-x4-sdk updated: bd4e670750...dede09001c
@@ -2,7 +2,7 @@
|
||||
default_envs = default
|
||||
|
||||
[crosspoint]
|
||||
version = 0.15.0
|
||||
version = ef-0.15.99
|
||||
|
||||
[base]
|
||||
platform = espressif32 @ 6.12.0
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -81,5 +81,4 @@ class BookListStore {
|
||||
* @return Book count, or -1 if list doesn't exist
|
||||
*/
|
||||
static int getBookCount(const std::string& name);
|
||||
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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", "< >", "");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", "", "");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
165
src/activities/util/QuickMenuActivity.cpp
Normal file
165
src/activities/util/QuickMenuActivity.cpp
Normal 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();
|
||||
}
|
||||
46
src/activities/util/QuickMenuActivity.h
Normal file
46
src/activities/util/QuickMenuActivity.h
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
81
src/main.cpp
81
src/main.cpp
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user