mod: Phase 1 - bring forward mod-exclusive files with ActivityManager migration
Brings ~55 mod-exclusive files to the upstream-based mod/master-resync branch: Activities (migrated to new ActivityManager pattern): - Clock/Time: SetTimeActivity, SetTimezoneOffsetActivity, NtpSyncActivity - Dictionary: DictionaryDefinitionActivity, DictionarySuggestionsActivity, DictionaryWordSelectActivity, LookedUpWordsActivity - Bookmark: EpubReaderBookmarkSelectionActivity - Book management: BookManageMenuActivity, EndOfBookMenuActivity - OPDS: OpdsServerListActivity, OpdsSettingsActivity - Utility: DirectoryPickerActivity, NumericStepperActivity Utilities (unchanged): - BookManager, BookSettings, BookmarkStore, BootNtpSync - Dictionary, LookupHistory, TimeSync, OpdsServerStore Libraries: PlaceholderCover, TableData, ChapterXPathIndexer Scripts: inject_mod_version, generate_book_icon, preview_placeholder_cover Docs: KOReader sync XPath mapping Migration changes: - ActivityWithSubactivity -> Activity base class - Callback constructors -> finish()/setResult() pattern - enterNewActivity() -> startActivityForResult() - Activity::RenderLock&& -> RenderLock&& These files won't compile yet - they reference mod settings and I18n strings that will be added in subsequent phases. Made-with: Cursor
171
.cursor/plans/ttf_font_investigation_61ba7279.plan.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: TTF Font Investigation
|
||||
overview: Investigate replacing compile-time bitmap fonts with runtime TTF rendering using stb_truetype (the core of lvgl-ttf-esp32), integrated into the existing custom GfxRenderer pipeline for the ESP32-C3 e-ink reader.
|
||||
todos:
|
||||
- id: poc-stb
|
||||
content: "Phase 1: Add stb_truetype.h and build a minimal proof-of-concept that loads a TTF from SD, rasterizes glyphs, and draws them via GfxRenderer"
|
||||
status: pending
|
||||
- id: measure-ram
|
||||
content: "Phase 1: Measure actual RAM consumption and render performance of stb_truetype on ESP32-C3"
|
||||
status: pending
|
||||
- id: spiffs-mmap
|
||||
content: "Phase 3: Test SPIFFS memory-mapping of TTF files using esp_partition_mmap() to avoid loading into RAM"
|
||||
status: pending
|
||||
- id: font-provider
|
||||
content: "Phase 2: Create FontProvider abstraction layer and TtfFontProvider with glyph caching"
|
||||
status: pending
|
||||
- id: renderer-refactor
|
||||
content: "Phase 2: Refactor GfxRenderer to use FontProvider interface instead of direct EpdFontFamily"
|
||||
status: pending
|
||||
- id: settings-integration
|
||||
content: "Phase 4: Update settings to support arbitrary font sizes and custom font selection"
|
||||
status: pending
|
||||
- id: remove-bitmap-fonts
|
||||
content: "Phase 5: Remove compiled bitmap reader fonts, keep only small UI bitmap fonts"
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# TTF Font Rendering Investigation
|
||||
|
||||
## Current State
|
||||
|
||||
The project uses **no LVGL** -- it has a custom `GfxRenderer` that draws directly into an e-ink framebuffer. Fonts are pre-rasterized offline (TTF -> Python FreeType script -> C header bitmaps) and embedded at compile time.
|
||||
|
||||
**Cost of current approach:**
|
||||
|
||||
- **~2.7 MB flash** (mod build, Bookerly + NotoSans + Ubuntu) up to **~7 MB** (full build with OpenDyslexic)
|
||||
- **Only 4 discrete sizes** per family (12/14/16/18 pt) -- no runtime scaling
|
||||
- Each size x style (regular/bold/italic/bold-italic) is a separate ~80-200 KB bitmap blob
|
||||
- App partition is only **6.25 MB** -- fonts consume 43-100%+ of available space
|
||||
|
||||
## Why lvgl-ttf-esp32 Is Relevant (and What Isn't)
|
||||
|
||||
The [lvgl-ttf-esp32](https://github.com/huming2207/lvgl-ttf-esp32) repo wraps **stb_truetype** (a single-header C library) with an LVGL font driver. Since this project does not use LVGL, the wrapper is irrelevant, but the **stb_truetype library itself** is exactly what's needed -- a lightweight, zero-dependency TTF rasterizer that runs on ESP32.
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph current [Current Pipeline]
|
||||
TTF_Offline["TTF files (offline)"] --> fontconvert["fontconvert.py (FreeType)"]
|
||||
fontconvert --> headers["56 .h files (~2.7-7 MB flash)"]
|
||||
headers --> EpdFont["EpdFont / EpdFontFamily"]
|
||||
EpdFont --> GfxRenderer["GfxRenderer::renderChar()"]
|
||||
end
|
||||
|
||||
subgraph proposed [Proposed Pipeline]
|
||||
TTF_SD["TTF files on SD card (~100-500 KB each)"] --> stb["stb_truetype.h (runtime)"]
|
||||
stb --> cache["Glyph cache (RAM + SD)"]
|
||||
cache --> TtfFont["TtfFont (new class)"]
|
||||
TtfFont --> FontProvider["FontProvider interface"]
|
||||
FontProvider --> GfxRenderer2["GfxRenderer::renderChar()"]
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Core Idea
|
||||
|
||||
1. **stb_truetype.h** -- add as a single header file in `lib/`. It rasterizes individual glyphs from TTF data on demand.
|
||||
2. **TTF files on SD card** -- load at runtime from `.crosspoint/fonts/`. A typical TTF family (4 styles) is ~400-800 KB total vs 2.7 MB+ as bitmaps.
|
||||
3. **Glyph cache** -- since e-ink pages are static, cache rasterized glyphs in RAM (LRU, ~20-50 KB) and optionally persist to SD card to avoid re-rasterizing across page turns.
|
||||
4. `**FontProvider` abstraction** -- interface over both `EpdFont` (bitmap, for UI fonts) and new `TtfFont` (runtime, for reader fonts), so both can coexist.
|
||||
|
||||
## Integration Points
|
||||
|
||||
These are the key files/interfaces that would need changes:
|
||||
|
||||
|
||||
| Component | File | Change |
|
||||
| ----------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| Font abstraction | New `lib/FontProvider/` | `FontProvider` interface with `getGlyph()`, `getMetrics()` |
|
||||
| TTF renderer | New `lib/TtfFont/` | Wraps stb_truetype, manages TTF loading + glyph cache |
|
||||
| GfxRenderer | [lib/GfxRenderer/GfxRenderer.h](lib/GfxRenderer/GfxRenderer.h) | Change `fontMap` from `EpdFontFamily` to `FontProvider*`; update `renderChar`, `getTextWidth`, `getSpaceWidth` |
|
||||
| Font registration | [src/main.cpp](src/main.cpp) | Register TTF fonts from SD instead of (or alongside) bitmap fonts |
|
||||
| Settings | [src/CrossPointSettings.cpp](src/CrossPointSettings.cpp) | `getReaderFontId()` supports arbitrary sizes, not just 4 discrete ones |
|
||||
| PlaceholderCover | [lib/PlaceholderCover/PlaceholderCoverGenerator.cpp](lib/PlaceholderCover/PlaceholderCoverGenerator.cpp) | Uses own `renderGlyph()` -- needs similar adaptation |
|
||||
| Text layout | [lib/Epub/Epub/ParsedText.cpp](lib/Epub/Epub/ParsedText.cpp) | Uses `getTextWidth()` / `getSpaceWidth()` for line breaking -- works unchanged if FontProvider is transparent |
|
||||
|
||||
|
||||
## Feasibility Analysis
|
||||
|
||||
### Memory (ESP32-C3, ~380 KB RAM)
|
||||
|
||||
- **stb_truetype itself**: ~15-20 KB code in flash, minimal RAM overhead
|
||||
- **TTF file in memory**: requires the full TTF loaded into RAM for glyph access. Options:
|
||||
- **Memory-mapped from flash (SPIFFS)**: store TTF in SPIFFS (3.4 MB available, currently unused), memory-map via `mmap()` on ESP-IDF -- zero RAM cost
|
||||
- **Partial loading from SD**: read only needed tables on demand (stb_truetype supports custom `stbtt_read()` but the default API expects full file in memory)
|
||||
- **Load into PSRAM**: ESP32-C3 has no PSRAM, so this is not an option
|
||||
- **Glyph cache**: ~50 bytes metadata + bitmap per glyph. At 18pt, a glyph bitmap is ~20x25 pixels = ~63 bytes (1-bit). Caching 256 glyphs = ~30 KB RAM.
|
||||
- **Rasterization temp buffer**: stb_truetype allocates ~10-20 KB temporarily per glyph render (uses `malloc`)
|
||||
|
||||
**Verdict**: The biggest constraint is holding the TTF file in RAM. A typical Bookerly-Regular.ttf is ~150 KB. With 4 styles loaded, that's ~600 KB -- **too much for 380 KB RAM**. The viable path is using **SPIFFS** to store TTFs and memory-map them, or implementing a chunked reader that loads TTF table data on demand from SD.
|
||||
|
||||
### Flash Savings
|
||||
|
||||
- **Remove**: 2.7-7 MB of bitmap font headers from firmware
|
||||
- **Add**: ~40 KB for stb_truetype + TtfFont code
|
||||
- **Net savings**: **2.6-6.9 MB flash freed**
|
||||
- TTF files move to SD card or SPIFFS (not in firmware)
|
||||
|
||||
### Performance
|
||||
|
||||
- stb_truetype rasterizes a glyph in **~0.5-2 ms** on ESP32 (160 MHz)
|
||||
- A typical page has ~~200-300 glyphs, but with caching, only unique glyphs need rasterizing (~~60-80 per page)
|
||||
- **First page render**: ~60-160 ms extra for cache warmup
|
||||
- **Subsequent pages**: mostly cache hits, negligible overhead
|
||||
- E-ink refresh takes ~300-1000 ms anyway, so TTF rasterization cost is acceptable
|
||||
|
||||
### Anti-aliasing for E-ink
|
||||
|
||||
stb_truetype produces 8-bit alpha bitmaps (256 levels). The current system uses 1-bit or 2-bit glyphs. The adapter would:
|
||||
|
||||
- **1-bit mode**: threshold the alpha (e.g., alpha > 128 = black)
|
||||
- **2-bit mode**: quantize to 4 levels (0, 85, 170, 255) for e-ink grayscale
|
||||
|
||||
This should actually produce **better quality** than the offline FreeType conversion since stb_truetype does sub-pixel hinting.
|
||||
|
||||
## Recommended Implementation Phases
|
||||
|
||||
### Phase 1: Proof of Concept (stb_truetype standalone)
|
||||
|
||||
- Add stb_truetype.h to the project
|
||||
- Write a minimal test that loads a TTF from SD, rasterizes a few glyphs, and draws them via `GfxRenderer::drawPixel()`
|
||||
- Measure RAM usage and render time
|
||||
- Validate glyph quality on e-ink
|
||||
|
||||
### Phase 2: FontProvider Abstraction
|
||||
|
||||
- Create `FontProvider` interface matching `EpdFontFamily`'s public API
|
||||
- Wrap existing `EpdFontFamily` in a `BitmapFontProvider`
|
||||
- Create `TtfFontProvider` backed by stb_truetype + glyph cache
|
||||
- Refactor `GfxRenderer::fontMap` to use `FontProvider*`
|
||||
|
||||
### Phase 3: TTF Storage Strategy
|
||||
|
||||
- Evaluate SPIFFS memory mapping vs. SD-card chunked loading
|
||||
- Implement the chosen strategy
|
||||
- Handle font discovery (scan SD card for `.ttf` files)
|
||||
|
||||
### Phase 4: Settings and UI Integration
|
||||
|
||||
- Replace discrete font-size enum with a continuous size setting (or finer granularity)
|
||||
- Add "Custom Font" option in settings
|
||||
- Update section cache invalidation when font/size changes
|
||||
|
||||
### Phase 5: Remove Bitmap Reader Fonts
|
||||
|
||||
- Keep bitmap fonts only for UI (Ubuntu 10/12, NotoSans 8) which are small (~62 KB)
|
||||
- Remove Bookerly, NotoSans, OpenDyslexic bitmap headers
|
||||
- Ship TTF files on SD card (or downloadable)
|
||||
|
||||
## Key Risk: TTF-in-RAM on ESP32-C3
|
||||
|
||||
The critical question is whether TTF file data can be accessed without loading the full file into RAM. Three mitigation strategies:
|
||||
|
||||
1. **SPIFFS + mmap**: Store TTFs in the 3.4 MB SPIFFS partition and use ESP-IDF's `esp_partition_mmap()` to map them into the address space. Zero RAM cost, but SPIFFS is read-only after flashing (unless written at runtime).
|
||||
2. **SD card + custom I/O**: Implement `stbtt_GetFontOffsetForIndex` and glyph extraction using buffered SD reads. stb_truetype's API assumes a contiguous byte array, so this would require a patched or wrapper approach.
|
||||
3. **Load one style at a time**: Only keep the active style's TTF in RAM (~150 KB). Switch styles by unloading/reloading. Feasible but adds latency on style changes (bold/italic).
|
||||
|
||||
Strategy 1 (SPIFFS mmap) is the most promising since the SPIFFS partition is already allocated but unused.
|
||||
21
chat-summaries/2026-02-09_00-00-summary.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Project Documentation for CrossPoint Reader Firmware
|
||||
|
||||
**Date:** 2026-02-09
|
||||
|
||||
## Task
|
||||
|
||||
Create three documentation files in `/mod/docs/` covering the Xteink X4 hardware capabilities, project file structure, and CI/build/code style for the CrossPoint Reader firmware.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
|
||||
1. **`mod/docs/hardware.md`** -- Device hardware capabilities documentation covering CPU (ESP32-C3), 16MB flash with partition layout, 480x800 eink display (2-bit grayscale, 3 refresh modes), 7 physical buttons, SD card storage, battery monitoring, WiFi networking, USB-C, and power management (deep sleep with GPIO wakeup).
|
||||
|
||||
2. **`mod/docs/file-structure.md`** -- Complete project file structure map documenting `src/` (main app with Activity-based UI system), `lib/` (16 libraries including EPUB engine, HAL, fonts, XML/ZIP/JPEG support), `open-x4-sdk/` (hardware driver submodule), `scripts/` (build helpers), `test/` (hyphenation tests), `docs/` (upstream docs), and `.github/` (CI/templates).
|
||||
|
||||
3. **`mod/docs/ci-build-and-code-style.md`** -- CI pipeline documentation covering 4 GitHub Actions workflows (CI with format/cppcheck/build/gate jobs, release, RC, PR title check), PlatformIO build system with 3 environments, clang-format 21 rules (2-space indent, 120-col limit, K&R braces), cppcheck static analysis config, and contribution guidelines (semantic PR titles, AI disclosure, scope alignment).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None. All three documents are complete and ready for review.
|
||||
59
chat-summaries/2026-02-09_04-43-summary.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Sleep Screen Tweaks Implementation
|
||||
|
||||
## Task Description
|
||||
Implemented the two "Sleep Screen tweaks" from the plan:
|
||||
1. **Gradient fill for letterboxed areas** - When a sleep screen image doesn't match the display's aspect ratio, the void (letterbox) areas are now filled with a dithered gradient sampled from the nearest ~20 pixels of the image's edge, fading toward white.
|
||||
2. **Fix "Fit" mode for small images** - Images smaller than the 480x800 display are now scaled up (nearest-neighbor) to fit while preserving aspect ratio, instead of being displayed at native size with wasted screen space.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.cpp`
|
||||
- Modified `drawBitmap()` scaling logic: when both `maxWidth` and `maxHeight` are provided, always computes an aspect-ratio-preserving scale factor (supports both upscaling and downscaling)
|
||||
- Modified `drawBitmap()` rendering loop: uses block-fill approach where each source pixel maps to a screen rectangle (handles both upscaling blocks and 1:1/downscaling single pixels via a unified loop)
|
||||
- Applied same changes to `drawBitmap1Bit()` for 1-bit bitmap consistency
|
||||
- Added `drawPixelGray()` method: draws a pixel using its 2-bit grayscale value, dispatching correctly based on the current render mode (BW, GRAYSCALE_LSB, GRAYSCALE_MSB)
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.h`
|
||||
- Added `drawPixelGray(int x, int y, uint8_t val2bit)` declaration
|
||||
|
||||
### `lib/GfxRenderer/BitmapHelpers.cpp`
|
||||
- Added `quantizeNoiseDither()`: hash-based noise dithering that always uses noise (unlike `quantize()` which is controlled by a compile-time flag), used for smooth gradient rendering on the 4-level display
|
||||
|
||||
### `lib/GfxRenderer/BitmapHelpers.h`
|
||||
- Added `quantizeNoiseDither()` declaration
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- Removed the `if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight)` gate in `renderBitmapSleepScreen()` — position/scale is now always computed regardless of whether the image is larger or smaller than the screen
|
||||
- Added anonymous namespace with:
|
||||
- `LetterboxGradientData` struct for edge sample storage
|
||||
- `sampleBitmapEdges()`: reads bitmap row-by-row to sample per-column (or per-row) average gray values from the first/last 20 pixels of the image edges
|
||||
- `drawLetterboxGradients()`: draws dithered gradients in letterbox areas using sampled edge colors, interpolating toward white
|
||||
- Integrated gradient rendering into the sleep screen flow: edge sampling (first pass), then gradient + bitmap rendering in BW pass, and again in each grayscale pass (LSB, MSB)
|
||||
|
||||
## Follow-up: Letterbox Fill Settings
|
||||
|
||||
Added three letterbox fill options and two new persisted settings:
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Added `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE` (plain white), `LETTERBOX_GRADIENT` (default), `LETTERBOX_SOLID`
|
||||
- Added `SLEEP_SCREEN_GRADIENT_DIR` enum: `GRADIENT_TO_WHITE` (default), `GRADIENT_TO_BLACK`
|
||||
- Added `sleepScreenLetterboxFill` and `sleepScreenGradientDir` member fields
|
||||
|
||||
### `src/CrossPointSettings.cpp`
|
||||
- Incremented `SETTINGS_COUNT` to 32
|
||||
- Added serialization (read/write) for the two new fields at the end for backward compatibility
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Added "Letterbox Fill" menu entry (None / Gradient / Solid) in Display category
|
||||
- Added "Gradient Direction" menu entry (To White / To Black) in Display category
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- Renamed `drawLetterboxGradients` → `drawLetterboxFill` with added `solidFill` and `targetColor` parameters
|
||||
- Solid mode: uses edge color directly (no distance-based interpolation), quantized with noise dithering
|
||||
- Gradient direction: interpolates from edge color toward `targetColor` (255 for white, 0 for black)
|
||||
- `renderBitmapSleepScreen` reads the settings and skips edge sampling entirely when fill mode is "None"
|
||||
|
||||
## Follow-up Items
|
||||
- Test with various cover image sizes and aspect ratios on actual hardware
|
||||
- Test custom images from `/sleep/` directory (1-bit and multi-bit)
|
||||
- Monitor RAM usage via serial during gradient rendering
|
||||
25
chat-summaries/2026-02-09_16-29-summary.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Edge Data Caching for Sleep Screen Letterbox Fill
|
||||
|
||||
## Task
|
||||
Cache the letterbox edge-sampling calculations so they are only computed once per cover image (on first sleep) and reused from a binary cache file on subsequent sleeps.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.h`
|
||||
- Added `#include <string>` and updated `renderBitmapSleepScreen` signature to accept an optional `edgeCachePath` parameter (defaults to empty string for no caching).
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- Added `#include <Serialization.h>` for binary read/write helpers.
|
||||
- Added `loadEdgeCache()` function in the anonymous namespace: loads edge data from a binary cache file, validates the cache version and screen dimensions against current values to detect stale data.
|
||||
- Added `saveEdgeCache()` function: writes edge data (edgeA, edgeB arrays + metadata) to a compact binary file (~10 bytes header + 2 * edgeCount bytes data, typically under 1KB).
|
||||
- Updated `renderBitmapSleepScreen()`: tries loading from cache before sampling. On cache miss, samples edges from bitmap and saves to cache. On cache hit, skips the bitmap sampling pass entirely.
|
||||
- Updated `renderCoverSleepScreen()`: derives the edge cache path from the cover BMP path (e.g. `cover.bmp` -> `cover_edges.bin`) and passes it to `renderBitmapSleepScreen`. The cache is stored alongside the cover BMP in the book's `.crosspoint` directory.
|
||||
- Custom sleep images (`renderCustomSleepScreen`) do not use caching since the image may change randomly each sleep.
|
||||
|
||||
### Cache Design
|
||||
- **Format**: Binary file with version byte, screen dimensions, horizontal flag, edge count, letterbox sizes, and two edge color arrays.
|
||||
- **Invalidation**: Validated by cache version and screen dimensions. Naturally scoped per-book (lives in `/.crosspoint/epub_<hash>/`). Different cover modes (FIT vs CROP) produce different BMP files and thus different cache paths.
|
||||
- **Size**: ~970 bytes for a 480-wide image.
|
||||
|
||||
## Follow-up Items
|
||||
- None
|
||||
26
chat-summaries/2026-02-09_16-35-summary.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Letterbox Fill: 4-Mode Restructure (Solid, Blended, Gradient, None)
|
||||
|
||||
## Task
|
||||
Restructure the letterbox fill modes from 3 (None, Gradient, Solid) to 4 distinct modes with clearer semantics.
|
||||
|
||||
## New Modes
|
||||
1. **Solid** (new) - Picks the dominant (average) shade from the edge and fills the entire letterbox area with that single dithered color.
|
||||
2. **Blended** (renamed from old "Solid") - Uses per-pixel sampled edge colors with noise dithering, no distance-based interpolation.
|
||||
3. **Gradient** - Existing gradient behavior (interpolates per-pixel edge color toward a target color).
|
||||
4. **None** - No fill.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Updated `SLEEP_SCREEN_LETTERBOX_FILL` enum: `LETTERBOX_NONE=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_BLENDED=2`, `LETTERBOX_GRADIENT=3`.
|
||||
- Note: enum values changed from the old 3-value layout. Existing saved settings may need reconfiguring.
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Updated "Letterbox Fill" option labels to: "None", "Solid", "Blended", "Gradient".
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- `drawLetterboxFill()`: Changed signature from `bool solidFill` to `uint8_t fillMode`. Added dominant shade computation for SOLID mode (averages all edge samples into one value per side). BLENDED and SOLID both skip gradient interpolation; SOLID additionally skips per-pixel edge lookups.
|
||||
- `renderBitmapSleepScreen()`: Removed `solidFill` bool, passes `fillMode` directly to `drawLetterboxFill`. Updated log message to show the mode name.
|
||||
|
||||
## Follow-up Items
|
||||
- None
|
||||
46
chat-summaries/2026-02-12_17-08-summary.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Dictionary Feature Bug Fixes
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** mod/add-dictionary
|
||||
|
||||
## Task
|
||||
|
||||
Fix three bugs reported after initial implementation of PR #857 dictionary word lookup feature.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix: Lookup fails after first successful lookup (Dictionary.cpp)
|
||||
|
||||
**Root cause:** `cleanWord()` lowercases the search term, but the dictionary index is sorted case-sensitively (uppercase entries sort before lowercase). The binary search lands in the wrong segment on the second pass because the index is already loaded and the sparse offset table was built for a case-sensitive sort order.
|
||||
|
||||
**Fix:** Extracted the binary-search + linear-scan logic into a new `searchIndex()` private helper. The `lookup()` method now performs a **two-pass search**: first with the lowercased word, then with the first-letter-capitalized variant. This handles dictionaries that store headwords as "Hello" instead of "hello". Also removed `stripHtml` from the header since HTML is now rendered, not stripped.
|
||||
|
||||
**Files:** `src/util/Dictionary.h`, `src/util/Dictionary.cpp`
|
||||
|
||||
### 2. Fix: Raw HTML displayed in definitions (DictionaryDefinitionActivity)
|
||||
|
||||
**Root cause:** Dictionary uses `sametypesequence=h` (HTML format). The original activity rendered definitions as plain text.
|
||||
|
||||
**Fix:** Complete rewrite of the definition activity to **render HTML with styled text**:
|
||||
- New `parseHtml()` method tokenizes HTML into `TextAtom` structs (word + style + newline directives)
|
||||
- Supports bold (`<b>`, `<strong>`, headings), italic (`<i>`, `<em>`), and mixed bold-italic via `EpdFontFamily::Style`
|
||||
- Handles `<ol>` (numbered with digit/alpha support), `<ul>` (bullet points), nested lists with indentation
|
||||
- Decodes HTML entities (named + numeric/hex → UTF-8)
|
||||
- Skips `<svg>` content, treats `</html>` as section separators
|
||||
- `wrapText()` flows styled atoms into positioned line segments (`Segment` struct with x-offset and style)
|
||||
- `renderScreen()` draws each segment with correct position and style via `renderer.drawText(fontId, x, y, text, true, style)`
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
|
||||
|
||||
### 3. Fix: No button hints on word selection screen (DictionaryWordSelectActivity)
|
||||
|
||||
**Fix:** Added `GUI.drawButtonHints()` call at the end of `renderScreen()` with labels: "« Back", "✓ Lookup", "↕ Row", "↔ Word".
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- If the reader font doesn't include bold/italic variants, styled text gracefully falls back to regular style
|
||||
- Nested list indentation uses 15px per level after the first
|
||||
- Alpha list numbering (`list-style-type: lower-alpha`) is supported; other custom list styles fall back to numeric
|
||||
- Button hint labels may need tuning once tested on device (especially in landscape orientation)
|
||||
53
chat-summaries/2026-02-12_17-34-summary.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Dictionary Feature Bug Fixes (Round 2)
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** mod/add-dictionary
|
||||
|
||||
## Task
|
||||
|
||||
Fix three remaining bugs with the dictionary word lookup feature after initial implementation and first round of fixes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix: Lookup only works for first word searched (Dictionary.cpp)
|
||||
|
||||
**Root cause:** The binary search used C++ `operator<=` (case-sensitive, pure `strcmp` order) for comparing index entries, but StarDict `.idx` files are sorted using `stardict_strcmp` — a two-level comparison that sorts case-*insensitively* first, then uses case-sensitive `strcmp` as a tiebreaker. This means entries like "Simple" and "simple" are adjacent, and uppercase/lowercase entries for the same letter are interleaved (e.g., "Silver", "silver", "Simple", "simple", "Simpson", "simpson").
|
||||
|
||||
With the wrong sort order, the binary search overshoots for many words: uppercase entries like "Simpson" are case-sensitively < "simple" (because 'S' < 's'), so `lo` moves past the segment actually containing "simple". The linear scan then starts too late and doesn't find the word. By coincidence, some words (like "professor") happen to land in correct segments while others (like "simple") don't.
|
||||
|
||||
**Fix:**
|
||||
- Added `stardictCmp()` (case-insensitive first, then case-sensitive tiebreaker) and `asciiCaseCmp()` helper functions
|
||||
- Binary search now uses `stardictCmp(key, word) <= 0` instead of `key <= word`
|
||||
- Linear scan early termination now uses `stardictCmp(key, word) > 0` instead of `key > word`
|
||||
- Exact match now uses `asciiCaseCmp(key, word) == 0` (case-insensitive) since `cleanWord` lowercases the search term but the dictionary may store entries in any case
|
||||
- Removed the two-pass search approach (no longer needed — single pass handles all casing)
|
||||
|
||||
**Files:** `src/util/Dictionary.cpp`
|
||||
|
||||
### 2. Fix: Unrendered glyphs in pronunciation guide (DictionaryDefinitionActivity.cpp)
|
||||
|
||||
**Root cause:** The dictionary stores IPA pronunciation as raw text between entries, e.g., `/əmˈsɛlvz/` appearing between `</html>` and the next `<p>` tag. These IPA Extension Unicode characters (U+0250–U+02FF) are not in the e-ink display's bitmap font, rendering as empty boxes.
|
||||
|
||||
**Fix:** Added IPA detection in `parseHtml()`: when encountering `/` or `[` delimiters, the parser looks ahead for a matching close delimiter within 80 characters. If the enclosed content contains any non-ASCII byte (> 0x7F), the entire section (including delimiters) is skipped. This removes IPA transcriptions like `/ˈsɪmpəl/` and `[hɜː(ɹ)b]` while preserving legitimate ASCII bracket content like "[citation needed]" or "and/or".
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryDefinitionActivity.cpp`
|
||||
|
||||
### 3. Fix: Thinner button hints with overlap detection (DictionaryWordSelectActivity)
|
||||
|
||||
**Root cause:** Button hints used `GUI.drawButtonHints()` which draws 40px-tall buttons, taking excessive space over the book page content. No overlap detection meant hints could obscure the selected word at the bottom of the screen.
|
||||
|
||||
**Fix:**
|
||||
- Replaced `GUI.drawButtonHints()` with a custom `drawHints()` method
|
||||
- Draws thinner hints (22px instead of 40px) using `drawRect` + small text
|
||||
- Converts the selected word's bounding box from the current orientation to portrait coordinates (handles portrait, inverted, landscape CW/CCW)
|
||||
- Checks vertical and horizontal overlap between each of the 4 button hint areas and the selected word (including hyphenation continuations)
|
||||
- Individual hints that overlap the cursor are hidden (white area cleared, no button drawn)
|
||||
- Uses the theme's button positions `[58, 146, 254, 342]` to match the physical button layout
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryWordSelectActivity.h`, `src/activities/reader/DictionaryWordSelectActivity.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- The landscape coordinate conversions for overlap detection are best-guess transforms; if they're wrong for the specific device rotation mapping, they may need tuning after testing
|
||||
- The IPA skip heuristic is conservative (only skips content with non-ASCII in `/`/`[` delimiters); some edge-case IPA content outside these delimiters would still show
|
||||
- `SMALL_FONT_ID` is now included via `fontIds.h` in the word select activity
|
||||
47
chat-summaries/2026-02-12_17-52-summary.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Dictionary Feature Bug Fixes (Round 3)
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** mod/add-dictionary
|
||||
|
||||
## Task
|
||||
|
||||
Fix three issues reported after round 2 of dictionary fixes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix: Definitions truncated for some words (Dictionary.cpp)
|
||||
|
||||
**Root cause:** The `asciiCaseCmp` case-insensitive match introduced in round 2 returns the *first* case variant found in the index. In StarDict order, "Professor" (capitalized) sorts before "professor" (lowercase). If the dictionary has separate entries for each — e.g., "Professor" as a title (short definition) and "professor" as the common noun (full multi-page definition) — the shorter entry is returned.
|
||||
|
||||
**Fix:** The linear scan in `searchIndex` now remembers the first case-insensitive match as a fallback, but continues scanning adjacent entries (case variants are always adjacent in StarDict order). If an exact case-sensitive match is found, it's used immediately. Otherwise, the first case-insensitive match is used. This ensures `cleanWord("professor")` → `"professor"` finds the full lowercase entry, not the shorter capitalized one.
|
||||
|
||||
**Files:** `src/util/Dictionary.cpp`
|
||||
|
||||
### 2. Fix: Non-renderable foreign script characters in definitions (DictionaryDefinitionActivity)
|
||||
|
||||
**Root cause:** Dictionary definitions include text from other languages (Chinese, Greek, Arabic, Cyrillic, etc.) as etymological references or examples. These characters aren't in the e-ink bitmap font and render as empty boxes. This is the same class of issue as the IPA pronunciation fix from round 2, but affecting inline content within definitions.
|
||||
|
||||
**Fix:**
|
||||
- Added `isRenderableCodepoint(uint32_t cp)` static helper that whitelists character ranges the e-ink font supports:
|
||||
- U+0000–U+024F: Basic Latin through Latin Extended-B (ASCII + accented chars)
|
||||
- U+0300–U+036F: Combining Diacritical Marks
|
||||
- U+2000–U+206F: General Punctuation (dashes, quotes, bullets, ellipsis)
|
||||
- U+20A0–U+20CF: Currency Symbols
|
||||
- U+2100–U+214F: Letterlike Symbols
|
||||
- U+2190–U+21FF: Arrows
|
||||
- Replaced the byte-by-byte character append in `parseHtml()` with a UTF-8-aware decoder that reads multi-byte sequences, decodes the codepoint, and only appends renderable characters. Invalid or non-renderable characters are silently skipped.
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryDefinitionActivity.h`, `src/activities/reader/DictionaryDefinitionActivity.cpp`
|
||||
|
||||
### 3. Fix: Revert to standard-height hints, keep overlap hiding (DictionaryWordSelectActivity)
|
||||
|
||||
**What changed:** Reverted from 22px thin custom hints back to the standard 40px theme-style buttons (rounded corners with `cornerRadius=6`, `SMALL_FONT_ID` text, matching `LyraTheme::drawButtonHints` exactly). The overlap detection is preserved.
|
||||
|
||||
**Key design choice:** Instead of calling `GUI.drawButtonHints()` (which always clears all 4 button areas, erasing page content even for hidden buttons), the method draws each button individually in portrait mode. Hidden buttons are skipped entirely (`continue`), so the page content and word highlight underneath remain visible. Non-hidden buttons get the full theme treatment: white fill + rounded rect border + centered text.
|
||||
|
||||
**Files:** `src/activities/reader/DictionaryWordSelectActivity.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- The `isRenderableCodepoint` whitelist is conservative — if the font gains additional glyph coverage (e.g., Greek letters for math), the whitelist can be extended
|
||||
- Entity-decoded characters bypass the codepoint filter since they're appended as raw bytes; this is fine for the current entity set (all produce ASCII or General Punctuation characters)
|
||||
40
chat-summaries/2026-02-12_20-00-summary.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Bookmark Feature Implementation
|
||||
|
||||
## Task
|
||||
Implement bookmark functionality for the e-reader, replacing existing "Coming soon" stubs with full add/remove bookmark, visual page indicator, and bookmark navigation features.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files Created
|
||||
- **`src/util/BookmarkStore.h`** / **`src/util/BookmarkStore.cpp`** - Bookmark persistence utility. Stores bookmarks as binary data (`bookmarks.bin`) per-book in the epub cache directory on SD card. Each bookmark is a (spineIndex, pageNumber) pair (4 bytes). Provides static methods: `load`, `save`, `addBookmark`, `removeBookmark`, `hasBookmark`.
|
||||
|
||||
- **`src/activities/reader/EpubReaderBookmarkSelectionActivity.h`** / **`.cpp`** - New activity for the "Go to Bookmark" list UI, modeled on the existing chapter selection activity. Shows bookmark entries as "Chapter Title - Page N" with ButtonNavigator for scrolling. Selecting a bookmark navigates to that spine/page.
|
||||
|
||||
### Edited Files
|
||||
- **`src/activities/reader/EpubReaderMenuActivity.h`** - Added `REMOVE_BOOKMARK` to `MenuAction` enum. Changed `buildMenuItems()` to accept `isBookmarked` parameter; dynamically shows "Remove Bookmark" or "Add Bookmark" as the first menu item.
|
||||
|
||||
- **`src/activities/reader/EpubReaderActivity.cpp`** - Main integration point:
|
||||
- Added includes for `BookmarkStore.h` and `EpubReaderBookmarkSelectionActivity.h`
|
||||
- Menu creation now computes `isBookmarked` state and passes it through
|
||||
- `ADD_BOOKMARK` handler: calls `BookmarkStore::addBookmark()`, shows "Bookmark added" popup
|
||||
- `REMOVE_BOOKMARK` handler (new): calls `BookmarkStore::removeBookmark()`, shows "Bookmark removed" popup
|
||||
- `GO_TO_BOOKMARK` handler: loads bookmarks, opens `EpubReaderBookmarkSelectionActivity` if any exist, falls back to Table of Contents if no bookmarks but TOC exists, otherwise returns to reader
|
||||
- `renderContents()`: draws a small bookmark ribbon (fillPolygon, 5-point shape) in the top-right corner when the current page is bookmarked
|
||||
|
||||
## Follow-up Changes (same session)
|
||||
|
||||
### Force half refresh on menu exit
|
||||
- `onReaderMenuBack()`: sets `pagesUntilFullRefresh = 1` so the next render uses `HALF_REFRESH` to clear menu/popup ghosting artifacts from the e-ink display.
|
||||
- `ADD_BOOKMARK` / `REMOVE_BOOKMARK` handlers: also set `pagesUntilFullRefresh = 1` after their popups.
|
||||
|
||||
### Bookmark snippet (first sentence)
|
||||
- `Bookmark` struct now includes a `snippet` string field storing the first sentence from the bookmarked page.
|
||||
- `BookmarkStore` binary format upgraded to v2: version marker byte (0xFF) + count + entries with variable-length snippet. Backward-compatible: reads v1 files (no snippets) gracefully.
|
||||
- `addBookmark()` now accepts an optional `snippet` parameter (max 120 chars).
|
||||
- `EpubReaderActivity::onReaderMenuConfirm(ADD_BOOKMARK)`: extracts the first sentence from the page by iterating PageLine elements and their TextBlock words, stopping at sentence-ending punctuation (.!?:).
|
||||
- `EpubReaderBookmarkSelectionActivity::getBookmarkLabel()`: displays bookmark as "Chapter Title - First sentence here - Page N".
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device to verify bookmark ribbon sizing/positioning looks good across orientations
|
||||
- Consider caching bookmark state in memory to avoid SD reads on every page render (currently `hasBookmark` reads from SD each time in `renderContents`)
|
||||
- The bookmark selection list could potentially support deleting bookmarks directly from the list in a future iteration
|
||||
32
chat-summaries/2026-02-12_21-30-summary.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Implement Dictionary Word Lookup Feature (PR #857)
|
||||
|
||||
## Task
|
||||
|
||||
Ported upstream PR #857 (dictionary word lookup feature) into the local codebase on `mod/add-dictionary` branch. Two adaptations were made:
|
||||
|
||||
1. **Vector compatibility**: PR #857 was written against upstream `master` which used `std::list` for word storage. Our codebase already has PR #802 applied (list-to-vector). The `TextBlock.h` getters added by PR #857 were changed to return `const std::vector<...>&` instead of `const std::list<...>&`.
|
||||
|
||||
2. **Dictionary path tweak**: Changed dictionary file lookup from SD card root (`/dictionary.idx`, `/dictionary.dict`) to a `/.dictionary/` subfolder (`/.dictionary/dictionary.idx`, `/.dictionary/dictionary.dict`).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New files (10)
|
||||
|
||||
- `src/util/Dictionary.h` / `.cpp` -- StarDict 3 format dictionary lookup (sparse index + binary search)
|
||||
- `src/util/LookupHistory.h` / `.cpp` -- Per-book lookup history stored in book cache dir
|
||||
- `src/activities/reader/DictionaryDefinitionActivity.h` / `.cpp` -- Definition display with pagination
|
||||
- `src/activities/reader/DictionaryWordSelectActivity.h` / `.cpp` -- Word selection from current page with orientation-aware navigation
|
||||
- `src/activities/reader/LookedUpWordsActivity.h` / `.cpp` -- Lookup history browser with long-press delete
|
||||
|
||||
### Modified files (5)
|
||||
|
||||
- `lib/Epub/Epub/blocks/TextBlock.h` -- Added `getWords()`, `getWordXpos()`, `getWordStyles()` accessors (returning `std::vector`)
|
||||
- `lib/Epub/Epub/Page.h` -- Added `getBlock()` accessor to `PageLine`
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h` -- Added `LOOKUP`/`LOOKED_UP_WORDS` enum values, `hasDictionary` constructor param, dynamic `buildMenuItems()`
|
||||
- `src/activities/reader/EpubReaderActivity.h` -- Added includes for new activity headers
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` -- Added includes, `Dictionary::exists()` check, `LOOKUP` and `LOOKED_UP_WORDS` case handling
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Dictionary files (`dictionary.idx`, `dictionary.dict`, `dictionary.ifo`) must be placed in `/.dictionary/` folder on the SD card root
|
||||
- Menu items "Lookup" and "Lookup History" only appear when dictionary files are detected
|
||||
56
chat-summaries/2026-02-12_22-00-summary.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Dictionary Feature Polish & Menu Reorganization
|
||||
|
||||
**Date:** 2026-02-12
|
||||
|
||||
## Task Description
|
||||
|
||||
Continued polishing the dictionary word lookup feature across multiple iterations: fixing side button hint placement and orientation, adding CCW text rotation, fixing pronunciation line rendering, adding index caching, and reorganizing the reader menu.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Side button hint fixes (`DictionaryWordSelectActivity.cpp`)
|
||||
- Moved `drawSideButtonHints` call inside `drawHints()` where renderer is in portrait mode (fixes wrong placement)
|
||||
- Made all button hint labels orientation-aware (portrait, inverted, landscape CW/CCW each get correct labels matching their button-to-action mapping)
|
||||
- Replaced `GUI.drawSideButtonHints()` with custom drawing: solid background, overlap/cursor hiding, text truncation, and orientation-aware rotation
|
||||
- Changed word navigation labels from "Prev Word"/"Next Word" to "« Word"/"Word »"
|
||||
|
||||
### GfxRenderer CCW text rotation (`lib/GfxRenderer/`)
|
||||
- Added `drawTextRotated90CCW()` to `GfxRenderer.h` and `GfxRenderer.cpp`
|
||||
- Mirrors the existing CW rotation: text reads top-to-bottom instead of bottom-to-top
|
||||
- Used for side button hints in landscape CCW orientation
|
||||
|
||||
### Definition screen fixes (`DictionaryDefinitionActivity.cpp/.h`)
|
||||
- Fixed pronunciation commas: definitions start with `/ˈsɪm.pəl/, /ˈsɪmpəl/` before `<p>` — now skips all content before first `<` tag in `parseHtml()`
|
||||
- Added side button hints with proper CCW rotation and solid backgrounds
|
||||
- Updated bottom button labels: "« Back", "" (hidden stub), "« Page", "Page »"
|
||||
- Added half refresh on initial screen entry (`firstRender` flag)
|
||||
|
||||
### Dictionary index caching (`Dictionary.h/.cpp`)
|
||||
- New `loadCachedIndex()`: reads `/.dictionary/dictionary.cache` — validates magic + idx file size, loads sparse offsets directly (~7KB binary read vs 17MB scan)
|
||||
- New `saveCachedIndex()`: persists after first full scan
|
||||
- Cache format: `[magic 4B][idxFileSize 4B][totalWords 4B][count 4B][offsets N×4B]`
|
||||
- Auto-invalidates when `.idx` file size changes
|
||||
- New public methods: `cacheExists()`, `deleteCache()`
|
||||
|
||||
### Reader menu reorganization (`EpubReaderMenuActivity.h`, `EpubReaderActivity.cpp`)
|
||||
- New `MenuAction` enum values: `ADD_BOOKMARK`, `GO_TO_BOOKMARK`, `DELETE_DICT_CACHE`
|
||||
- Reordered menu: Add Bookmark, Lookup Word, Lookup Word History, Reading Orientation, Table of Contents, Go to Bookmark, Go to %, Close Book, Sync Progress, Delete Book Cache, Delete Dictionary Cache
|
||||
- Renamed: "Go to Chapter" → "Table of Contents", "Go Home" → "Close Book", "Lookup" → "Lookup Word"
|
||||
- Bookmark stubs show "Coming soon" popup
|
||||
- Delete Dictionary Cache checks existence and clears in-memory state
|
||||
|
||||
## Files Modified
|
||||
- `lib/GfxRenderer/GfxRenderer.h` — added `drawTextRotated90CCW` declaration
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` — added `drawTextRotated90CCW` implementation
|
||||
- `src/util/Dictionary.h` — added `cacheExists()`, `deleteCache()`, `loadCachedIndex()`, `saveCachedIndex()`
|
||||
- `src/util/Dictionary.cpp` — cache load/save implementation, delete cache
|
||||
- `src/activities/reader/DictionaryWordSelectActivity.cpp` — orientation-aware hints, custom side button drawing
|
||||
- `src/activities/reader/DictionaryDefinitionActivity.h` — added `firstRender` flag
|
||||
- `src/activities/reader/DictionaryDefinitionActivity.cpp` — pronunciation fix, side hints, half refresh, label changes
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h` — new enum values, reordered menu
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` — handlers for new menu actions
|
||||
|
||||
## Follow-up Items
|
||||
- Bookmark feature implementation (stubs are in place)
|
||||
- Test CCW text rotation rendering on device
|
||||
- Verify cache invalidation works when dictionary files are replaced
|
||||
28
chat-summaries/2026-02-12_23-00-summary.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Implement Letterbox Edge Row Copy for MATCHED Mode
|
||||
|
||||
## Task
|
||||
Implement the "FrameBuffer Edge Row Copy" plan for the MATCHED letterbox fill mode. Instead of computing letterbox colors from sampled edge data, the new approach copies the cover's rendered edge row directly from the frameBuffer into the letterbox area after each `drawBitmap` call.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- **Added `#include <cstring>`** for `memcpy` usage.
|
||||
- **Added `copyEdgeRowsToLetterbox()` helper** (anonymous namespace): Copies physical columns (horizontal letterbox) or physical rows (vertical letterbox) in the frameBuffer. For horizontal letterbox, iterates per-bit across 480 physical rows. For vertical letterbox, uses `memcpy` of 100-byte physical rows.
|
||||
- **Updated `renderBitmapSleepScreen()`**:
|
||||
- Added `scaledWidth`/`scaledHeight` computation matching `drawBitmap`'s floor logic.
|
||||
- Added `isMatched` flag.
|
||||
- MATCHED mode now skips edge sampling entirely (`sampleBitmapEdges` / cache load).
|
||||
- After each `drawBitmap` call (BW, LSB, MSB passes), calls `copyEdgeRowsToLetterbox` for MATCHED mode.
|
||||
- **Cleaned up dead code**:
|
||||
- Removed the entire MATCHED case from `drawLetterboxFill()` (no longer called for MATCHED).
|
||||
- Removed `grayToVal2bit` helper (was only used by the removed MATCHED case).
|
||||
- Removed `skipFillInGreyscale` flag (no longer needed — the edge copy participates in all passes naturally).
|
||||
|
||||
## Build
|
||||
Successfully compiled with `pio run` (0 errors, 0 warnings relevant to changes).
|
||||
|
||||
## Follow-up
|
||||
- Needs on-device testing to verify:
|
||||
1. The letterbox blends seamlessly with the cover edge (pixel-perfect 1:1 match).
|
||||
2. No scan coupling corruption (the scattered pixel distribution from dithering should cause less coupling than uniform blocks).
|
||||
3. If corruption is still unacceptable, the fallback is the previous flat-fill + greyscale-skip approach (revert this change).
|
||||
41
chat-summaries/2026-02-12_merge-master-into-mod-master.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Merge master into mod/master
|
||||
|
||||
**Date:** 2026-02-12
|
||||
|
||||
## Task
|
||||
|
||||
Compare upstream `master` (14 new commits) with `mod/master` (2 mod commits) since their common ancestor (`6202bfd` — release/1.0.0 merge), assess merge risk, and perform the merge.
|
||||
|
||||
## Branch Summary
|
||||
|
||||
### Upstream (`master`) — 14 commits, 47 files, ~6000 lines
|
||||
- Unified navigation handling with ButtonNavigator utility
|
||||
- Italian hyphenation support
|
||||
- Natural sort in file browser
|
||||
- Auto WiFi reconnect to last network
|
||||
- Extended Python debugging monitor
|
||||
- More power saving on idle
|
||||
- OPDS fixes (absolute URLs, prevent sleep during download)
|
||||
- Uniform debug message formatting (millis() timestamps)
|
||||
- File browser Back/Home label fix, GPIO trigger fix
|
||||
- USER_GUIDE.md updates
|
||||
|
||||
### Mod (`mod/master`) — 2 commits, 10 files, ~588 lines
|
||||
- `.gitignore` tweaks for mod fork
|
||||
- Sleep screen letterbox fill and image upscaling feature
|
||||
|
||||
## Conflict Resolution
|
||||
|
||||
Single conflict in `src/activities/boot_sleep/SleepActivity.cpp`:
|
||||
- **Upstream** changed `Serial.println` → `Serial.printf("[%lu] [SLP] ...\n", millis())` for uniform debug logging
|
||||
- **Mod** had already adopted this format in new code, but the original lines it modified were the old format
|
||||
- **Resolution:** Kept mod's `renderBitmapSleepScreen(bitmap, edgeCachePath)` call with upstream's `millis()` log format
|
||||
|
||||
## Result
|
||||
|
||||
Merge commit: `182c236`
|
||||
|
||||
## Follow-up
|
||||
|
||||
- Test sleep screen behavior end-to-end (letterbox fill + upstream idle power saving changes)
|
||||
- Verify new upstream features (navigation, WiFi auto-connect) work alongside mod changes
|
||||
23
chat-summaries/2026-02-12_mod-version-env.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Add `env:mod` with version + git hash
|
||||
|
||||
## Task
|
||||
Add a PlatformIO environment that flashes firmware with a `-mod+<git_hash>` version suffix (e.g. `1.0.0-mod+a3f7c21`).
|
||||
|
||||
## Changes
|
||||
|
||||
### New file: `scripts/inject_mod_version.py`
|
||||
- PlatformIO pre-build script
|
||||
- Reads `version` from the `[crosspoint]` section of `platformio.ini`
|
||||
- Runs `git rev-parse --short HEAD` to get the current commit hash
|
||||
- Injects `-DCROSSPOINT_VERSION="{version}-mod+{hash}"` into build flags
|
||||
|
||||
### Modified: `platformio.ini`
|
||||
- Added `[env:mod]` section (lines 58-64) that extends `base`, includes the new script via `extra_scripts`, and inherits base build flags
|
||||
|
||||
## Usage
|
||||
```
|
||||
pio run -e mod -t upload
|
||||
```
|
||||
|
||||
## Follow-up
|
||||
- None
|
||||
39
chat-summaries/2026-02-12_prerender-book-covers-summary.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Prerender Book Covers/Thumbnails on First Open
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** `mod/prerender-book-covers`
|
||||
|
||||
## Task
|
||||
|
||||
Implement todo item: "Process/render all covers/thumbs when opening book for first time" with a progress indicator so the reader doesn't appear frozen.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/components/UITheme.h`
|
||||
- Added `PRERENDER_THUMB_HEIGHTS[]` and `PRERENDER_THUMB_HEIGHTS_COUNT` constants (226, 400) representing all known theme `homeCoverHeight` values (Lyra and Base). This ensures thumbnails are pre-generated for all themes.
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Added prerender block in `onEnter()` after `setupCacheDir()` and before `addBook()`.
|
||||
- Checks whether `cover.bmp`, `cover_crop.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` exist.
|
||||
- If any are missing, shows "Preparing book..." popup with a progress bar that updates after each generation step.
|
||||
- On subsequent opens, all files already exist and the popup is skipped entirely.
|
||||
|
||||
### `src/activities/reader/XtcReaderActivity.cpp`
|
||||
- Added same prerender pattern in `onEnter()`.
|
||||
- Generates `cover.bmp`, `thumb_226.bmp`, and `thumb_400.bmp` (XTC has no cropped cover variant).
|
||||
|
||||
### `src/activities/reader/TxtReaderActivity.cpp`
|
||||
- Added prerender for `cover.bmp` only (TXT has no thumbnail support).
|
||||
- Shows "Preparing book..." popup if the cover needs generating.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Letterbox edge data not prerendered:** The sleep screen's letterbox gradient fill (`cover_edges.bin`) depends on runtime settings (crop mode, screen dimensions) and is already efficiently cached after first sleep. The expensive part (JPEG-to-BMP conversion) is what this change addresses.
|
||||
- **TXT `addBook()` cover path unchanged:** The `coverBmpPath` field in `RecentBook` is used for home screen thumbnails, not sleep covers. Since TXT has no thumbnail support, passing `""` remains correct.
|
||||
- **HomeActivity::loadRecentCovers() kept as fallback:** Books opened before this change will still have thumbnails generated lazily on the Home screen. No code removal needed.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Mark todo item as complete in `mod/docs/todo.md`
|
||||
- Test on device with each book format (EPUB, XTC/XTCH, TXT)
|
||||
- If a new theme is added with a different `homeCoverHeight`, update `PRERENDER_THUMB_HEIGHTS`
|
||||
33
chat-summaries/2026-02-13_cleanup-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Letterbox Fill: Hash-Based Block Dithering Fix & Cleanup
|
||||
|
||||
**Date:** 2026-02-13
|
||||
|
||||
## Task Description
|
||||
|
||||
Resolved an e-ink display crosstalk issue where a specific book cover ("The World in a Grain") became completely washed out and invisible when using "Dithered" letterbox fill mode. The root cause was pixel-level Bayer dithering creating a high-frequency checkerboard pattern in the BW pass for gray values in the 171-254 range (level-2/level-3 boundary), which caused display crosstalk during HALF_REFRESH.
|
||||
|
||||
## Solution
|
||||
|
||||
Hash-based block dithering with 2x2 pixel blocks for gray values in the problematic BW-boundary range (171-254). Each 2x2 block gets a uniform level (2 or 3) determined by a spatial hash, with the proportion approximating the target gray. Standard Bayer dithering is used for all other gray ranges.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Modified Files
|
||||
- **`src/activities/boot_sleep/SleepActivity.cpp`** — Added `bayerCrossesBwBoundary()` and `hashBlockDither()` functions; `drawLetterboxFill()` uses hash-based block dithering for BW-boundary gray values, standard Bayer for everything else. Removed all debug instrumentation (H1-H20 logs, frame buffer checksums, edge histograms, rewind verification).
|
||||
- **`src/CrossPointSettings.h`** — Reordered letterbox fill enum to `DITHERED=0, SOLID=1, NONE=2` with `DITHERED` as default.
|
||||
- **`src/SettingsList.h`** — Updated settings UI labels to match new enum order.
|
||||
- **`lib/GfxRenderer/GfxRenderer.h`** — Removed `getRenderMode()` getter (was only needed by debug instrumentation).
|
||||
|
||||
### Deleted Files
|
||||
- 16 debug log files from `.cursor/` directory
|
||||
|
||||
## Key Findings
|
||||
|
||||
- Single-pixel alternation (block=1) causes display crosstalk regardless of pattern regularity (Bayer or hash).
|
||||
- 2x2 minimum pixel runs are sufficient to avoid crosstalk on this e-ink display.
|
||||
- The irregular hash pattern is less visually noticeable than a regular Bayer grid at the same block size.
|
||||
- The issue only affects gray values 171-254 where Bayer produces a level-2/level-3 mix (the BW pass boundary).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None. Feature is stable and working for all tested book covers.
|
||||
49
chat-summaries/2026-02-13_letterbox-extend-edges.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Letterbox Fill: Replace Dithering with Edge Replication ("Extend Edges")
|
||||
|
||||
**Date:** 2026-02-13
|
||||
|
||||
## Task Description
|
||||
|
||||
After implementing Bayer ordered dithering for the "Blended" letterbox fill mode (replacing earlier noise dithering), the user reported that the problematic cover (*The World in a Grain* by Vince Beiser) looked even worse. Investigation revealed that **any dithering technique** is fundamentally incompatible with the e-ink multi-pass rendering pipeline for large uniform fill areas:
|
||||
|
||||
- The BW HALF_REFRESH pass creates a visible dark/white pattern in the letterbox
|
||||
- The subsequent greyscale correction can't cleanly handle mixed-level patterns in large uniform areas
|
||||
- This causes crosstalk artifacts that can affect adjacent cover rendering
|
||||
- Uniform fills (all same level, like Solid mode) work fine because all pixels go through identical correction
|
||||
|
||||
The user's key insight: the display already renders the cover's grayscale correctly (including Atkinson-dithered mixed levels). Instead of re-dithering an averaged color, replicate the cover's actual boundary pixels into the letterbox -- letting the display render them the same way it renders the cover itself.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Renamed `LETTERBOX_BLENDED` (value 2) → `LETTERBOX_EXTENDED`
|
||||
- Updated comment to reflect "None / Solid / Extend Edges"
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Changed UI label from `"Blended"` → `"Extend Edges"`
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- **Removed**: `BAYER_4X4` matrix, `quantizeBayerDither()` function (all Bayer dithering code)
|
||||
- **Added**: `getPackedPixel()`, `setPackedPixel()` helpers for packed 2-bit array access
|
||||
- **Rewrote** `LetterboxFillData` struct:
|
||||
- Added `edgeA`/`edgeB` (dynamically allocated packed 2-bit arrays) for per-pixel edge data
|
||||
- Added `edgePixelCount`, `scale` for coordinate mapping
|
||||
- Added `freeEdgeData()` cleanup method
|
||||
- **Renamed** `computeEdgeAverages()` → `computeEdgeData()`:
|
||||
- New `captureEdgePixels` parameter controls whether to allocate and fill edge arrays
|
||||
- For horizontal letterboxing: captures first/last visible BMP rows
|
||||
- For vertical letterboxing: captures leftmost/rightmost pixel per visible row
|
||||
- Still computes averages for SOLID mode in the same pass
|
||||
- **Rewrote** `drawLetterboxFill()`:
|
||||
- SOLID mode: unchanged (uniform fill with snapped level)
|
||||
- EXTENDED mode: maps screen coordinates → BMP coordinates via scale factor, looks up stored 2-bit pixel values from the cover's boundary row/column
|
||||
- **Updated** `renderBitmapSleepScreen()`: new log messages, `freeEdgeData()` call at end
|
||||
|
||||
## Backward Compatibility
|
||||
- Enum value 2 is unchanged (was `LETTERBOX_BLENDED`, now `LETTERBOX_EXTENDED`)
|
||||
- Serialized settings files continue to work without migration
|
||||
|
||||
## Follow-up Items
|
||||
- Test "Extend Edges" mode with both covers (Power Broker and World in a Grain)
|
||||
- Test vertical letterboxing (left/right) if applicable covers are available
|
||||
- Verify SOLID mode still works as expected
|
||||
42
chat-summaries/2026-02-13_letterbox-fill-redesign.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Letterbox Fill Redesign
|
||||
|
||||
**Date:** 2026-02-13
|
||||
**Task:** Strip out the 5-mode letterbox edge fill system and replace with a simplified 3-mode design
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Problem
|
||||
The existing letterbox fill feature had 5 modes (None, Solid, Blended, Gradient, Matched) with ~300 lines of complex code including per-pixel edge arrays, malloc'd buffers, binary edge caching, framebuffer-level column/row copying, and a gradient direction sub-setting. Several modes introduced visual corruption that couldn't be resolved.
|
||||
|
||||
### New Design
|
||||
Simplified to 3 modes:
|
||||
- **None** -- no fill
|
||||
- **Solid** -- computes dominant edge color, snaps to nearest of 4 e-ink levels (black/dark gray/light gray/white), fills uniformly
|
||||
- **Blended** -- computes dominant edge color, fills with exact gray value using noise dithering for smooth approximation
|
||||
|
||||
### Files Changed
|
||||
|
||||
1. **`src/CrossPointSettings.h`** -- Removed `LETTERBOX_GRADIENT`, `LETTERBOX_MATCHED` enum values; removed `SLEEP_SCREEN_GRADIENT_DIR` enum and `sleepScreenGradientDir` member; changed default to `LETTERBOX_NONE`
|
||||
|
||||
2. **`src/SettingsList.h`** -- Trimmed Letterbox Fill options to `{None, Solid, Blended}`; removed Gradient Direction setting entry
|
||||
|
||||
3. **`src/CrossPointSettings.cpp`** -- Removed `sleepScreenGradientDir` from write path; added dummy read for backward compatibility with old settings files; decremented `SETTINGS_COUNT` from 32 to 31
|
||||
|
||||
4. **`src/activities/boot_sleep/SleepActivity.cpp`** -- Major rewrite:
|
||||
- Removed: `LetterboxGradientData` struct, `loadEdgeCache()`/`saveEdgeCache()`, `sampleBitmapEdges()`, `copyEdgeRowsToLetterbox()`, old `drawLetterboxFill()`
|
||||
- Added: `LetterboxFillData` struct (2 bytes vs arrays), `snapToEinkLevel()`, `computeEdgeAverages()` (running sums only, no malloc), simplified `drawLetterboxFill()`
|
||||
- Cleaned `renderBitmapSleepScreen()`: removed matched/gradient logic, edge cache paths, unused `scaledWidth`/`scaledHeight`
|
||||
- Cleaned `renderCoverSleepScreen()`: removed edge cache path derivation
|
||||
- Removed unused includes (`<Serialization.h>`, `<cstring>`), added `<cmath>`
|
||||
|
||||
5. **`src/activities/boot_sleep/SleepActivity.h`** -- Removed `edgeCachePath` parameter from `renderBitmapSleepScreen()` signature; removed unused `<string>` include
|
||||
|
||||
### Backward Compatibility
|
||||
- Enum values 0/1/2 (None/Solid/Blended) unchanged -- existing settings preserved
|
||||
- Old Gradient (3) or Matched (4) values rejected by `readAndValidate`, falling back to default (None)
|
||||
- Old `sleepScreenGradientDir` byte consumed via dummy read during settings load
|
||||
- Orphaned `_edges.bin` cache files on SD cards are harmless
|
||||
|
||||
## Follow-up Items
|
||||
- Test all 3 fill modes on device with various cover aspect ratios
|
||||
- Consider cleaning up orphaned `_edges.bin` files (optional, low priority)
|
||||
41
chat-summaries/2026-02-13_merge-upstream-summary.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Merge upstream/master into mod/master
|
||||
|
||||
**Date:** 2026-02-13
|
||||
**Branch:** `mod/merge-upstream` (from `mod/master`)
|
||||
**Commit:** `82bfbd8`
|
||||
|
||||
## Task
|
||||
|
||||
Merged 3 new upstream/master commits into the mod fork:
|
||||
1. `7a385d7` feat: Allow screenshot retrieval from device (#820)
|
||||
2. `cb24947` feat: Add central logging pragma (#843) — replaces Serial.printf with LOG_* macros across ~50 files, adds Logging library
|
||||
3. `6e51afb` fix: Account for nbsp character as non-breaking space (#757)
|
||||
|
||||
## Conflicts Resolved
|
||||
|
||||
### src/main.cpp (1 conflict region)
|
||||
- Kept mod's `HalPowerManager` (deep sleep, power saving) + upstream's `Logging.h` and `LOG_*` macros + screenshot serial handler (`logSerial`)
|
||||
|
||||
### src/activities/boot_sleep/SleepActivity.cpp (3 conflict regions)
|
||||
- Kept mod's entire letterbox fill rework (~330 lines of dithering, edge caching, etc.)
|
||||
- Replaced upstream's reverted positioning logic (size-gated) with mod's always-compute-scale approach
|
||||
- Applied upstream's `LOG_*` pattern to all mod `Serial.printf` calls
|
||||
|
||||
## Additional Changes (beyond conflict resolution)
|
||||
|
||||
- **lib/GfxRenderer/GfxRenderer.cpp** — Fixed one `Serial.printf` that auto-merge missed converting to `LOG_ERR` (caused linker error with the new Logging library's `#define Serial` macro)
|
||||
- **lib/hal/HalPowerManager.cpp** — Converted 4 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, added `#include <Logging.h>`
|
||||
- **src/util/BookSettings.cpp** — Converted 3 `Serial.printf` calls to `LOG_DBG`/`LOG_ERR`, replaced `#include <HardwareSerial.h>` with `#include <Logging.h>`
|
||||
- **src/util/BookmarkStore.cpp** — Converted 2 `Serial.printf` calls to `LOG_ERR`/`LOG_DBG`, added `#include <Logging.h>`
|
||||
- **platformio.ini** — Added `-DENABLE_SERIAL_LOG` and `-DLOG_LEVEL=2` to `[env:mod]` build flags (was missing, other envs all have these)
|
||||
|
||||
## Build Verification
|
||||
|
||||
PlatformIO build (`pio run -e mod`) succeeded:
|
||||
- RAM: 31.0% (101724/327680 bytes)
|
||||
- Flash: 96.8% (6342796/6553600 bytes)
|
||||
|
||||
## Follow-up
|
||||
|
||||
- The merge is on branch `mod/merge-upstream` — fast-forward `mod/master` when ready
|
||||
- The `TxtReaderActivity.cpp` has a pre-existing `[[nodiscard]]` warning for `generateCoverBmp()` (not introduced by this merge)
|
||||
34
chat-summaries/2026-02-13_per-book-letterbox-fill.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Per-book Letterbox Fill Override
|
||||
|
||||
**Date:** 2026-02-13
|
||||
**Branch:** mod/fix-edge-fills
|
||||
|
||||
## Task
|
||||
|
||||
Add the ability to override the sleep cover "letterbox fill mode" on a per-book basis (EPUB only for the menu UI; all book types respected at sleep time).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New files
|
||||
- `src/util/BookSettings.h` / `src/util/BookSettings.cpp` — Lightweight per-book settings utility. Stores a `letterboxFillOverride` field (0xFF = use global default) in `{cachePath}/book_settings.bin`. Versioned binary format with field count for forward compatibility, matching the pattern used by BookmarkStore and CrossPointSettings.
|
||||
|
||||
### Modified files
|
||||
- `src/activities/reader/EpubReaderMenuActivity.h` — Added `LETTERBOX_FILL` to the `MenuAction` enum. Added `bookCachePath`, `pendingLetterboxFill`, letterbox fill labels, and helper methods (`letterboxFillToIndex`, `indexToLetterboxFill`, `saveLetterboxFill`). Constructor now accepts a `bookCachePath` parameter and loads the current per-book settings.
|
||||
- `src/activities/reader/EpubReaderMenuActivity.cpp` — Handle `LETTERBOX_FILL` action: cycles through Default/Dithered/Solid/None on Confirm (handled locally like `ROTATE_SCREEN`), saves immediately. Renders the current value on the right side of the menu item.
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` — Passes `epub->getCachePath()` to the menu activity constructor. Added `ROTATE_SCREEN` and `LETTERBOX_FILL` to the `onReaderMenuConfirm` switch as no-ops to prevent compiler warnings.
|
||||
- `src/activities/boot_sleep/SleepActivity.h` — Added `fillModeOverride` parameter to `renderBitmapSleepScreen()`.
|
||||
- `src/activities/boot_sleep/SleepActivity.cpp` — `renderCoverSleepScreen()` now loads `BookSettings` from the book's cache path after determining the book type. Passes the per-book override to `renderBitmapSleepScreen()`. `renderBitmapSleepScreen()` uses the override if valid, otherwise falls back to the global `SETTINGS.sleepScreenLetterboxFill`.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. User opens the EPUB reader menu (Confirm button while reading).
|
||||
2. "Letterbox Fill" appears between "Reading Orientation" and "Table of Contents".
|
||||
3. Pressing Confirm cycles: Default → Dithered → Solid → None → Default...
|
||||
4. The selection is persisted immediately to `book_settings.bin` in the book's cache directory.
|
||||
5. When the device enters sleep with the cover screen, the per-book override is loaded and used instead of the global setting (if set).
|
||||
6. XTC and TXT books also have their per-book override checked at sleep time, but can only be configured for EPUB via the reader menu (XTC/TXT lack general menus).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Consider adding the letterbox fill override to XTC/TXT reader menus if those get general menus in the future.
|
||||
- The `BookSettings` struct is extensible — other per-book overrides can be added by appending fields and incrementing `BOOK_SETTINGS_COUNT`.
|
||||
36
chat-summaries/2026-02-13_summary.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Revert Letterbox Fill to Dithered / Solid / None (with edge cache)
|
||||
|
||||
**Date:** 2026-02-13
|
||||
|
||||
## Task
|
||||
Reverted letterbox fill modes from None/Solid/Extend Edges back to Dithered (default)/Solid/None per user request. Restored Bayer ordered dithering for "Dithered" mode and re-introduced edge average caching to avoid recomputing on every sleep.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/CrossPointSettings.h`
|
||||
- Reordered enum: `LETTERBOX_DITHERED=0`, `LETTERBOX_SOLID=1`, `LETTERBOX_NONE=2`
|
||||
- Changed default from `LETTERBOX_NONE` to `LETTERBOX_DITHERED`
|
||||
|
||||
### `src/SettingsList.h`
|
||||
- Updated UI labels from `{"None", "Solid", "Extend Edges"}` to `{"Dithered", "Solid", "None"}`
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.h`
|
||||
- Added `#include <string>`
|
||||
- Restored `edgeCachePath` parameter to `renderBitmapSleepScreen()`
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- **Removed**: `getPackedPixel()`, `setPackedPixel()`, all EXTENDED-mode logic, `freeEdgeData()`, per-pixel edge arrays
|
||||
- **Simplified** `LetterboxFillData` to just `avgA`, `avgB`, `letterboxA`, `letterboxB`, `horizontal`, `valid`
|
||||
- **Restored** `BAYER_4X4[4][4]` matrix and `quantizeBayerDither()` function
|
||||
- **Renamed** `computeEdgeData()` → `computeEdgeAverages()` (averages-only, no edge pixel capture)
|
||||
- **Added** edge average cache: `loadEdgeCache()` / `saveEdgeCache()` (~12 byte binary file per cover)
|
||||
- **Updated** `drawLetterboxFill()`: DITHERED uses Bayer dithering, SOLID uses snap-to-level
|
||||
- **Updated** `renderBitmapSleepScreen()`: accepts `edgeCachePath`, tries cache before computing
|
||||
- **Updated** `renderCoverSleepScreen()`: derives `edgeCachePath` from cover BMP path (`_edges.bin`)
|
||||
|
||||
## Build
|
||||
Compilation succeeds (ESP32-C3 target, PlatformIO).
|
||||
|
||||
## Follow-up
|
||||
- The specific "The World in a Grain" cover still has rendering issues with dithered mode — to be investigated separately
|
||||
- Custom sleep BMPs (`/sleep/` directory, `/sleep.bmp`) intentionally skip caching since the selected BMP can change each sleep
|
||||
50
chat-summaries/2026-02-14_21-52-summary.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Placeholder Cover Generation for Books Without Covers
|
||||
|
||||
## Task
|
||||
Implement placeholder cover BMP generation for books that have no embedded cover image (or have covers in unsupported formats). Previously, these books showed empty rectangles on the home screen and fell back to the default sleep screen.
|
||||
|
||||
## Root Cause
|
||||
Cover generation failed silently in three cases:
|
||||
- **EPUB**: No `coverItemHref` in metadata, or cover is non-JPG format (PNG/SVG/GIF)
|
||||
- **TXT**: No matching image file on the SD card
|
||||
- **XTC**: First-page render failure (rare)
|
||||
|
||||
When `generateCoverBmp()` returned `false`, no BMP was created, and no fallback was attempted.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.h` - Header for the placeholder generator
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` - Implementation: allocates a 1-bit pixel buffer, renders title/author text using EpdFont glyph data (ported from GfxRenderer::renderChar), writes a 1-bit BMP file with word-wrapped centered text, border, and separator line
|
||||
|
||||
### Modified Files
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` - Added placeholder fallback after `generateCoverBmp()` and `generateThumbBmp()` fail during first-open prerender
|
||||
- `src/activities/reader/TxtReaderActivity.cpp` - Added placeholder fallback, added thumbnail generation (previously TXT had none), now passes thumb path to RECENT_BOOKS.addBook() instead of empty string
|
||||
- `src/activities/reader/XtcReaderActivity.cpp` - Added placeholder fallback after cover/thumb generation fail (rare case)
|
||||
- `src/activities/boot_sleep/SleepActivity.cpp` - Added placeholder generation before falling back to default sleep screen (handles books opened before this feature was added)
|
||||
- `lib/Txt/Txt.h` - Added `getThumbBmpPath()` and `getThumbBmpPath(int height)` methods
|
||||
- `lib/Txt/Txt.cpp` - Implemented the new thumb path methods
|
||||
|
||||
### Architecture
|
||||
- Option B approach: shared `PlaceholderCoverGenerator` utility called from reader activities
|
||||
- Generator is independent of `GfxRenderer` (no display framebuffer dependency)
|
||||
- Includes Ubuntu 10 Regular and Ubuntu 12 Bold font data directly for self-contained rendering
|
||||
- Memory: 48KB for full cover (480x800), 4-12KB for thumbnails; allocated and freed per call
|
||||
|
||||
## Flash Impact
|
||||
- Before: 96.4% flash usage (6,317,250 bytes)
|
||||
- After: 97.3% flash usage (6,374,546 bytes)
|
||||
- Delta: ~57KB (mostly from duplicate font bitmap data included in the generator)
|
||||
|
||||
## Layout Revision (traditional book cover style)
|
||||
- Border: moved inward with proportional edge padding (~10px at full size) and thickened to ~5px, keeping it visible within the device bezel
|
||||
- Title: 2x integer-scaled Ubuntu 12 Bold (effectively ~24pt) for full-size covers, positioned in the top 2/3 zone, max 5 lines
|
||||
- Author: positioned in the bottom 1/3 zone, max 3 lines with word wrapping
|
||||
- Book icon: 48x48 1-bit bitmap (generated by `scripts/generate_book_icon.py`), displayed at 2x for full covers, 1x for medium thumbnails, omitted for small thumbnails
|
||||
- Separator line between title and author zones
|
||||
- All dimensions scale proportionally for thumbnails
|
||||
|
||||
## Follow-up Items
|
||||
- Books opened before this feature was added won't get home screen thumbnails until re-opened (SleepActivity handles the sleep cover case by generating on demand)
|
||||
- Font data duplication adds ~57KB flash; could be reduced by exposing shared font references instead of including headers directly
|
||||
- Preview scripts in `scripts/` can regenerate the icon and layout previews: `generate_book_icon.py`, `preview_placeholder_cover.py`
|
||||
59
chat-summaries/2026-02-14_pr857-update-integration.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# PR #857 Full Feature Update Integration
|
||||
|
||||
**Date:** 2026-02-14
|
||||
|
||||
## Task Description
|
||||
|
||||
Implemented the full feature update from PR #857 ("feat: Add dictionary word lookup feature") into our fork, following the detailed plan in `pr_857_update_integration_190041ae.plan.md`. This covered dictionary intelligence features (stemming, edit distance, fuzzy matching), the `ActivityWithSubactivity` refactor for inline definition display, en-dash/em-dash splitting, cross-page hyphenation, reverse-chronological lookup history, and a new "Did you mean?" suggestions activity.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files (2)
|
||||
- **`src/activities/reader/DictionarySuggestionsActivity.h`** — New "Did you mean?" activity header. Adapted from PR with `orientation` parameter for our `DictionaryDefinitionActivity` constructor.
|
||||
- **`src/activities/reader/DictionarySuggestionsActivity.cpp`** — Suggestions list UI with `UITheme`-aware layout, sub-activity management for definition display.
|
||||
|
||||
### Modified Files (9)
|
||||
1. **`src/util/Dictionary.h`** — Added `getStemVariants()`, `findSimilar()` (public) and `editDistance()` (private) declarations.
|
||||
2. **`src/util/Dictionary.cpp`** — Added ~250 lines: morphological stemming (`getStemVariants`), Levenshtein distance (`editDistance`), and fuzzy index scan (`findSimilar`). Preserved fork's `/.dictionary/` paths, `stardictCmp`/`asciiCaseCmp`, `cacheExists()`/`deleteCache()`.
|
||||
3. **`src/activities/reader/DictionaryDefinitionActivity.h`** — Added optional `onDone` callback parameter and member. Enables "Done" button to exit all the way back to the reader.
|
||||
4. **`src/activities/reader/DictionaryDefinitionActivity.cpp`** — Split Confirm handler: calls `onDone()` if set, else `onBack()`. Button hint shows "Done" when callback provided. Preserved all HTML parsing, styled rendering, side button hints.
|
||||
5. **`src/activities/reader/DictionaryWordSelectActivity.h`** — Changed base class to `ActivityWithSubactivity`. Replaced `onLookup` callback with `nextPageFirstWord` string. Added `pendingBackFromDef`/`pendingExitToReader` state.
|
||||
6. **`src/activities/reader/DictionaryWordSelectActivity.cpp`** — Major update:
|
||||
- En-dash/em-dash splitting in `extractWords()` (splits on U+2013/U+2014)
|
||||
- Cross-page hyphenation in `mergeHyphenatedWords()` using `nextPageFirstWord`
|
||||
- Cascading lookup flow: exact → stem variants → similar suggestions → "Not found"
|
||||
- Sub-activity delegation in `loop()` for definition/suggestions screens
|
||||
- Preserved custom `drawHints()` with overlap detection and `PageForward`/`PageBack` support
|
||||
7. **`src/activities/reader/LookedUpWordsActivity.h`** — Replaced `onSelectWord` with `onDone` callback. Added `readerFontId`, `orientation`, `pendingBackFromDef`/`pendingExitToReader`, `getPageItems()`.
|
||||
8. **`src/activities/reader/LookedUpWordsActivity.cpp`** — Major rewrite:
|
||||
- Reverse-chronological word display
|
||||
- Inline cascading lookup flow (same as word select)
|
||||
- `UITheme`-aware layout with `GUI.drawHeader()`/`GUI.drawList()`
|
||||
- `onNextRelease`/`onPreviousRelease`/`onNextContinuous`/`onPreviousContinuous` navigation
|
||||
- Sub-activity management for definition/suggestions
|
||||
- Preserved delete confirmation mode
|
||||
9. **`src/activities/reader/EpubReaderActivity.cpp`** — Simplified LOOKUP handler (removed `onLookup` callback, added `nextPageFirstWord` extraction). Simplified LOOKED_UP_WORDS handler (removed inline lookup, passes `readerFontId` and `orientation`). Removed unused `LookupHistory.h` include.
|
||||
|
||||
### Cleanup
|
||||
- Removed unused `DictionaryDefinitionActivity.h` include from `EpubReaderActivity.h`
|
||||
- Removed unused `util/LookupHistory.h` include from `EpubReaderActivity.cpp`
|
||||
|
||||
## Architectural Summary
|
||||
|
||||
**Before:** `EpubReaderActivity` orchestrated definition display via callbacks — word select and history both called back to the reader to create definition activities.
|
||||
|
||||
**After:** `DictionaryWordSelectActivity` and `LookedUpWordsActivity` manage their own sub-activity chains (definition, suggestions) using `ActivityWithSubactivity`. This enables the cascading lookup flow: exact match → stem variants → similar suggestions → "Not found".
|
||||
|
||||
## What Was Preserved (Fork Advantages)
|
||||
- Full HTML parsing in `DictionaryDefinitionActivity`
|
||||
- Custom `drawHints()` with overlap detection in `DictionaryWordSelectActivity`
|
||||
- `PageForward`/`PageBack` button support in word selection
|
||||
- `DELETE_DICT_CACHE` menu item and `cacheExists()`/`deleteCache()`
|
||||
- `stardictCmp`/`asciiCaseCmp` for proper StarDict index comparison
|
||||
- `/.dictionary/` path prefix
|
||||
|
||||
## Follow-up Items
|
||||
- Test the full lookup flow on device (exact → stems → suggestions → not found)
|
||||
- Verify cross-page hyphenation with a book that has page-spanning hyphenated words
|
||||
- Verify en-dash/em-dash splitting with books using those characters
|
||||
- Confirm reverse-chronological history order is intuitive for users
|
||||
29
chat-summaries/2026-02-15_00-15-summary.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Placeholder Cover Visual Refinements & Home Screen Integration
|
||||
|
||||
## Task
|
||||
Refined the placeholder cover layout to match a mockup, and integrated placeholder generation into the home screen's thumbnail loading.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Layout Refinements (`PlaceholderCoverGenerator.cpp`, `preview_placeholder_cover.py`)
|
||||
- **Icon position**: Moved from above-title to side-by-side (icon left, title right)
|
||||
- **Author scale**: Increased from 1x to 2x on full-size covers for larger author text
|
||||
- **Line spacing**: Reduced to 75% of advanceY so 2-3 title lines fit within icon height
|
||||
- **Vertical centering**: Title text centers against icon when 1-2 lines; top-aligns with overflow for 3+ lines. Uses `ascender`-based visual height instead of `advanceY`-based for accurate centering
|
||||
- **Horizontal centering**: The icon+gap+text block is now centered as a unit based on actual rendered text width, not the full available text area
|
||||
|
||||
### Home Screen Integration (`HomeActivity.cpp`)
|
||||
- Added `PlaceholderCoverGenerator` fallback in `loadRecentCovers()` — when format-specific `generateThumbBmp()` fails (or for TXT which had no handler), a placeholder thumbnail is generated instead of clearing `coverBmpPath` and showing a blank rectangle
|
||||
- This covers the case where a book was previously opened, cache was cleared, and the home screen needs to regenerate thumbnails
|
||||
|
||||
## Files Changed
|
||||
- `lib/PlaceholderCover/PlaceholderCoverGenerator.cpp` — layout logic updates
|
||||
- `scripts/preview_placeholder_cover.py` — matching preview updates
|
||||
- `src/activities/home/HomeActivity.cpp` — placeholder fallback in loadRecentCovers
|
||||
|
||||
## Commit
|
||||
`632b76c` on `mod/generate-placeholder-covers`
|
||||
|
||||
## Follow-up Items
|
||||
- Test on actual device to verify C++ bitmap font rendering matches preview expectations
|
||||
- The preview script uses Helvetica (different metrics than ubuntu_12_bold), so on-device appearance will differ slightly from previews
|
||||
55
chat-summaries/2026-02-15_00-30-summary.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Flash Size Optimization: Per-Family Font and Per-Language Hyphenation Flags
|
||||
|
||||
**Date**: 2026-02-15
|
||||
|
||||
## Task Description
|
||||
|
||||
Investigated ESP32-C3 flash usage (97.3% full at 6.375 MB / 6.5 MB app partition) and implemented build flags to selectively exclude font families and hyphenation language tries. Fonts alone consumed 65.6% of flash (3.99 MB).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. `lib/EpdFont/builtinFonts/all.h`
|
||||
- Wrapped Bookerly includes (16 files) in `#ifndef OMIT_BOOKERLY`
|
||||
- Wrapped Noto Sans 12-18pt includes (16 files) in `#ifndef OMIT_NOTOSANS` (kept `notosans_8_regular.h` outside guard for UI small font)
|
||||
- Wrapped OpenDyslexic includes (16 files) in `#ifndef OMIT_OPENDYSLEXIC`
|
||||
|
||||
### 2. `src/main.cpp`
|
||||
- Wrapped Bookerly 14pt font objects (previously always included) in `#ifndef OMIT_BOOKERLY`
|
||||
- Added per-family `#ifndef OMIT_*` guards inside the existing `#ifndef OMIT_FONTS` block for other sizes
|
||||
- Added matching guards to font registration in `setupDisplayAndFonts()`
|
||||
|
||||
### 3. `src/SettingsList.h`
|
||||
- Added `FontFamilyMapping` struct and `kFontFamilyMappings[]` compile-time table with per-family `#ifndef` guards
|
||||
- Switched Font Family setting from `SettingInfo::Enum` to `SettingInfo::DynamicEnum` with getter/setter that maps between list indices and fixed enum values (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2)
|
||||
- Added `static_assert` ensuring at least one font family is available
|
||||
|
||||
### 4. `src/activities/settings/SettingsActivity.cpp`
|
||||
- Added DynamicEnum toggle support in `toggleCurrentSetting()` (cycles through options via `valueGetter`/`valueSetter`)
|
||||
- Added DynamicEnum display support in `render()` display lambda
|
||||
|
||||
### 5. `src/CrossPointSettings.cpp`
|
||||
- Guarded `getReaderFontId()` switch cases with `#ifndef OMIT_*`, added `default:` fallback to first available font
|
||||
- Guarded `getReaderLineCompression()` switch cases with `#ifndef OMIT_*`, added `default:` fallback
|
||||
- Added `#error` directive if all font families are omitted
|
||||
|
||||
### 6. `lib/Epub/Epub/hyphenation/LanguageRegistry.cpp`
|
||||
- Added per-language `#ifndef OMIT_HYPH_xx` guards around includes, `LanguageHyphenator` objects, and entries
|
||||
- Switched from `std::array<LanguageEntry, 6>` to `std::vector<LanguageEntry>` for variable entry count
|
||||
- Languages: DE (201 KB), EN (27 KB), ES (13 KB), FR (7 KB), IT (2 KB), RU (33 KB)
|
||||
|
||||
### 7. `platformio.ini`
|
||||
- Added to `[env:mod]`: `-DOMIT_OPENDYSLEXIC`, `-DOMIT_HYPH_DE`, `-DOMIT_HYPH_EN`, `-DOMIT_HYPH_ES`, `-DOMIT_HYPH_FR`, `-DOMIT_HYPH_IT`, `-DOMIT_HYPH_RU`
|
||||
|
||||
## Design Decisions
|
||||
- Enum values stay fixed (BOOKERLY=0, NOTOSANS=1, OPENDYSLEXIC=2) for settings file compatibility
|
||||
- Existing `OMIT_FONTS` flag left untouched; per-family flags nest inside it
|
||||
- DynamicEnum used for Font Family to handle index-to-value mapping when fonts are removed from the middle of the options list
|
||||
|
||||
## Estimated Savings (mod build)
|
||||
- OpenDyslexic fonts: ~1,052 KB
|
||||
- All hyphenation tries: ~282 KB
|
||||
- **Total: ~1,334 KB (~1.30 MB)** -- from 97.3% down to ~76.9%
|
||||
|
||||
## Follow-up Items
|
||||
- Build and verify the `mod` environment compiles cleanly and flash size reduction matches estimates
|
||||
- Other available flags for future use: `OMIT_BOOKERLY`, `OMIT_NOTOSANS` (individual language OMIT_HYPH_xx flags can also be used selectively)
|
||||
30
chat-summaries/2026-02-15_11-39-summary.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Table Rendering Fixes: Entities and Colspan Support
|
||||
|
||||
## Task Description
|
||||
Fix two issues with the newly implemented EPUB table rendering:
|
||||
1. Stray ` ` entities appearing as literal text in table cells instead of whitespace
|
||||
2. Cells with `colspan` attributes (e.g., section headers like "Anders Celsius", "Scientific career") rendering as narrow single-column cells instead of spanning the full table width
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `flushPartWordBuffer()`
|
||||
- Added detection and replacement of literal ` ` strings in the word buffer before flushing to `ParsedText`
|
||||
- This handles double-encoded `&nbsp;` entities common in Wikipedia and other generated EPUBs, where XML parsing converts `&` to `&` leaving literal ` ` in the character data
|
||||
|
||||
### 2. `lib/Epub/Epub/TableData.h` — `TableCell` struct
|
||||
- Added `int colspan = 1` field to store the HTML `colspan` attribute value
|
||||
|
||||
### 3. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `startElement()`
|
||||
- Added parsing of the `colspan` attribute from `<td>` and `<th>` tags
|
||||
- Stores the parsed value (minimum 1) in the `TableCell::colspan` field
|
||||
|
||||
### 4. `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` — `processTable()`
|
||||
- **Column count**: Changed from `max(row.cells.size())` to sum of `cell.colspan` per row, correctly determining logical column count
|
||||
- **Natural width measurement**: Only non-spanning cells (colspan=1) contribute to per-column width calculations; spanning cells use combined width
|
||||
- **Layout**: Added `spanContentWidth()` and `spanFullCellWidth()` lambdas to compute the combined content width and full cell width for cells spanning multiple columns
|
||||
- **Cell mapping**: Each `PageTableCellData` now maps to an actual cell (not a logical column), with correct x-offset and combined column width for spanning cells
|
||||
- **Fill logic**: Empty cells are appended only for unused logical columns after all actual cells are placed
|
||||
|
||||
## Follow-up Items
|
||||
- Rowspan support is not yet implemented (uncommon in typical EPUB content)
|
||||
- The ` ` fix only handles the most common double-encoded entity; other double-encoded entities (e.g., `&mdash;`) could be handled similarly if needed
|
||||
49
chat-summaries/2026-02-15_13-00-summary.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Table Rendering Tweaks: Centering, Line Breaks, Padding
|
||||
|
||||
## Task Description
|
||||
Three visual tweaks to the EPUB table rendering based on comparison with a Wikipedia article (Anders Celsius):
|
||||
1. Full-width spanning rows (like "Anders Celsius", "Scientific career", "Signature") should be center-aligned
|
||||
2. `<br>` tags and block elements within table cells should create actual line breaks (e.g., "(aged 42)" then "Uppsala, Sweden" on the next line)
|
||||
3. Cell padding was too tight — text too close to grid borders
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Forced Line Breaks in Table Cells
|
||||
|
||||
**`lib/Epub/Epub/ParsedText.h`**:
|
||||
- Added `std::vector<bool> forceBreakAfter` member to track mandatory line break positions
|
||||
- Added `void addLineBreak()` public method
|
||||
|
||||
**`lib/Epub/Epub/ParsedText.cpp`**:
|
||||
- `addWord()`: grows `forceBreakAfter` vector alongside other word vectors
|
||||
- `addLineBreak()`: sets `forceBreakAfter.back() = true` on the last word
|
||||
- `computeLineBreaks()` (DP algorithm): respects forced breaks — cannot extend a line past a forced break point, and forced breaks override continuation groups
|
||||
- `computeHyphenatedLineBreaks()` (greedy): same — stops line at forced break, won't backtrack past one
|
||||
- `hyphenateWordAtIndex()`: when splitting a word, transfers the forced break flag to the remainder (last part)
|
||||
|
||||
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `startNewTextBlock()`:
|
||||
- When `inTable`, instead of being a no-op, now: flushes the word buffer, calls `addLineBreak()` on the current ParsedText, and resets `nextWordContinues`
|
||||
- This means `<br>`, `<p>`, `<div>` etc. within table cells now produce real visual line breaks
|
||||
|
||||
### 2. Center-Aligned Full-Width Spanning Cells
|
||||
|
||||
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`** — `processTable()`:
|
||||
- Before laying out a cell's content, checks if `cell.colspan >= numCols` (spans full table width)
|
||||
- If so, sets the cell's BlockStyle alignment to `CssTextAlign::Center`
|
||||
- This correctly centers section headers and title rows in Wikipedia infobox-style tables
|
||||
|
||||
### 3. Increased Cell Padding
|
||||
|
||||
**`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**:
|
||||
- `TABLE_CELL_PAD_X`: 2 → 4 pixels (horizontal padding)
|
||||
- Added `TABLE_CELL_PAD_Y = 2` pixels (vertical padding)
|
||||
- Row height now includes `2 * TABLE_CELL_PAD_Y` for top/bottom padding
|
||||
|
||||
**`lib/Epub/Epub/Page.cpp`**:
|
||||
- `TABLE_CELL_PADDING_X`: 2 → 4 pixels (matches parser constant)
|
||||
- Added `TABLE_CELL_PADDING_Y = 2` pixels
|
||||
- Cell text Y position now accounts for vertical padding: `baseY + 1 + TABLE_CELL_PADDING_Y`
|
||||
|
||||
## Follow-up Items
|
||||
- The padding constants are duplicated between `ChapterHtmlSlimParser.cpp` and `Page.cpp` — could be unified into a shared header
|
||||
- Vertical centering within cells (when a cell has fewer lines than the tallest cell) is not implemented
|
||||
32
chat-summaries/2026-02-15_14-30-summary.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Table Width Hints: HTML Attributes + CSS Width Support
|
||||
|
||||
## Task Description
|
||||
Add support for author-specified column widths from HTML `width` attributes and CSS `width` property on `<table>`, `<col>`, `<td>`, and `<th>` elements, using them as hints for column sizing in `processTable()`.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. CSS Layer (`lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`)
|
||||
- Added `width` bit to `CssPropertyFlags`
|
||||
- Added `CssLength width` field to `CssStyle`
|
||||
- Added `hasWidth()` convenience method
|
||||
- Updated `applyOver()`, `reset()`, `clearAll()`, `anySet()` to include `width`
|
||||
- Added `else if (propName == "width")` case to `CssParser::parseDeclarations()` using `interpretLength()`
|
||||
|
||||
### 2. Table Data (`lib/Epub/Epub/TableData.h`)
|
||||
- Added `CssLength widthHint` and `bool hasWidthHint` to `TableCell`
|
||||
- Added `std::vector<CssLength> colWidthHints` to `TableData` (from `<col>` tags)
|
||||
- Added `#include "css/CssStyle.h"` for `CssLength`
|
||||
|
||||
### 3. Parser (`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`)
|
||||
- Removed `"col"` from `TABLE_SKIP_TAGS` (was being skipped entirely)
|
||||
- Added `parseHtmlWidthAttr()` helper: parses HTML `width="200"` (pixels) and `width="50%"` (percent) into `CssLength`
|
||||
- Added `<col>` handling in `startElement`: parses `width` HTML attribute and `style` CSS, pushes to `tableData->colWidthHints`
|
||||
- Updated `<td>`/`<th>` handling: now parses `width` HTML attribute, `style` attribute, and stylesheet CSS width (via `resolveStyle`). CSS takes priority over HTML attribute. Stored in `TableCell::widthHint`
|
||||
|
||||
### 4. Layout (`processTable()`)
|
||||
- Added step 3a: resolves width hints per column. Priority: `<col>` hints > max cell hint (colspan=1 only). Percentages resolve relative to available content width.
|
||||
- Modified step 3b: hinted columns get their resolved pixel width (clamped to min col width). If all hinted widths exceed available space, they're scaled down proportionally. Unhinted columns use the existing two-pass fair-share algorithm on the remaining space.
|
||||
|
||||
## Follow-up Items
|
||||
- The `<colgroup>` tag is still skipped entirely; `<col>` tags within `<colgroup>` won't be reached. Could un-skip `<colgroup>` (make it transparent like `<thead>`/`<tbody>`) if needed.
|
||||
- `rowspan` is not yet supported.
|
||||
41
chat-summaries/2026-02-15_17-21-summary.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Cherry-pick Image Support from pablohc/crosspoint-reader@2d8cbcf (PR #556)
|
||||
|
||||
## Task
|
||||
Merge EPUB embedded image support (JPEG/PNG) from pablohc's fork into the mod branch, based on upstream PR #556.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files (11)
|
||||
- `lib/Epub/Epub/blocks/ImageBlock.h` / `.cpp` - Image block type for page layout
|
||||
- `lib/Epub/Epub/converters/DitherUtils.h` - 4x4 Bayer dithering for 4-level grayscale
|
||||
- `lib/Epub/Epub/converters/ImageDecoderFactory.h` / `.cpp` - Format-based decoder selection
|
||||
- `lib/Epub/Epub/converters/ImageToFramebufferDecoder.h` / `.cpp` - Base decoder interface
|
||||
- `lib/Epub/Epub/converters/JpegToFramebufferConverter.h` / `.cpp` - JPEG decoder (picojpeg)
|
||||
- `lib/Epub/Epub/converters/PngToFramebufferConverter.h` / `.cpp` - PNG decoder (PNGdec)
|
||||
- `lib/Epub/Epub/converters/PixelCache.h` - 2-bit pixel cache for fast re-render
|
||||
- `scripts/generate_test_epub.py` - Test EPUB generator
|
||||
|
||||
### Modified Files (13)
|
||||
- `lib/Epub/Epub/blocks/Block.h` - Removed unused `layout()` virtual
|
||||
- `lib/Epub/Epub/blocks/TextBlock.h` - Removed unused `layout()` override
|
||||
- `lib/Epub/Epub/Page.h` / `.cpp` - Added `PageImage` class, `TAG_PageImage=3`, `hasImages()`, `getImageBoundingBox()`
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` / `.cpp` - Image extraction/decoding from EPUB, new constructor params
|
||||
- `lib/Epub/Epub/Section.cpp` - Derive content base/image paths, bumped version 12→13
|
||||
- `lib/GfxRenderer/GfxRenderer.h` / `.cpp` - Added `getRenderMode()`, implemented `displayWindow()`
|
||||
- `lib/hal/HalDisplay.h` / `.cpp` - Added `displayWindow()` for partial refresh
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` - Image-aware refresh with double FAST_REFRESH optimization
|
||||
- `platformio.ini` - Added `PNGdec` dependency, `PNG_MAX_BUFFERED_PIXELS=6402` build flag
|
||||
|
||||
## Key Conflict Resolutions
|
||||
- `TAG_PageImage = 3` (not 2) to avoid collision with mod's `TAG_PageTableRow = 2`
|
||||
- Preserved mod's bookmark ribbon rendering in `renderContents`
|
||||
- Preserved mod's table rendering (`PageTableRow`) alongside new `PageImage`
|
||||
- Section file version bumped to invalidate cached sections
|
||||
|
||||
## Build Result
|
||||
- `mod` environment: SUCCESS (RAM 31.0%, Flash 77.5%)
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device with JPEG/PNG EPUBs
|
||||
- Run `scripts/generate_test_epub.py` to create test EPUBs
|
||||
- Consider whether `displayWindow()` experimental path should be enabled
|
||||
52
chat-summaries/2026-02-15_17-30-summary.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# EPUB Table Rendering Implementation
|
||||
|
||||
## Task
|
||||
Replace the `[Table omitted]` placeholder in the EPUB reader with full column-aligned table rendering, including grid lines, proportional column widths, and proper serialization.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New file
|
||||
- **`lib/Epub/Epub/TableData.h`** -- Lightweight structs (`TableCell`, `TableRow`, `TableData`) for buffering table content during SAX parsing.
|
||||
|
||||
### Modified files
|
||||
|
||||
- **`lib/Epub/Epub/ParsedText.h` / `.cpp`**
|
||||
- Added `getNaturalWidth()` public method to measure the single-line content width of a ParsedText. Used by column width calculation.
|
||||
|
||||
- **`lib/Epub/Epub/Page.h` / `.cpp`**
|
||||
- Added `TAG_PageTableRow = 2` to `PageElementTag` enum.
|
||||
- Added `getTag()` pure virtual method to `PageElement` base class for tag-based serialization.
|
||||
- Added `PageTableCellData` struct (cell lines, column width, x-offset).
|
||||
- Added `PageTableRow` class with render (grid lines + cell text), serialize, and deserialize support.
|
||||
- Updated `Page::serialize()` to use `el->getTag()` instead of hardcoded tag.
|
||||
- Updated `Page::deserialize()` to handle `TAG_PageTableRow`.
|
||||
|
||||
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h`**
|
||||
- Added `#include "../TableData.h"`.
|
||||
- Added table state fields: `bool inTable`, `std::unique_ptr<TableData> tableData`.
|
||||
- Added `processTable()` and `addTableRowToPage()` method declarations.
|
||||
|
||||
- **`lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`**
|
||||
- Added table-related tag arrays (`TABLE_TRANSPARENT_TAGS`, `TABLE_SKIP_TAGS`).
|
||||
- Replaced `[Table omitted]` placeholder with full table buffering logic in `startElement`.
|
||||
- Modified `startNewTextBlock` to be a no-op when inside a table (cell content stays in one ParsedText).
|
||||
- Added table close handling in `endElement` for `</td>`, `</th>`, and `</table>`.
|
||||
- Disabled the 750-word early split when inside a table.
|
||||
- Implemented `processTable()`: column width calculation (natural + proportional distribution), per-cell layout via `layoutAndExtractLines`, `PageTableRow` creation.
|
||||
- Implemented `addTableRowToPage()`: page-break handling for table rows.
|
||||
|
||||
## Design Decisions
|
||||
- Tables are buffered entirely during parsing, then processed on `</table>` close (two-pass: measure then layout).
|
||||
- Column widths are proportional to natural content width, with equal distribution of extra space when content fits.
|
||||
- Grid lines (1px) drawn around every cell; 2px horizontal cell padding.
|
||||
- Nested tables are skipped (v1 limitation).
|
||||
- `<caption>`, `<colgroup>`, `<col>` are skipped; `<thead>`, `<tbody>`, `<tfoot>` are transparent.
|
||||
- `<th>` cells get bold text. Cell text is left-aligned with no paragraph indent.
|
||||
- Serialization is backward-compatible: old firmware encountering the new tag will re-parse the section.
|
||||
|
||||
## Follow-up Items
|
||||
- Nested table support (currently skipped)
|
||||
- `colspan` / `rowspan` support
|
||||
- `<caption>` rendering as centered text above the table
|
||||
- CSS border detection (currently always draws grid lines)
|
||||
- Consider CSS-based cell alignment
|
||||
39
chat-summaries/2026-02-15_20-30-summary.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Adjust Low Power Mode: Fix Processing Bug and Sync with PR #852
|
||||
|
||||
**Date:** 2026-02-15
|
||||
**Branch:** mod/adjust-low-power-mode
|
||||
|
||||
## Task
|
||||
|
||||
Fix a bug where the device enters low-power mode (10MHz CPU) during first-time book opening and chapter indexing, causing significant slowdown. Also sync minor logging differences with upstream PR #852.
|
||||
|
||||
## Root Causes (three issues combined)
|
||||
|
||||
1. **Missing delegation in ActivityWithSubactivity**: `main.cpp` calls `preventAutoSleep()` on `ReaderActivity` (the top-level activity). `ReaderActivity` creates `EpubReaderActivity` as a subactivity, but `ActivityWithSubactivity` never delegated `preventAutoSleep()` or `skipLoopDelay()` to the active subactivity.
|
||||
|
||||
2. **Stale check across activity transitions**: The `preventAutoSleep()` check at the top of the main loop runs before `loop()`. When an activity transitions mid-loop (HomeActivity -> ReaderActivity), the pre-loop check is stale but the post-loop power-saving decision fires.
|
||||
|
||||
3. **Section object vs section file**: `!section` alone was insufficient as a condition. The Section object is created early in the `!section` block, making `section` non-null, but `createSectionFile()` (the slow operation) runs afterward. A separate `loadingSection` flag is needed to cover the full duration.
|
||||
|
||||
## Changes Made
|
||||
|
||||
1. **ActivityWithSubactivity** (`src/activities/ActivityWithSubactivity.h`)
|
||||
- Added `preventAutoSleep()` override that delegates to `subActivity->preventAutoSleep()`
|
||||
- Added `skipLoopDelay()` override with same delegation pattern
|
||||
|
||||
2. **main.cpp** (`src/main.cpp`)
|
||||
- Added a second `preventAutoSleep()` re-check after `currentActivity->loop()` returns, before the power-saving block
|
||||
|
||||
3. **EpubReaderActivity** (`src/activities/reader/EpubReaderActivity.h`, `.cpp`)
|
||||
- Added `volatile bool loadingSection` flag
|
||||
- `preventAutoSleep()` returns `!section || loadingSection`
|
||||
- `!section` covers the pre-Section-object period (including cover prerendering in onEnter)
|
||||
- `loadingSection` covers the full `!section` block in `renderScreen()` where `createSectionFile()` runs
|
||||
- Flag is also cleared on the error path
|
||||
|
||||
4. **TxtReaderActivity** (`src/activities/reader/TxtReaderActivity.h`)
|
||||
- `preventAutoSleep()` returns `!initialized`
|
||||
- Covers cover prerendering and page index building
|
||||
|
||||
5. **HalPowerManager.cpp** (`lib/hal/HalPowerManager.cpp`)
|
||||
- Synced log messages with upstream PR: `LOG_ERR` -> `LOG_DBG` with frequency values (matching commit `ff89fb1`)
|
||||
28
chat-summaries/2026-02-15_cover-thumbnail-fix.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Fix: Cover/Thumbnail Pipeline on Home Screen
|
||||
|
||||
**Date:** 2026-02-15
|
||||
|
||||
## Task Description
|
||||
|
||||
Multiple issues with book cover thumbnails on the home screen:
|
||||
1. After clearing a book's cache, the home screen showed a placeholder instead of the real cover.
|
||||
2. Books without covers showed blank rectangles instead of generated placeholder covers.
|
||||
|
||||
## Root Cause
|
||||
|
||||
`Epub::generateThumbBmp()` wrote an empty 0-byte BMP file as a "don't retry" sentinel when a book had no cover. This empty file:
|
||||
- Blocked the placeholder fallback in `EpubReaderActivity::onEnter()` (file exists check passes)
|
||||
- Tricked the home screen into thinking a valid thumbnail exists (skips regeneration)
|
||||
- Failed to parse in `LyraTheme::drawRecentBookCover()` resulting in a blank gray rectangle
|
||||
|
||||
## Changes Made
|
||||
|
||||
- **`lib/Epub/Epub.cpp`**: Removed the empty sentinel file write from `generateThumbBmp()`. Now it simply returns `false` when there's no cover, letting callers generate valid placeholder BMPs that serve the same "don't retry" purpose.
|
||||
- **`src/activities/home/HomeActivity.cpp`**: Changed placeholder fallback in `loadRecentCovers()` from `if (!success && !Storage.exists(coverPath))` to `if (!success)` as defense-in-depth for edge cases like global cache clear.
|
||||
- **`src/RecentBooksStore.h`**: Added `removeBook(const std::string& path)` method declaration.
|
||||
- **`src/RecentBooksStore.cpp`**: Implemented `removeBook()` — finds and erases the book by path, then persists the updated list.
|
||||
- **`src/activities/reader/EpubReaderActivity.cpp`**: After clearing cache in the `DELETE_CACHE` handler, calls `RECENT_BOOKS.removeBook(epub->getPath())` so the book is cleanly removed from recents when its cache is wiped.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None.
|
||||
42
chat-summaries/2026-02-15_merge-master-css-perf.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Merge upstream master (CSS perf #779) into mod/master-img
|
||||
|
||||
**Date**: 2026-02-15
|
||||
|
||||
## Task
|
||||
|
||||
Merge the latest changes from `master` (upstream) into `mod/master-img`.
|
||||
|
||||
## Changes Merged
|
||||
|
||||
One upstream commit: `46c2109 perf: Improve large CSS files handling (#779)`
|
||||
|
||||
This commit significantly refactored the CSS subsystem:
|
||||
- Streaming CSS parser with `StackBuffer` for zero-heap parsing
|
||||
- Extracted `parseDeclarationIntoStyle()` from inline logic
|
||||
- Rule limits and selector validation
|
||||
- `CssParser` now owns its `cachePath` and manages caching internally
|
||||
- CSS loading skipped when "Book's Embedded Style" is off
|
||||
|
||||
## Conflicts Resolved
|
||||
|
||||
### Section.cpp (2 regions)
|
||||
- Combined mod's image support variables (`contentBase`, `imageBasePath`) with master's new CSS parser loading pattern (`cssParser->loadFromCache()`)
|
||||
- Merged constructor call: kept mod's `epub`, `contentBase`, `imageBasePath` params while adopting master's `cssParser` local variable pattern
|
||||
- Added master's `cssParser->clear()` calls on error/success paths
|
||||
|
||||
### CssParser.cpp (1 region)
|
||||
- Accepted master's complete rewrite of the CSS parser
|
||||
- Ported mod's `width` CSS property handler into the new `parseDeclarationIntoStyle()` function
|
||||
|
||||
## Auto-merged Files Verified
|
||||
- `CssStyle.h`: `width` property and supporting code preserved
|
||||
- `platformio.ini`: `PNGdec` library and `PNG_MAX_BUFFERED_PIXELS` preserved; `[env:mod]` section intact; master's `gnu++2a` and `build_unflags` applied
|
||||
- `ChapterHtmlSlimParser.h`: image/table support members preserved
|
||||
- `RecentBooksStore.cpp`: `removeBook()` method preserved
|
||||
- `Epub.cpp`, `Epub.h`, `CssParser.h`, `ReaderActivity.cpp`: auto-merged cleanly
|
||||
|
||||
## Build Result
|
||||
- `pio run -e mod` succeeded with zero errors
|
||||
|
||||
## Commit
|
||||
- `744d616 Merge branch 'master' into mod/master-img`
|
||||
51
chat-summaries/2026-02-16_05-30-summary.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Port PR #838 and PR #907 into fork
|
||||
|
||||
## Task
|
||||
Cherry-pick / manually port two upstream PRs into the fork:
|
||||
- **PR #907**: Cover image outlines to improve legibility
|
||||
- **PR #838**: Fallback logic for epub cover extraction
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #907 (cover outlines) — `src/components/themes/lyra/LyraTheme.cpp`
|
||||
- Always draw a rectangle outline around cover tiles before drawing the bitmap on top
|
||||
- Removed `hasCover` flag — simplified logic so outline is always present, preventing low-contrast covers from blending into the background
|
||||
|
||||
### PR #838 (cover fallback logic) — 3 files
|
||||
|
||||
#### `lib/Epub/Epub.h`
|
||||
- Added declarations for 4 new methods: `generateInvalidFormatCoverBmp`, `generateInvalidFormatThumbBmp`, `isValidThumbnailBmp` (static), `getCoverCandidates` (private)
|
||||
- Added doc comments on existing `generateCoverBmp` and `generateThumbBmp`
|
||||
|
||||
#### `lib/Epub/Epub.cpp`
|
||||
- Added `#include <HalDisplay.h>` and `#include <algorithm>`
|
||||
- **`generateCoverBmp`**: Added invalid BMP detection/retry, cover fallback candidate probing (`getCoverCandidates`), case-insensitive extension checking. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
|
||||
- **`generateThumbBmp`**: Same changes as `generateCoverBmp` — invalid BMP detection, fallback probing, case-insensitive check. Returns `false` on failure (no internal X-pattern fallback) so callers control the fallback chain
|
||||
- **`generateInvalidFormatThumbBmp`** (new): Creates 1-bit BMP with X pattern as thumbnail marker
|
||||
- **`generateInvalidFormatCoverBmp`** (new): Creates 1-bit BMP with X pattern as cover marker, using display dimensions
|
||||
- **`isValidThumbnailBmp`** (new, static): Validates BMP by checking file size > 0 and 'BM' header
|
||||
- **`getCoverCandidates`** (new, private): Returns list of common cover filenames to probe
|
||||
|
||||
#### `src/activities/home/HomeActivity.cpp`
|
||||
- Replaced `Storage.exists()` check with `Epub::isValidThumbnailBmp()` to catch corrupt/empty thumbnails
|
||||
- Added epub.load retry logic (cache-only first, then full build)
|
||||
- After successful thumbnail generation, update `RECENT_BOOKS` with the new thumb path
|
||||
- Thumbnail fallback chain: Real Cover → PlaceholderCoverGenerator → X-Pattern marker (epub only; xtc/other formats fall back to placeholder only)
|
||||
|
||||
## Adaptations from upstream PR
|
||||
- All `Serial.printf` calls converted to `LOG_DBG`/`LOG_ERR` macros (fork convention)
|
||||
- No `#include <HardwareSerial.h>` (not needed with Logging.h)
|
||||
- Retained fork's `bookMetadataCache` null-check guards
|
||||
- HomeActivity changes manually adapted to fork's `loadRecentCovers()` structure (upstream has inline code in a different method)
|
||||
- Fork's `PlaceholderCoverGenerator` is the preferred fallback; X-pattern marker is last resort only
|
||||
|
||||
#### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
- EPUB sleep screen cover now follows Real Cover → Placeholder → X-Pattern fallback chain
|
||||
- Upgraded `Storage.exists()` check to `Epub::isValidThumbnailBmp()` for the epub cover path
|
||||
|
||||
#### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Cover and thumbnail pre-rendering now follows Real Cover → Placeholder → X-Pattern fallback chain
|
||||
- Upgraded all `Storage.exists()` checks to `Epub::isValidThumbnailBmp()` for cover/thumb paths
|
||||
|
||||
## Follow-up Items
|
||||
- Build and test on device to verify cover generation pipeline works end-to-end
|
||||
29
chat-summaries/2026-02-16_15-18-summary.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Created CrossPoint Reader Development Skill
|
||||
|
||||
**Date**: 2026-02-16
|
||||
**Task**: Create a Cursor agent skill for CrossPoint Reader firmware development guidance.
|
||||
|
||||
## Changes Made
|
||||
|
||||
Created project-level skill at `.cursor/skills/crosspoint-reader-dev/` with 6 files:
|
||||
|
||||
| File | Lines | Purpose |
|
||||
|------|-------|---------|
|
||||
| `SKILL.md` | 202 | Core rules: agent identity, hardware constraints, resource protocol, architecture overview, HAL usage, coding standards, error handling, activity lifecycle, UI rules |
|
||||
| `architecture.md` | 138 | Build system (PlatformIO CLI + VS Code), build flags, environments, generated files, local config, platform detection |
|
||||
| `coding-patterns.md` | 135 | FreeRTOS tasks, malloc patterns, global font loading, button mapping, UI rendering rules |
|
||||
| `debugging-and-testing.md` | 148 | Build commands, serial monitoring, crash debugging (OOM, stack overflow, use-after-free, watchdog), testing checklist, CI/CD pipeline |
|
||||
| `git-workflow.md` | 98 | Repository detection, branch naming, commit messages, when to commit |
|
||||
| `cache-management.md` | 100 | SD card cache structure, invalidation rules, file format versioning |
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Progressive disclosure**: SKILL.md kept to 202 lines (well under 500 limit) with always-needed info; detailed references in separate files one level deep
|
||||
- **Project-level storage**: `.cursor/skills/` so it's shared with anyone using the repo
|
||||
- **Description** includes broad trigger terms: ESP32-C3, PlatformIO, EPUB, e-ink, HAL, activity lifecycle, FreeRTOS, SD card, embedded C++
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Consider adding the skill directory to `.gitignore` if this should remain personal, or commit if sharing with collaborators
|
||||
- Update cache file format version numbers if they've changed since the guide was written
|
||||
- Skill will auto-activate when the agent detects firmware/embedded development context
|
||||
57
chat-summaries/2026-02-16_19-30-summary.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Reader Menu Improvements
|
||||
|
||||
## Task
|
||||
Overhaul the EPUB reader menu: consolidate dictionary actions behind long-press, add portrait/landscape toggle with preferred-orientation settings, add font size cycling, and rename several menu labels.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Consolidated Dictionary Menu Items
|
||||
- Removed "Lookup Word History" and "Delete Dictionary Cache" from the reader menu
|
||||
- Long-pressing Confirm on "Lookup Word" now opens the Lookup History screen
|
||||
- "Delete Dictionary Cache" moved to a sentinel entry at the bottom of the Lookup History word list
|
||||
|
||||
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `LookedUpWordsActivity.h`, `LookedUpWordsActivity.cpp`, `EpubReaderActivity.cpp`
|
||||
|
||||
### 2. Toggle Portrait/Landscape
|
||||
- Renamed "Reading Orientation" to "Toggle Portrait/Landscape" in the reader menu (kept the original name in global Settings)
|
||||
- Short-press now toggles between preferred portrait and preferred landscape orientations
|
||||
- Long-press opens a popup sub-menu with all 4 orientation options
|
||||
- Added `preferredPortrait` and `preferredLandscape` settings to `CrossPointSettings` (serialized at end for backward-compat)
|
||||
- Added corresponding settings to `SettingsList.h` using `DynamicEnum` (maps non-sequential enum values correctly)
|
||||
|
||||
**Files:** `CrossPointSettings.h`, `CrossPointSettings.cpp`, `SettingsList.h`, `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`
|
||||
|
||||
### 3. Toggle Font Size
|
||||
- Added new `TOGGLE_FONT_SIZE` menu action
|
||||
- Cycles through Small → Medium → Large → Extra Large → Small on each press
|
||||
- Shows current size value next to the label (like orientation)
|
||||
- Applied on menu exit via extended `onBack` callback `(uint8_t orientation, uint8_t fontSize)`
|
||||
- Added `applyFontSize()` to `EpubReaderActivity` (saves to settings, resets section for re-layout)
|
||||
|
||||
**Files:** `EpubReaderMenuActivity.h`, `EpubReaderMenuActivity.cpp`, `EpubReaderActivity.h`, `EpubReaderActivity.cpp`
|
||||
|
||||
### 4. Label Renames
|
||||
- "Letterbox Fill" → "Override Letterbox Fill" (reader menu only; global setting keeps original name)
|
||||
- "Sync Progress" → "Sync Reading Progress"
|
||||
|
||||
**Files:** `english.yaml` (I18n source), regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
|
||||
|
||||
### 5. Long-Press Safety
|
||||
- Added `ignoreNextConfirmRelease` flag to `EpubReaderMenuActivity` to prevent stale releases
|
||||
- Added `initialSkipRelease` constructor parameter to `LookedUpWordsActivity`
|
||||
- Extended `ignoreNextConfirmRelease` guard to cover the word-selection path (not just delete-confirm mode)
|
||||
- Orientation sub-menu also uses `ignoreNextConfirmRelease` to avoid selecting on long-press release
|
||||
|
||||
### 6. New I18n Strings
|
||||
- `STR_TOGGLE_ORIENTATION`: "Toggle Portrait/Landscape"
|
||||
- `STR_TOGGLE_FONT_SIZE`: "Toggle Font Size"
|
||||
- `STR_OVERRIDE_LETTERBOX_FILL`: "Override Letterbox Fill"
|
||||
- `STR_PREFERRED_PORTRAIT`: "Preferred Portrait"
|
||||
- `STR_PREFERRED_LANDSCAPE`: "Preferred Landscape"
|
||||
|
||||
## Build Status
|
||||
Successfully compiled (default environment). RAM: 31.1%, Flash: 99.4%.
|
||||
|
||||
## Follow-up Items
|
||||
- Translations: New strings fall back to English for all non-English languages. Translators can add entries to their respective YAML files.
|
||||
- The `SettingInfo::Enum` approach doesn't work for non-sequential enum values (portrait=0, inverted=2). Used `DynamicEnum` with getter/setter lambdas instead.
|
||||
31
chat-summaries/2026-02-16_21-30-summary.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Merge Assessment & Cherry-pick: master -> mod/master
|
||||
|
||||
## Task
|
||||
Assess and merge new commits from `master` into `mod/master`.
|
||||
|
||||
## Analysis
|
||||
- 19 commits on `master` not on `mod/master`, but 16 were already cherry-picked or manually ported (different hashes, same content)
|
||||
- A full `git merge master` produced 30+ conflicts due to duplicate cherry-picks with different patch IDs
|
||||
- Identified 3 genuinely new commits
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Cherry-pick `97c3314` (#932) - `f21720d`
|
||||
- perf: Skip constructing unnecessary `std::string` in TextBlock.cpp
|
||||
- 1-line change, applied cleanly
|
||||
|
||||
### 2. Cherry-pick `2a32d8a` (#926) - `424e332`
|
||||
- chore: Improve Russian language support
|
||||
- Renamed `russia.yaml` -> `russian.yaml`, updated `i18n.md`, fixed translation strings
|
||||
- Applied cleanly
|
||||
|
||||
### 3. Cherry-pick `0bc6747` (#827) - `61fb11c`
|
||||
- feat: Add PNG cover image support for EPUB books
|
||||
- Added `PngToBmpConverter` library (new files, 858 lines)
|
||||
- Resolved 2 conflicts:
|
||||
- `Epub.cpp`: Discarded incoming JPG/PNG block (used old variable names), added PNG thumbnail support to mod's existing structure using `effectiveCoverImageHref` with case-insensitive checks. Fixed `generateCoverBmp()` PNG block to also use `effectiveCoverImageHref`. Added `.png` to `getCoverCandidates()`.
|
||||
- `ImageToFramebufferDecoder.cpp`: Took upstream `LOG_ERR` version over mod's `Serial.printf`
|
||||
|
||||
## Follow-up Items
|
||||
- Build and test PNG cover rendering on device
|
||||
- `mod/master` is now fully caught up with `master`
|
||||
66
chat-summaries/2026-02-16_22-00-summary.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Improve Home Screen
|
||||
|
||||
## Task Description
|
||||
Enhance the Lyra theme home screen with six improvements: empty-state placeholder, adaptive recent book cards, 2-line title wrapping with author, adjusted button positioning, optional clock display, and a manual Set Time activity.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### i18n Strings (8 YAML files + auto-generated C++ files)
|
||||
- Added `STR_CHOOSE_SOMETHING`, `STR_HOME_SCREEN_CLOCK`, `STR_CLOCK_AMPM`, `STR_CLOCK_24H`, `STR_SET_TIME` to all 8 translation YAML files with localized text
|
||||
- Regenerated `lib/I18n/I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp` via `gen_i18n.py`
|
||||
|
||||
### Settings: Clock Format (`CrossPointSettings.h`, `.cpp`, `SettingsList.h`)
|
||||
- Added `CLOCK_FORMAT` enum (`CLOCK_OFF`, `CLOCK_AMPM`, `CLOCK_24H`) and `homeScreenClock` member (default OFF)
|
||||
- Added persistence in `writeSettings()` and `loadFromFile()` (appended at end for backward compatibility)
|
||||
- Added "Home Screen Clock" setting under Display category in `SettingsList.h`
|
||||
|
||||
### Lyra Theme: Empty State Placeholder (`LyraTheme.cpp`)
|
||||
- When `recentBooks` is empty, draws centered "Choose something to read" text instead of blank area
|
||||
|
||||
### Lyra Theme: 1-Book Horizontal Layout (`LyraTheme.cpp`)
|
||||
- When 1 recent book: cover on the left (natural aspect ratio), title + author on the right
|
||||
- Title uses `UI_12_FONT_ID` with generous wrapping (up to 5 lines, no truncation unless very long)
|
||||
- Author in `UI_10_FONT_ID` below title with 4px gap
|
||||
|
||||
### Lyra Theme: Multi-Book Tile Layout (`LyraTheme.cpp`)
|
||||
- 2-3 books: tile-based layout with cover centered within tile (no stretching)
|
||||
- Cover bitmap rendering preserves aspect ratio: crops if wider than slot, centers if narrower
|
||||
- Title wraps to 2 lines with ellipsis, author in `SMALL_FONT_ID` below
|
||||
|
||||
### Lyra Theme: Selection Background Fix (`LyraTheme.cpp`)
|
||||
- Bottom section of selection highlight now uses full remaining height below cover
|
||||
- Prevents author text from clipping outside the selection area
|
||||
|
||||
### Lyra Theme: Shared Helpers (`LyraTheme.cpp`)
|
||||
- Extracted `wrapText` lambda for reusable word-wrap logic (parameterized by font, maxLines, maxWidth)
|
||||
- Extracted `renderCoverBitmap` lambda for aspect-ratio-preserving cover rendering
|
||||
|
||||
### Lyra Metrics (`LyraTheme.h`)
|
||||
- Increased `homeCoverTileHeight` from 287 to 310 to accommodate expanded text area
|
||||
- Menu buttons shift down automatically since they're positioned relative to this metric
|
||||
|
||||
### Home Screen Clock (`HomeActivity.cpp`)
|
||||
- Added clock rendering in the header area (top-left) after `drawHeader()`
|
||||
- Respects `homeScreenClock` setting (OFF / AM/PM / 24H)
|
||||
- Skips rendering if system time is unset (year <= 2000)
|
||||
|
||||
### Set Time Activity (NEW: `SetTimeActivity.h`, `SetTimeActivity.cpp`)
|
||||
- New sub-activity for manual time entry: displays HH:MM with field selection
|
||||
- Left/Right switches between hours and minutes, Up/Down adjusts values
|
||||
- Confirm saves via `settimeofday()`, Back discards
|
||||
- Wired into Settings > Display as an action item
|
||||
|
||||
### Settings Activity Wiring (`SettingsActivity.h`, `SettingsActivity.cpp`)
|
||||
- Added `SetTime` to `SettingAction` enum
|
||||
- Added include and switch case for `SetTimeActivity`
|
||||
- Added "Set Time" action to display settings category
|
||||
|
||||
## Build Verification
|
||||
- `pio run -e default` succeeded
|
||||
- RAM: 31.1% (101,772 / 327,680 bytes)
|
||||
- Flash: 99.5%
|
||||
|
||||
## Follow-up Items
|
||||
- Test on hardware: verify clock display, card layout with 0/1/2/3 books, Set Time activity
|
||||
- Fine-tune `homeCoverTileHeight` value if text area feels too tight or loose visually
|
||||
- Consider NTP auto-sync when WiFi is available (currently only during KOReader sync)
|
||||
63
chat-summaries/2026-02-16_upstream-sync-summary.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Upstream Sync: `upstream/master` into `mod/master`
|
||||
|
||||
**Date:** 2026-02-16
|
||||
**Task:** Synchronize recent upstream changes into the mod fork while preserving all mod-specific features.
|
||||
|
||||
## Strategy
|
||||
|
||||
- **Cherry-pick** approach (not full merge) for granular control
|
||||
- 13 upstream commits cherry-picked across 3 phases; 4 skipped (mod already had enhanced implementations)
|
||||
- Working branch `mod/sync-upstream` used, then fast-forwarded into `mod/master`
|
||||
|
||||
## Phases & Commits
|
||||
|
||||
### Phase 1 — Low-risk (6 commits)
|
||||
| PR | Description | Notes |
|
||||
|----|-------------|-------|
|
||||
| #689 | Hyphenation optimization | Naming conflict resolved (mod's `OMIT_HYPH_*` guards preserved) |
|
||||
| #832 | Settings size auto-calc | Mod's `sleepScreenLetterboxFill` added to new `SettingsWriter` |
|
||||
| #840 | clang-format-fix shebang | Clean |
|
||||
| #917 | SCOPE.md dictionary docs | Clean |
|
||||
| #856 | Multiple author display | Clean |
|
||||
| #858 | Miniz compilation warning | Clean |
|
||||
|
||||
### Phase 2 — Major refactors (3 commits)
|
||||
| PR | Description | Notes |
|
||||
|----|-------------|-------|
|
||||
| #774 | Activity render() refactor | ~15 conflicts. Mod's 5 custom activities (Bookmarks, Dictionary) adapted to new `render(RenderLock&&)` pattern. Deadlock in `EpubReaderActivity.cpp` identified and fixed (redundant mutex around `enterNewActivity()`). |
|
||||
| #916 | RAII RenderLock | Clean cherry-pick + audit of mod code for manual mutex usage |
|
||||
| #728 | I18n system | ~15-20 conflicts. 16 new `StrId` keys added for mod strings. `DynamicEnum` font handling preserved. `SettingsList.h` and `SettingsActivity.cpp` adapted. |
|
||||
|
||||
### Phase 3 — Post-I18n fixes (4 commits)
|
||||
| PR | Description | Notes |
|
||||
|----|-------------|-------|
|
||||
| #884 | Empty button icon fix | Clean |
|
||||
| #906 | Webserver docs update | Clean |
|
||||
| #792 | Translators doc | Clean |
|
||||
| #796 | Battery icon alignment | Clean |
|
||||
|
||||
### Mod adaptation commits (3)
|
||||
- `mod: adapt mod activities to #774 render() pattern` — Systematic refactor of 5 activity pairs
|
||||
- `mod: convert remaining manual render locks to RAII RenderLock` — Audit & cleanup
|
||||
- `mod: remove duplicate I18n.h include in HomeActivity.cpp` — Cleanup
|
||||
|
||||
## Skipped Upstream Commits (4)
|
||||
- #676, #700 (image/cover improvements) — Mod already has enhanced pipeline
|
||||
- #668 (JPEG support) — Already in mod
|
||||
- #780 (cover fallback) — Already cherry-picked into mod
|
||||
|
||||
## Files Changed
|
||||
111 files changed, ~23,700 insertions, ~17,700 deletions across hyphenation tries, I18n system, activity refactors, and documentation.
|
||||
|
||||
## Key Decisions
|
||||
- **Image pipeline:** Kept mod's versions entirely; upstream's cover fixes were already ported
|
||||
- **Deadlock fix:** Removed redundant `xSemaphoreTake`/`xSemaphoreGive` around `enterNewActivity()` in `EpubReaderActivity.cpp` — the new `RenderLock` inside `enterNewActivity()` would deadlock with the outer manual lock
|
||||
- **I18n integration:** Added mod-specific `StrId` keys rather than keeping hardcoded strings
|
||||
|
||||
## Verification
|
||||
- All mod features (bookmarks, dictionary, letterbox fill, placeholder covers, table rendering, embedded images) verified for code-path integrity post-sync
|
||||
- No broken references or missing dependencies found
|
||||
|
||||
## Follow-up Items
|
||||
- Full PlatformIO build on hardware to confirm compilation
|
||||
- Runtime testing of all mod features on device
|
||||
38
chat-summaries/2026-02-17_02-33-summary.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Clock Persistence, Size Setting, and Timezone Support
|
||||
|
||||
## Task Description
|
||||
Implemented the plan from `clock_settings_and_timezone_fd0bf03f.plan.md` covering three features:
|
||||
1. Fix homeScreenClock setting not persisting across reboots
|
||||
2. Add clock size setting (Small/Medium/Large)
|
||||
3. Add timezone selection with North American presets and custom UTC offset
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Fix Persistence Bug
|
||||
- **`src/CrossPointSettings.cpp`**: Removed stale legacy `sleepScreenGradientDir` read (lines 270-271) from `loadFromFile()` that was causing a one-byte deserialization offset, corrupting `preferredPortrait`, `preferredLandscape`, and `homeScreenClock`.
|
||||
|
||||
### 2. Clock Size Setting
|
||||
- **`src/CrossPointSettings.h`**: Added `CLOCK_SIZE` enum (SMALL/MEDIUM/LARGE) and `uint8_t clockSize` field.
|
||||
- **`src/CrossPointSettings.cpp`**: Added `clockSize` to `writeSettings()` and `loadFromFile()` serialization.
|
||||
- **`src/SettingsList.h`**: Added clock size enum setting in Display category.
|
||||
- **`lib/I18n/I18nKeys.h`**: Added `STR_CLOCK_SIZE`, `STR_CLOCK_SIZE_SMALL`, `STR_CLOCK_SIZE_MEDIUM`, `STR_CLOCK_SIZE_LARGE`.
|
||||
- **All 8 YAML translation files**: Added clock size strings.
|
||||
- **`src/components/themes/BaseTheme.cpp`** and **`src/components/themes/lyra/LyraTheme.cpp`**: Updated `drawHeader()` to select font (SMALL_FONT_ID / UI_10_FONT_ID / UI_12_FONT_ID) based on `clockSize` setting.
|
||||
|
||||
### 3. Timezone Support
|
||||
- **`src/CrossPointSettings.h`**: Added `TIMEZONE` enum (UTC, Eastern, Central, Mountain, Pacific, Alaska, Hawaii, Custom), `uint8_t timezone` and `int8_t timezoneOffsetHours` fields, and `getTimezonePosixStr()` declaration.
|
||||
- **`src/CrossPointSettings.cpp`**: Added timezone/offset serialization, validation (-12 to +14), and `getTimezonePosixStr()` implementation returning POSIX TZ strings (including DST rules for NA timezones).
|
||||
- **`lib/I18n/I18nKeys.h`** + **all YAML files**: Added timezone strings and "Set UTC Offset" label.
|
||||
- **`src/SettingsList.h`**: Added timezone enum setting in Display category.
|
||||
- **`src/activities/settings/SetTimezoneOffsetActivity.h/.cpp`** (new files): UTC offset picker activity (-12 to +14), using same UI pattern as `SetTimeActivity`.
|
||||
- **`src/activities/settings/SettingsActivity.h`**: Added `SetTimezoneOffset` to `SettingAction` enum.
|
||||
- **`src/activities/settings/SettingsActivity.cpp`**: Added include, action entry, handler for SetTimezoneOffset, and `setenv`/`tzset` call after every settings save.
|
||||
- **`src/main.cpp`**: Apply saved timezone via `setenv`/`tzset` on boot after `SETTINGS.loadFromFile()`.
|
||||
- **`src/util/TimeSync.cpp`**: Apply timezone before starting NTP sync so time displays correctly.
|
||||
|
||||
## Build Status
|
||||
Firmware builds successfully (99.5% flash usage).
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify clock persistence, size changes, timezone selection, and custom UTC offset picker.
|
||||
- Timezone strings use English fallbacks for non-English languages.
|
||||
33
chat-summaries/2026-02-17_03-04-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Move Clock Settings to Own Category
|
||||
|
||||
## Task Description
|
||||
Moved all clock-related settings into a dedicated "Clock" tab in the settings screen, renamed "Home Screen Clock" to "Clock" (since it's now shown globally), and made "Set UTC Offset" conditionally visible only when timezone is set to "Custom".
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Rename "Home Screen Clock" -> "Clock"
|
||||
- **`lib/I18n/I18nKeys.h`**: Renamed `STR_HOME_SCREEN_CLOCK` to `STR_CLOCK`.
|
||||
- **All 8 YAML translation files**: Updated key name and shortened labels (e.g., "Home Screen Clock" -> "Clock", "Hodiny na domovske obrazovce" -> "Hodiny").
|
||||
- **`src/CrossPointSettings.h`**: Renamed field `homeScreenClock` to `clockFormat`.
|
||||
- **`src/CrossPointSettings.cpp`**: Updated all references (`writeSettings`, `loadFromFile`).
|
||||
- **`src/SettingsList.h`**: Updated setting entry and JSON API key.
|
||||
- **`src/main.cpp`**, **`src/components/themes/BaseTheme.cpp`**, **`src/components/themes/lyra/LyraTheme.cpp`**: Updated all `SETTINGS.homeScreenClock` references to `SETTINGS.clockFormat`.
|
||||
|
||||
### 2. New "Clock" settings category
|
||||
- **`lib/I18n/I18nKeys.h`**: Added `STR_CAT_CLOCK`.
|
||||
- **All 8 YAML translation files**: Added localized "Clock" category label.
|
||||
- **`src/SettingsList.h`**: Changed category of Clock, Clock Size, and Timezone from `STR_CAT_DISPLAY` to `STR_CAT_CLOCK`.
|
||||
|
||||
### 3. Wire up in SettingsActivity
|
||||
- **`src/activities/settings/SettingsActivity.h`**: Added `clockSettings` vector, `rebuildClockActions()` helper, bumped `categoryCount` to 5.
|
||||
- **`src/activities/settings/SettingsActivity.cpp`**:
|
||||
- Added `STR_CAT_CLOCK` to `categoryNames` (index 1, between Display and Reader).
|
||||
- Added routing for `STR_CAT_CLOCK` in the category-sorting loop.
|
||||
- Implemented `rebuildClockActions()`: always adds "Set Time", conditionally adds "Set UTC Offset" only when `timezone == TZ_CUSTOM`. Called on `onEnter()` and after every `toggleCurrentSetting()`.
|
||||
- Updated category switch indices (Display=0, Clock=1, Reader=2, Controls=3, System=4).
|
||||
|
||||
## Build Status
|
||||
Firmware builds successfully.
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify the new Clock tab, setting visibility toggle, and tab bar layout with 5 tabs.
|
||||
50
chat-summaries/2026-02-17_clock-fix-ntp-sleep-summary.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Clock Bug Fix, NTP Auto-Sync, and Sleep Persistence
|
||||
|
||||
**Date**: 2026-02-17
|
||||
|
||||
## Task Description
|
||||
|
||||
Three clock-related improvements:
|
||||
1. Fix SetTimeActivity immediately dismissing when opened
|
||||
2. Add automatic NTP time sync on WiFi connection
|
||||
3. Verify time persistence across deep sleep modes
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Bug Fix: SetTimeActivity immediate dismiss (`src/activities/settings/SetTimeActivity.cpp`)
|
||||
- **Root cause**: `loop()` used `wasReleased()` for Back, Confirm, Left, and Right buttons. The parent `SettingsActivity` enters this subactivity on a `wasPressed()` event, so the button release from the original press immediately triggered the exit path.
|
||||
- **Fix**: Changed all four `wasReleased()` calls to `wasPressed()`, matching the pattern used by all other subactivities (e.g., `LanguageSelectActivity`).
|
||||
|
||||
### 2. Shared NTP Utility (`src/util/TimeSync.h`, `src/util/TimeSync.cpp`)
|
||||
- Created `TimeSync` namespace with three functions:
|
||||
- `startNtpSync()` -- non-blocking: configures and starts SNTP service
|
||||
- `waitForNtpSync(int timeoutMs = 5000)` -- blocking: starts SNTP and polls until sync completes or timeout
|
||||
- `stopNtpSync()` -- stops the SNTP service
|
||||
|
||||
### 3. NTP on WiFi Connection (`src/activities/network/WifiSelectionActivity.cpp`)
|
||||
- Added `#include "util/TimeSync.h"` and call to `TimeSync::startNtpSync()` in `checkConnectionStatus()` right after `WL_CONNECTED` is detected. This is non-blocking so it doesn't delay the UI flow.
|
||||
|
||||
### 4. KOReaderSync refactor (`src/activities/reader/KOReaderSyncActivity.cpp`)
|
||||
- Removed the local `syncTimeWithNTP()` anonymous namespace function and `#include <esp_sntp.h>`
|
||||
- Replaced both call sites with `TimeSync::waitForNtpSync()` (blocking, since KOReader needs accurate time for API requests)
|
||||
- Added `#include "util/TimeSync.h"`
|
||||
|
||||
### 5. Boot time debug log (`src/main.cpp`)
|
||||
- Added `#include <ctime>` and a debug log in `setup()` that prints the current RTC time on boot (or "not set" if epoch). This helps verify time persistence across deep sleep during testing.
|
||||
|
||||
## Sleep Persistence Notes
|
||||
|
||||
- ESP32-C3's default ESP-IDF config uses both RTC and high-resolution timers for timekeeping
|
||||
- The RTC timer continues during deep sleep, so `time()` / `gettimeofday()` return correct wall-clock time after wake (with drift from the internal ~150kHz RC oscillator)
|
||||
- Time does NOT survive a full power-on reset (RTC timer resets)
|
||||
- NTP auto-sync on WiFi connection handles drift correction
|
||||
|
||||
## Build Verification
|
||||
|
||||
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Test on hardware: set time manually, sleep, wake, verify time in serial log
|
||||
- Test NTP: connect to WiFi from Settings, verify time updates automatically
|
||||
- Consider adding `TimeSync::stopNtpSync()` call when WiFi is disconnected (currently SNTP just stops getting responses, which is harmless)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Clock UI: Symmetry, Auto-Update, and System-Wide Header Clock
|
||||
|
||||
**Date**: 2026-02-17
|
||||
|
||||
## Task Description
|
||||
|
||||
Three improvements to clock display:
|
||||
1. Make clock positioning symmetric with battery icon in headers
|
||||
2. Auto-update the clock without requiring a button press
|
||||
3. Show the clock everywhere the battery appears in system UI headers
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Moved clock rendering into `drawHeader` (BaseTheme + LyraTheme)
|
||||
|
||||
Previously the clock was drawn only in `HomeActivity::render()` with ad-hoc positioning (`contentSidePadding`, `topPadding`). Now it's rendered inside `drawHeader()` in both theme implementations, using the same positioning pattern as the battery:
|
||||
|
||||
- **Battery** (right): icon at `rect.x + rect.width - 12 - batteryWidth`, text at `rect.y + 5`
|
||||
- **Clock** (left): text at `rect.x + 12`, `rect.y + 5`
|
||||
|
||||
Both use `SMALL_FONT_ID`, so the font matches. The 12px margin from the edge is now symmetric on both sides.
|
||||
|
||||
**Files changed:**
|
||||
- `src/components/themes/BaseTheme.cpp` -- added clock block in `drawHeader()`, added `#include <ctime>` and `#include "CrossPointSettings.h"`
|
||||
- `src/components/themes/lyra/LyraTheme.cpp` -- same changes for Lyra theme's `drawHeader()`
|
||||
|
||||
### 2. Clock now appears on all header screens automatically
|
||||
|
||||
Since `drawHeader()` is called by: HomeActivity, MyLibraryActivity, RecentBooksActivity, SettingsActivity, LookedUpWordsActivity, DictionarySuggestionsActivity -- the clock now appears on all of these screens when enabled. No per-activity code needed.
|
||||
|
||||
### 3. Removed standalone clock code from HomeActivity
|
||||
|
||||
- `src/activities/home/HomeActivity.cpp` -- removed the 15-line clock rendering block that was separate from `drawHeader()`
|
||||
|
||||
### 4. Added auto-update (once per minute) on home screen
|
||||
|
||||
- `src/activities/home/HomeActivity.h` -- added `lastRenderedMinute` field to track the currently displayed minute
|
||||
- `src/activities/home/HomeActivity.cpp` -- added minute-change detection in `loop()` that calls `requestUpdate()` when the minute rolls over, triggering a screen refresh
|
||||
|
||||
## Build Verification
|
||||
|
||||
- `pio run -e mod` -- SUCCESS (RAM: 31.0%, Flash: 78.8%)
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- The auto-update only applies to HomeActivity. Other screens (Settings, Library, etc.) will show the current time when they render but won't auto-refresh purely for clock updates, which is appropriate for e-ink.
|
||||
- The DictionarySuggestionsActivity and LookedUpWordsActivity pass a non-zero `contentX` offset in their header rect, so the clock position adjusts correctly via `rect.x + 12`.
|
||||
24
chat-summaries/2026-02-17_home-highlight-fixes.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Home Screen Book Card Highlight Fixes
|
||||
|
||||
## Task
|
||||
Fix visual issues with the selection highlight on home screen recent book cards:
|
||||
1. Bottom padding too tight against author text descenders
|
||||
2. Rounded corners not uniform across all four corners
|
||||
|
||||
## Changes Made
|
||||
|
||||
### LyraTheme.h
|
||||
- Increased `homeCoverTileHeight` from 310 to 318 to provide bottom padding below author text
|
||||
|
||||
### LyraTheme.cpp
|
||||
- Reduced `cornerRadius` from 6 to 4 to prevent radius clamping in the 8px-tall top strip (which was clamping `min(6, w/2, 4) = 4` while the bottom section used the full 6)
|
||||
|
||||
### GfxRenderer.cpp
|
||||
- No net changes. Two approaches were tried and reverted:
|
||||
1. **Dither origin** (global shift): misaligned the highlight's dot pattern with surrounding UI
|
||||
2. **Arc-relative dither** (per-corner relative coords): created visible seam artifacts at 3 of 4 corners where the arc's relative dither met the adjacent rectangle's absolute dither with inverted parity
|
||||
|
||||
## Key Decisions
|
||||
- **Bottom padding**: Originally tried shrinking the highlight (subtracting extra padding from bottom section height), but this cut off the author text. Correct approach: increase the tile height so the highlight naturally has more room.
|
||||
- **Corner radius**: Reducing from 6 to 4 ensures all corners (both the thin top strip and taller bottom section) use the same `maxRadius` through `fillRoundedRect`'s clamping formula.
|
||||
- **Dither approaches (both reverted)**: Two dither fixes were tried: (1) global dither origin shift misaligned with surrounding UI, (2) arc-relative dither created visible seam artifacts at corners where parity mismatched. The absolute dither pattern in `fillArc` is the least-bad option — the minor L/R asymmetry at radius 4 is much less noticeable than seam artifacts.
|
||||
37
chat-summaries/2026-02-17_long-press-confirm-toc.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Long-Press Confirm to Open Table of Contents
|
||||
|
||||
**Date**: 2026-02-17
|
||||
**Branch**: mod/improve-home-screen
|
||||
|
||||
## Task
|
||||
|
||||
Add long-press detection on the Confirm button while reading an EPUB to directly open the Table of Contents (chapter selection), bypassing the reader menu. Short press retains existing behavior (opens menu).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.h`
|
||||
- Added `bool ignoreNextConfirmRelease` member to suppress short-press after a long-press Confirm
|
||||
- Added `void openChapterSelection()` private method declaration
|
||||
|
||||
### `src/activities/reader/EpubReaderActivity.cpp`
|
||||
- Added `constexpr unsigned long longPressConfirmMs = 700` threshold constant
|
||||
- Extracted `openChapterSelection()` helper method from the duplicated `EpubReaderChapterSelectionActivity` construction code
|
||||
- Added long-press Confirm detection in `loop()` (before the existing short-press check): opens TOC directly if `epub->getTocItemsCount() > 0`
|
||||
- Refactored `onReaderMenuConfirm(SELECT_CHAPTER)` to use the new helper (was ~35 lines of inline construction)
|
||||
- Refactored `onReaderMenuConfirm(GO_TO_BOOKMARK)` fallback (no bookmarks + TOC available) to use the same helper
|
||||
- Reset `ignoreNextConfirmRelease` when `skipNextButtonCheck` clears, to avoid stale state across subactivity transitions
|
||||
|
||||
### `src/activities/reader/EpubReaderChapterSelectionActivity.h`
|
||||
- Added `bool ignoreNextConfirmRelease` member
|
||||
- Added `initialSkipRelease` constructor parameter (default `false`) to consume stale Confirm release when opened via long-press
|
||||
|
||||
### `src/activities/reader/EpubReaderChapterSelectionActivity.cpp`
|
||||
- Added guard in `loop()` to skip the first Confirm release when `ignoreNextConfirmRelease` is true
|
||||
|
||||
## Pattern Used
|
||||
|
||||
Follows the existing Back button short/long-press pattern: `isPressed() + getHeldTime() >= threshold` for long press, `wasReleased()` for short press, with `ignoreNextConfirmRelease` flag (same pattern as `EpubReaderMenuActivity`, `LookedUpWordsActivity`, and other activities).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None identified
|
||||
17
chat-summaries/2026-02-17_summary.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Cherry-pick upstream PR #939 — dangling pointer fix
|
||||
|
||||
## Task
|
||||
Cherry-pick commit `b47e1f6` from upstream PR [#939](https://github.com/crosspoint-reader/crosspoint-reader/pull/939) into `mod/master`.
|
||||
|
||||
## Changes
|
||||
- **File**: `src/activities/home/MyLibraryActivity.cpp` (lines 199-200)
|
||||
- **Fix**: Changed `folderName` from `auto` (deduced as `const char*` pointing to a temporary) to `std::string`, and called `.c_str()` at the point of use instead. This eliminates a dangling pointer caused by `.c_str()` on a temporary `std::string` from `basepath.substr(...)`.
|
||||
|
||||
## Method
|
||||
- Fetched PR ref via `git fetch upstream pull/939/head:pr-939`
|
||||
- Cherry-picked `b47e1f6` — applied cleanly with no conflicts
|
||||
- Build verified: SUCCESS (PlatformIO, 68s)
|
||||
- Cleaned up temporary `pr-939` branch ref
|
||||
|
||||
## Follow-up
|
||||
- None required. The commit preserves original authorship (Uri Tauber).
|
||||
42
chat-summaries/2026-02-18_fix-indexing-display-summary.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Fix Indexing Display Issues
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Branch:** `mod/merge-upstream-pr-979`
|
||||
|
||||
## Task
|
||||
|
||||
Fixed three issues with the indexing display implementation that was added as part of merging PR #979 (silent pre-indexing for next chapter):
|
||||
|
||||
1. **Restore original popup for direct chapter jumps** — The conditional logic that showed a full-screen "Indexing..." message when a status bar display mode was selected was removed. Direct chapter jumps now always show the original small popup overlay, regardless of the Indexing Display setting.
|
||||
|
||||
2. **Clear status bar indicator after silent indexing** — Added `preIndexedNextSpine` tracking member to prevent the `silentIndexingActive` flag from being re-set on re-renders after indexing completes. Changed `silentIndexNextChapterIfNeeded` to return `bool` and added `requestUpdate()` call to trigger a clean re-render that clears the indicator.
|
||||
|
||||
3. **Handle single-page chapters** — Updated the pre-indexing condition to trigger on the sole page of a 1-page chapter (not just the penultimate page of multi-page chapters).
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `src/activities/reader/EpubReaderActivity.h` — Added `preIndexedNextSpine` member, changed `silentIndexNextChapterIfNeeded` return type to `bool`
|
||||
- `src/activities/reader/EpubReaderActivity.cpp` — All three fixes applied
|
||||
|
||||
## Build
|
||||
|
||||
PlatformIO build succeeded (RAM: 31.1%, Flash: 99.6%).
|
||||
|
||||
## Follow-up fix: False "Indexing" indicator + image flash on e-ink
|
||||
|
||||
Two related issues: (1) paging backwards to a penultimate page of an already-indexed chapter showed "Indexing" in the status bar, and (2) the `requestUpdate()` that cleared the indicator caused images to flash on e-ink.
|
||||
|
||||
Root cause: `silentIndexingActive` was set optimistically based on page position alone, before checking whether the next chapter's cache actually exists. The subsequent `requestUpdate()` to clear the indicator triggered a full re-render causing image artifacts.
|
||||
|
||||
Fix — replaced optimistic flag with a pre-check:
|
||||
- Before rendering, probes the next chapter's section file via `Section::loadSectionFile`. If cached, sets `preIndexedNextSpine` and leaves `silentIndexingActive = false`. Only sets `true` when indexing is genuinely needed.
|
||||
- Removed `requestUpdate()` entirely — the indicator clears naturally on the next page turn.
|
||||
- Added early-out in `silentIndexNextChapterIfNeeded` for `preIndexedNextSpine` match to avoid redundant Section construction.
|
||||
|
||||
The pre-check cost (one `loadSectionFile` call) only happens once per chapter due to `preIndexedNextSpine` caching.
|
||||
|
||||
Silent indexing is only performed on text-only penultimate pages (`!p->hasImages()`). On image pages, silent indexing is skipped entirely — the normal popup handles indexing on the next chapter transition. This avoids conflicts with the grayscale rendering pipeline (`displayWindow` after `displayGrayBuffer` triggers `grayscaleRevert`, causing image inversion/ghosting).
|
||||
|
||||
For text-only pages: after `renderContents` returns, the indicator is cleared via `displayWindow(FAST_REFRESH)` on just the status bar strip. This is safe because text-only pages use simple BW rendering without the grayscale pipeline.
|
||||
|
||||
Build succeeded (RAM: 31.1%, Flash: 99.6%).
|
||||
39
chat-summaries/2026-02-18_merge-pr979-silent-indexing.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Merge PR #979: Silent Pre-Indexing + Indexing Display Setting
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Branch:** `mod/merge-upstream-pr-979`
|
||||
|
||||
## Task Description
|
||||
|
||||
Merged upstream PR #979 (silent pre-indexing for the next chapter) and implemented both "Possible Improvements" from the PR:
|
||||
1. A user setting to choose between Popup, Status Bar Text, or Status Bar Icon for indexing feedback.
|
||||
2. Status bar indicator rendering during silent pre-indexing.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #979 Cherry-Pick (2 files)
|
||||
- **`src/activities/reader/EpubReaderActivity.h`** -- Added `silentIndexNextChapterIfNeeded()` method declaration and `silentIndexingActive` flag.
|
||||
- **`src/activities/reader/EpubReaderActivity.cpp`** -- Moved viewport dimension calculations before the `!section` block. Added `silentIndexNextChapterIfNeeded()` implementation that pre-indexes the next chapter when the penultimate page is displayed.
|
||||
|
||||
### New "Indexing Display" Setting (5 files)
|
||||
- **`src/CrossPointSettings.h`** -- Added `INDEXING_DISPLAY` enum (POPUP, STATUS_TEXT, STATUS_ICON) and `indexingDisplay` field.
|
||||
- **`src/CrossPointSettings.cpp`** -- Added persistence (write/read) for the new setting at the end of the settings chain.
|
||||
- **`src/SettingsList.h`** -- Registered the new Enum setting in the Display category.
|
||||
- **`lib/I18n/I18nKeys.h`** -- Added 4 new string IDs: `STR_INDEXING_DISPLAY`, `STR_INDEXING_POPUP`, `STR_INDEXING_STATUS_TEXT`, `STR_INDEXING_STATUS_ICON`.
|
||||
- **`lib/I18n/translations/*.yaml`** (8 files) -- Added translations for all 8 languages.
|
||||
|
||||
### Status Bar Indicator Implementation
|
||||
- **`EpubReaderActivity.cpp`** -- Conditional popup logic: only shows popup when setting is POPUP; for STATUS_TEXT/STATUS_ICON, shows centered "Indexing..." text on blank screen during non-silent indexing. For silent pre-indexing, sets `silentIndexingActive` flag before rendering so `renderStatusBar()` draws the indicator (text or 8x8 hourglass icon) in the status bar.
|
||||
- Defined an 8x8 1-bit hourglass bitmap icon (`kIndexingIcon`) for the icon mode.
|
||||
|
||||
## Build Verification
|
||||
|
||||
- PlatformIO build: SUCCESS
|
||||
- RAM: 31.1% (101,820 / 327,680 bytes)
|
||||
- Flash: 99.6% (6,525,028 / 6,553,600 bytes)
|
||||
|
||||
## Follow-Up Items
|
||||
|
||||
- Test on device: verify silent indexing triggers on penultimate page, verify all three display modes work correctly.
|
||||
- The status bar indicator shows optimistically on penultimate pages even if the next chapter is already cached (false positive clears immediately after `loadSectionFile` check).
|
||||
- Flash usage is at 99.6% -- monitor for future additions.
|
||||
59
chat-summaries/2026-02-18_merge-upstream-prs.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Merge Upstream PRs #965, #939, #852, #972, #971, #977, #975
|
||||
|
||||
**Date:** 2026-02-18
|
||||
**Branch:** mod/merge-upstream-1
|
||||
|
||||
## Task
|
||||
|
||||
Port 7 upstream PRs from crosspoint-reader/crosspoint-reader into the mod branch.
|
||||
|
||||
## Status per PR
|
||||
|
||||
| PR | Description | Result |
|
||||
|---|---|---|
|
||||
| #939 | Fix dangling pointer in MyLibraryActivity | Already ported, no changes needed |
|
||||
| #852 | HalPowerManager idle CPU freq scaling | Completed partial port (Lock RAII, WiFi check, skipLoopDelay, render locks) |
|
||||
| #965 | Fix paragraph formatting inside list items | Fully ported |
|
||||
| #972 | Micro-optimizations to eliminate value copies | Ported (fontMap move, getDataFromBook const ref) |
|
||||
| #971 | Remove redundant hasPrintableChars pass | Fully ported |
|
||||
| #977 | Skip unsupported image formats during parsing | Fully ported |
|
||||
| #975 | Fix UITheme memory leak on theme reload | Fully ported |
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #852 (partial port completion)
|
||||
- `lib/hal/HalPowerManager.h` -- Added `LockMode` enum, `currentLockMode`/`modeMutex` members, nested `Lock` RAII class (non-copyable/non-movable), `extern powerManager` declaration
|
||||
- `lib/hal/HalPowerManager.cpp` -- Added mutex init in `begin()`, WiFi.getMode() check in `setPowerSaving()`, Lock constructor/destructor, LockMode guard
|
||||
- `src/activities/settings/ClearCacheActivity.h` -- Added `skipLoopDelay()` override
|
||||
- `src/activities/settings/OtaUpdateActivity.h` -- Added `skipLoopDelay()` override
|
||||
- `src/activities/Activity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
|
||||
- `src/activities/ActivityWithSubactivity.cpp` -- Added `HalPowerManager::Lock` in `renderTaskLoop()`
|
||||
|
||||
### PR #965
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h` -- Added `listItemUntilDepth` member
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Modified `startElement` to handle `<p>` inside `<li>` without line break; added depth reset in `endElement`
|
||||
|
||||
### PR #972
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` -- `std::move(font)` in `insertFont`
|
||||
- `src/RecentBooksStore.h` / `.cpp` -- `getDataFromBook` now takes `const std::string&`
|
||||
|
||||
### PR #971
|
||||
- `lib/EpdFont/EpdFont.h` / `.cpp` -- Removed `hasPrintableChars` method
|
||||
- `lib/EpdFont/EpdFontFamily.h` / `.cpp` -- Removed `hasPrintableChars` method
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp` -- Removed 3 early-return guards calling `hasPrintableChars`
|
||||
|
||||
### PR #977
|
||||
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp` -- Added `PARSE_BUFFER_SIZE` constant, `isFormatSupported` guard before image extraction, timing instrumentation
|
||||
|
||||
### PR #975
|
||||
- `src/components/UITheme.h` -- Changed `currentTheme` to `std::unique_ptr<const BaseTheme>`
|
||||
- `src/components/UITheme.cpp` -- Changed allocations to `std::make_unique`
|
||||
|
||||
## Build Result
|
||||
|
||||
Build succeeded: RAM 31.1%, Flash 99.5%
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- PR #972 LyraTheme loop variable change not applicable (our code uses index-based loops)
|
||||
- Test on device to verify all changes work as expected
|
||||
49
chat-summaries/2026-02-19_10-46-summary.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Sync mod/master with upstream 1.1.0-RC
|
||||
|
||||
## Task Description
|
||||
Integrated 19 missing upstream PRs from `master` (1.1.0-RC) into `mod/master` via phased cherry-picking, preserving all existing mod enhancements.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Branch Setup
|
||||
- Created safety branch `mod/backup-pre-sync` from `mod/master`
|
||||
- Created integration branch `mod/sync-upstream-1.1.0` (19 cherry-picked commits + 1 cleanup)
|
||||
|
||||
### Phase 1: Low-Risk PRs (8 PRs)
|
||||
- **#646** Ukrainian hyphenation support (resolved conflict: merged with mod's `OMIT_HYPH_*` conditional compilation guards)
|
||||
- **#732** Lyra screens (resolved 5 conflicts: preserved mod's clock, 3-cover layout, cover rendering; added upstream's drawSubHeader, popup, keyboard/text field methods)
|
||||
- **#725** Lyra Icons (resolved conflicts: added icon infrastructure, iconForName(), Lyra icon assets; removed stale Lyra3CoversTheme.cpp)
|
||||
- **#768** Tweak Lyra popup UI (resolved conflicts: preserved mod's epub loading logic, added upstream popup constants)
|
||||
- **#880** Flash objects listing script (clean)
|
||||
- **#897** Keyboard font size increase (clean)
|
||||
- **#927** Translators list update (clean)
|
||||
- **#935** Missing up/down button labels (resolved conflicts: kept GUI.drawList() over old manual rendering)
|
||||
|
||||
### Phase 2: Medium-Risk PRs (8 PRs - all applied cleanly)
|
||||
- **#783** KOSync repositioning fix
|
||||
- **#923** Bresenham line drawing
|
||||
- **#933** Font map lookup performance
|
||||
- **#944** 4-bit BMP support
|
||||
- **#952** Skip large CSS files to prevent crashes
|
||||
- **#963** Word width and space calculation fix
|
||||
- **#964** Scale cover images up
|
||||
- **#970** Fix prev-page teleport to end of book
|
||||
|
||||
### Phase 3: Large PR
|
||||
- **#831** Compressed fonts (30.7% flash reduction, 73 files). Resolved 2 conflicts: merged font decompressor init in main.cpp, merged clearFontCache with mod's silent indexing in EpubReaderActivity.
|
||||
|
||||
### Phase 4: Overlap Assessment
|
||||
- **#556** (JPEG/PNG image support): Confirmed mod already has complete coverage. All converter files identical. No action needed.
|
||||
- **#980** (Basic table support): Confirmed mod's column-aligned table rendering is a strict superset. No action needed.
|
||||
|
||||
### Post-Sync
|
||||
- Removed stale `Lyra3CoversTheme.h` (3-cover support merged into LyraTheme)
|
||||
- Fixed `UITheme.cpp` to use `LyraTheme` for LYRA_3_COVERS variant
|
||||
- Updated `open-x4-sdk` submodule to `91e7e2b` (drawImageTransparent support for Lyra Icons)
|
||||
- Ran clang-format on all source files
|
||||
- Build verified: RAM 31.4%, Flash 57.0%
|
||||
|
||||
## Follow-Up Items
|
||||
- Merge `mod/sync-upstream-1.1.0` into `mod/master` when ready
|
||||
- On-device smoke testing (book loading, images, tables, bookmarks, dictionary, sleep screen, clock, home screen)
|
||||
- Safety branch `mod/backup-pre-sync` available for rollback if needed
|
||||
33
chat-summaries/2026-02-19_21-30-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Port Upstream PRs #997, #1003, #1005, #1010
|
||||
|
||||
## Task
|
||||
Cherry-pick / port four upstream PRs from crosspoint-reader/crosspoint-reader into the mod fork.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #997 -- Already Ported (no changes)
|
||||
Glyph null-safety in `getSpaceWidth`/`getTextAdvanceX` was already present via commit `c1b8e53`.
|
||||
|
||||
### PR #1010 -- Fix Dangling Pointer
|
||||
- **src/main.cpp**: `onGoToReader()` now copies the `initialEpubPath` string before calling `exitActivity()`, preventing a dangling reference when the owning activity is destroyed.
|
||||
|
||||
### PR #1005 -- Use HalPowerManager for Battery Percentage
|
||||
- **lib/hal/HalPowerManager.h**: Changed `getBatteryPercentage()` return type from `int` to `uint16_t`.
|
||||
- **lib/hal/HalPowerManager.cpp**: Same return type change.
|
||||
- **src/Battery.h**: Emptied contents (was `static BatteryMonitor battery(BAT_GPIO0)`).
|
||||
- **src/main.cpp**: Removed `#include "Battery.h"`.
|
||||
- **src/activities/home/HomeActivity.cpp**: Removed `#include "Battery.h"`.
|
||||
- **src/components/themes/BaseTheme.cpp**: Replaced `Battery.h` include with `HalPowerManager.h`, replaced `battery.readPercentage()` with `powerManager.getBatteryPercentage()` (2 occurrences).
|
||||
- **src/components/themes/lyra/LyraTheme.cpp**: Same replacements (2 occurrences).
|
||||
|
||||
### PR #1003 -- Render Image Placeholders While Waiting for Decode
|
||||
- **lib/Epub/Epub/blocks/ImageBlock.h/.cpp**: Added `isCached()` method that checks if the `.pxc` cache file exists.
|
||||
- **lib/Epub/Epub/Page.h/.cpp**: Added `PageImage::isCached()`, `PageImage::renderPlaceholder()`, `Page::renderTextOnly()`, `Page::countUncachedImages()`, `Page::renderImagePlaceholders()`. Added `#include <GfxRenderer.h>` to Page.h.
|
||||
- **src/activities/reader/EpubReaderActivity.cpp**: Modified `renderContents()` to check for uncached images and display text + placeholder rectangles immediately (Phase 1 with HALF_REFRESH), then proceed with full image decode and display (Phase 2 with fast refresh). Existing mod-specific double FAST_REFRESH logic for anti-aliased image pages is preserved for the cached-image path.
|
||||
|
||||
## Build Result
|
||||
SUCCESS -- RAM: 31.5%, Flash: 70.4%. No linter errors.
|
||||
|
||||
## Follow-up Items
|
||||
- PR #1003 is still open upstream (not merged); may need to rebase if upstream changes before merge.
|
||||
- The phased rendering for uncached images skips the mod's double FAST_REFRESH technique (relies on Phase 1's HALF_REFRESH instead). If grayscale quality on first-decode image pages is suboptimal, this could be revisited.
|
||||
33
chat-summaries/2026-02-19_port-pr978-font-perf.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Port Upstream PR #978: Improve Font Drawing Performance
|
||||
|
||||
**Date:** 2026-02-19
|
||||
**Branch:** `mod/more-upstream-patches-for-1.1.0`
|
||||
**Commit:** `3a06418`
|
||||
|
||||
## Task
|
||||
|
||||
Port upstream PR #978 (perf: Improve font drawing performance) which optimizes glyph rendering by 15-23% on device.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Cherry-picked from upstream (commit `07d715e`)
|
||||
|
||||
- **`lib/GfxRenderer/GfxRenderer.cpp`**: Introduced `TextRotation` enum and `renderCharImpl<TextRotation>` template function that consolidates normal and 90-CW-rotated rendering paths. Hoists the `is2Bit` conditional above pixel loops (eliminating per-pixel branch). Uses `if constexpr` for compile-time rotation path selection. Fixes operator precedence bug in `bmpVal` calculation.
|
||||
- **`lib/GfxRenderer/GfxRenderer.h`**: Changed `renderChar` signature (`const int* y` -> `int* y`). Moved `getGlyphBitmap` from private to public (needed by the free-function template).
|
||||
|
||||
### Mod-specific extension
|
||||
|
||||
- Extended `TextRotation` enum with `Rotated90CCW` and refactored `drawTextRotated90CCW` to delegate to the template. This fixed two bugs in the mod's CCW code:
|
||||
1. Operator precedence bug in `bmpVal`: `3 - (byte >> bit_index) & 0x3` -> `3 - ((byte >> bit_index) & 0x3)`
|
||||
2. Missing compressed font support: was using raw `bitmap[offset]` instead of `getGlyphBitmap()`
|
||||
|
||||
## Recovery of Discarded Changes
|
||||
|
||||
During plan-mode dry-run cherry-pick reset, `git checkout -- .` inadvertently discarded unstaged working tree changes in 12 files (from PRs #1005, #1010, #1003). These were recovered by:
|
||||
- Cherry-picking upstream commits `cabbfcf` (#1005) and `63b2643` (#1010) with conflict resolution (kept mod's `CrossPointSettings.h` include)
|
||||
- Fetching unmerged PR #1003 branch and manually applying the diff (ImageBlock::isCached, Page placeholder rendering, EpubReaderActivity phased rendering)
|
||||
- Committed as `18be265`
|
||||
|
||||
## Build
|
||||
|
||||
PlatformIO build succeeded. RAM: 31.5%, Flash: 70.4%.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Port Upstream 1.1.0-RC Fixes
|
||||
|
||||
**Date:** 2026-02-19
|
||||
**Task:** Port 3 commits from upstream crosspoint-reader PR #992 (1.1.0-rc branch) into mod/master.
|
||||
|
||||
## Commits Ported
|
||||
|
||||
### 1. chore: Bump version to 1.1.0 (402e887) — Skipped
|
||||
- mod/master already has `version = 1.1.1-rc`, which is ahead of upstream's 1.1.0.
|
||||
|
||||
### 2. fix: Crash on unsupported bold/italic glyphs (3e2c518)
|
||||
- **File:** `lib/GfxRenderer/GfxRenderer.cpp`
|
||||
- Added null-safety checks to `getSpaceWidth()` and `getTextAdvanceX()`.
|
||||
- `getSpaceWidth()`: returns 0 if the space glyph is missing in the styled font variant.
|
||||
- `getTextAdvanceX()`: falls back to `REPLACEMENT_GLYPH` (U+FFFD) if a glyph is missing, and treats as zero-width if the replacement is also unavailable.
|
||||
- Prevents a RISC-V Load access fault (nullptr dereference) when indexing chapters with characters unsupported by bold/italic font variants.
|
||||
|
||||
### 3. fix: Increase PNGdec buffer for wide images (b8e743e)
|
||||
- **Files:** `platformio.ini`, `lib/Epub/Epub/converters/PngToFramebufferConverter.cpp`
|
||||
- Bumped `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 (supports up to 2048px wide RGBA images).
|
||||
- Added `bytesPerPixelFromType()` and `requiredPngInternalBufferBytes()` helper functions.
|
||||
- Added a pre-decode safety check that aborts with a log error if the PNG scanline buffer would overflow PNGdec's internal buffer.
|
||||
|
||||
## Verification
|
||||
- Build (`pio run -e default`) succeeded cleanly with no errors or warnings.
|
||||
- RAM: 31.5%, Flash: 70.4%.
|
||||
|
||||
## 4. CSS-aware image sizing (PR #1002, commit c8ba4fe)
|
||||
- **Files:** `lib/Epub/Epub/css/CssStyle.h`, `lib/Epub/Epub/css/CssParser.cpp`, `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp`
|
||||
- Added `imageHeight` field to `CssPropertyFlags` and `CssStyle` (our existing `width` field maps to upstream's `imageWidth`).
|
||||
- Added CSS `height` property parsing into `imageHeight`.
|
||||
- Added `imageHeight` and `width` to cache serialization; bumped `CSS_CACHE_VERSION` 2->3.
|
||||
- Replaced viewport-fit-only image scaling in `ChapterHtmlSlimParser` with CSS-aware sizing: resolves CSS height/width (including inline styles), preserves aspect ratio, clamps to viewport, includes divide-by-zero guards.
|
||||
- `platformio.ini` changes excluded from commit per user request (PNG buffer bump was already committed separately).
|
||||
|
||||
## Follow-up
|
||||
- None required. Changes are straightforward upstream ports.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Sleep Cover: Double FAST_REFRESH for Dithered Letterbox
|
||||
|
||||
**Date:** 2026-02-19
|
||||
|
||||
## Task
|
||||
|
||||
Apply the "double FAST_REFRESH" technique (proven for inline EPUB images via PR #556) to sleep cover rendering when dithered letterboxing is enabled. This replaces the corruption-prone HALF_REFRESH with two FAST_REFRESH passes through a white intermediate state.
|
||||
|
||||
Also reverted the hash-based block dithering workaround back to standard Bayer dithering for all gray ranges, confirming the root cause was HALF_REFRESH rather than the dithering pattern.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/activities/boot_sleep/SleepActivity.cpp`
|
||||
|
||||
- **Added `USE_SLEEP_DOUBLE_FAST_REFRESH` define** (set to 1): compile-time toggle for easy A/B testing.
|
||||
- **Removed `bayerCrossesBwBoundary()` and `hashBlockDither()`**: These were the HALF_REFRESH crosstalk workaround (2x2 hash blocks for gray 171-254). Removed entirely since double FAST_REFRESH addresses the root cause.
|
||||
- **Simplified `drawLetterboxFill()`**: Dithered mode now always uses `quantizeBayerDither()` for all gray ranges -- same algorithm used for non-boundary grays. No more hash/Bayer branching.
|
||||
- **Modified `renderBitmapSleepScreen()` BW pass**: When dithered letterbox is active and `USE_SLEEP_DOUBLE_FAST_REFRESH` is enabled:
|
||||
1. Clear screen to white + FAST_REFRESH (establishes clean baseline)
|
||||
2. Render letterbox fill + cover + FAST_REFRESH (shows actual content)
|
||||
- **Letterbox fill included in all greyscale passes**: Initially skipped to avoid scan coupling, but re-enabled after testing confirmed the double FAST_REFRESH baseline prevents corruption. This ensures the letterbox color matches the cover edge after the greyscale LUT is applied.
|
||||
- **Standard HALF_REFRESH path preserved** for letterbox=none, letterbox=solid, or when define is disabled.
|
||||
|
||||
## Build
|
||||
|
||||
Successfully compiled with `pio run` (0 errors, 0 warnings).
|
||||
|
||||
## Testing
|
||||
|
||||
Confirmed working on-device -- dithered letterbox renders cleanly without corruption, and letterbox color matches the cover edge including after greyscale layer application.
|
||||
|
||||
## Commit
|
||||
|
||||
`0fda903` on branch `mod/sleep-screen-tweaks-on-1.1.0-rc-double-fast`
|
||||
36
chat-summaries/2026-02-20_15-30-port-upstream-prs.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Port Upstream PRs #1038, #1045, #1037, #1019
|
||||
|
||||
## Task
|
||||
|
||||
Manually port 4 open upstream PRs into the `mod/sync-upstream-PRs` branch, pull in Romanian translations from `upstream/master`, and create a MERGED.md tracking document.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1038 (partial -- incremental fixes)
|
||||
- `lib/Epub/Epub/ParsedText.cpp`: Added `.erase()` calls in `layoutAndExtractLines` to remove consumed words after line extraction (fixes redundant early flush bug). Also fixed `wordContinues` flag handling in `hyphenateWordAtIndex` to preserve prefix's attach-to-previous flag.
|
||||
|
||||
### PR #1045 (direct string changes)
|
||||
- Updated `STR_FORGET_BUTTON` in all 9 translation yaml files to shorter labels.
|
||||
- Pulled `romanian.yaml` from `upstream/master` (merged upstream PR #987).
|
||||
|
||||
### PR #1037 (4-file manual port)
|
||||
- `lib/Utf8/Utf8.h`: Added `utf8IsCombiningMark()` utility function.
|
||||
- `lib/EpdFont/EpdFont.cpp`: Added combining mark positioning in `getTextBounds`.
|
||||
- `lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`: Added NFC-like precomposition for common Western European diacritics in `collectCodepoints()`.
|
||||
- `lib/GfxRenderer/GfxRenderer.cpp`: Added combining mark rendering in `drawText`, `drawTextRotated90CW`, `drawTextRotated90CCW` (mod-only), and `getTextAdvanceX`. Also wrapped cursor advance in `renderCharImpl` to skip combining marks.
|
||||
|
||||
### PR #1019 (1-file manual port)
|
||||
- `src/activities/home/MyLibraryActivity.cpp`: Added `getFileExtension()` helper and updated `drawList` call to show file extensions in the File Browser.
|
||||
|
||||
### Tracking
|
||||
- `mod/prs/MERGED.md`: Created tracking document with details for all 4 PRs.
|
||||
|
||||
## Build Verification
|
||||
|
||||
`pio run -e mod` succeeded (24.49s, Flash: 57.2%, RAM: 31.4%).
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- All 4 upstream PRs are still open; monitor for any review feedback or changes before they merge.
|
||||
- Romanian yaml is missing mod-specific string translations (using English fallbacks).
|
||||
- PR #1019 open question: how file extensions look on long filenames.
|
||||
23
chat-summaries/2026-02-20_16-52-summary.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Expandable Selected Row for Long Filenames
|
||||
|
||||
## Task
|
||||
|
||||
Implement a mod enhancement to PR #1019 (Display file extensions in File Browser). When the selected row's filename is too long, expand that row to 2 lines with character-level text wrapping. The file extension moves to the bottom-right of the expanded area. Non-selected rows retain single-line truncation behavior.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/components/themes/BaseTheme.cpp`
|
||||
- Added `wrapTextToLines` static helper in the anonymous namespace: character-level UTF-8-aware text wrapping with "..." truncation on the final line.
|
||||
- Modified `drawList`: pre-loop expansion detection for the selected item, pagination adjustment (`pageItems - 1`), expanded selection highlight (`2 * rowHeight`), 2-line title rendering via `wrapTextToLines`, extension repositioned to bottom-right of expanded area, `yPos` tracking for subsequent items.
|
||||
|
||||
### `src/components/themes/lyra/LyraTheme.cpp`
|
||||
- Added identical `wrapTextToLines` helper.
|
||||
- Modified `drawList` with analogous expansion logic, adapted for Lyra-specific styling (rounded-rect selection highlight, icon support, scroll bar-aware content width, preliminary text width computation for expansion check).
|
||||
|
||||
### `mod/prs/MERGED.md`
|
||||
- Updated PR #1019 section to document the mod enhancement, files modified, and design decisions.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- Visual testing on device to verify text positioning and edge cases (very long single-word filenames, last item on page expanding, single-item lists).
|
||||
- The page-up/page-down navigation in `MyLibraryActivity::loop()` uses the base `pageItems` from `getNumberOfItemsPerPage` which doesn't account for expansion. This causes a minor cosmetic mismatch (page jump size differs by 1 from visual page when expansion is active) but is functionally correct.
|
||||
29
chat-summaries/2026-02-20_17-32-summary.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Fix Text Wrapping and Spacing for Expanded Selected Row
|
||||
|
||||
## Task
|
||||
|
||||
Improve the expandable selected row feature (from PR #1019 enhancement). Two issues were identified from device testing:
|
||||
1. Character-level wrapping broke words mid-character (e.g., "Preside / nt"), resulting in unnatural line breaks.
|
||||
2. Poor vertical spacing -- text lines clustered near the top of the expanded highlight area with large empty space at the bottom.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### `src/components/themes/BaseTheme.cpp`
|
||||
- Rewrote `wrapTextToLines` with 3-tier break logic:
|
||||
1. Preferred delimiters: " -- ", " - ", en-dash, em-dash (breaks at last occurrence to maximize line 1)
|
||||
2. Word boundaries: last space or hyphen that fits
|
||||
3. Character-level fallback for long unbroken tokens
|
||||
- Extracted `truncateWithEllipsis` helper to reduce duplication
|
||||
- Fixed expanded row rendering: text lines vertically centered in 2x row height area, extension baseline-aligned with last text line
|
||||
|
||||
### `src/components/themes/lyra/LyraTheme.cpp`
|
||||
- Same `wrapTextToLines` rewrite and `truncateWithEllipsis` helper
|
||||
- Same vertical centering for expanded row text lines
|
||||
- Icon also vertically centered in expanded area
|
||||
- Extension baseline-aligned with last text line instead of fixed offset
|
||||
|
||||
### `mod/prs/MERGED.md`
|
||||
- Updated PR #1019 mod enhancement section to reflect the new wrapping strategy and spacing improvements
|
||||
|
||||
## Follow-up Items
|
||||
- Device testing to verify improved wrapping and spacing visually
|
||||
51
chat-summaries/2026-02-20_22-30-summary.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Port Upstream 1.1.0 RC Commits to mod/master
|
||||
|
||||
## Task Description
|
||||
|
||||
Audited all 11 commits from upstream PR #992 (1.1.0 Release Candidate) and ported the remaining unported/partially-ported ones to `mod/master`.
|
||||
|
||||
## Commit Audit Results
|
||||
|
||||
### Already Fully Ported (no changes needed)
|
||||
- **402e887** - Bump version to 1.1.0 (skip, not relevant)
|
||||
- **3e2c518** (#997) - Glyph null-safety (ported in c1b8e53)
|
||||
- **b8e743e** (#995) - PNGdec buffer size (ported in c1b8e53)
|
||||
- **588984e** (#1010) - Dangling pointer (ported in 18be265)
|
||||
- **87d9d1d** (#978) - Font drawing performance (ported in 3a06418)
|
||||
- **2cc497c** (#957) - Double FAST_REFRESH (ported in ad282ca)
|
||||
|
||||
### Already Effectively Present
|
||||
- **8db3542** (#1017) - Cover outlines for Lyra themes (mod's LyraTheme.cpp already draws outlines unconditionally; Lyra3CoversTheme.cpp doesn't exist in mod)
|
||||
|
||||
### Ported in This Session
|
||||
|
||||
#### 1. Aligned #1002 + Ported #1018 (CSS cache invalidation)
|
||||
**Files:** `CssParser.h`, `CssParser.cpp`, `Epub.cpp`, `ChapterHtmlSlimParser.cpp`
|
||||
- Added `tryInterpretLength()` (bool return + out-param) to properly skip non-length CSS values like `auto`, `inherit`
|
||||
- Added `deleteCache()` method to CssParser
|
||||
- Moved `CSS_CACHE_VERSION` to static class member
|
||||
- Added stale cache file removal in `loadFromCache()` on version mismatch
|
||||
- Added "both CSS width and height set" branch in image sizing logic
|
||||
- Refactored `parseCssFiles()` to early-return when cache exists
|
||||
- Refactored `load()` to call `loadFromCache()` and invalidate sections on stale cache
|
||||
|
||||
#### 2. Ported #1014 (Strip unused CSS rules)
|
||||
**File:** `CssParser.cpp`
|
||||
- Added selector filtering in `processRuleBlockWithStyle` to skip `+`, `>`, `[`, `:`, `#`, `~`, `*`, and whitespace selectors
|
||||
- Fixed `normalized()` trailing whitespace to use `while` loop and also strip `\n`
|
||||
- Added TODO comments for multi-class selector support
|
||||
|
||||
#### 3. Ported #990 (Continue reading card classic theme)
|
||||
**Files:** `BaseTheme.h`, `BaseTheme.cpp`
|
||||
- Changed `homeTopPadding` from 20 to 40
|
||||
- Computed `bookWidth` from cover BMP aspect ratio (clamped to 90% screen width, fallback to half-width)
|
||||
- Fixed centering: added `rect.x` offset to `bookX`, `boxX`, and `continueBoxX`
|
||||
- Simplified cover drawing (removed scaling/centering math since bookWidth now matches aspect ratio)
|
||||
|
||||
## Build Status
|
||||
All changes compile successfully (0 errors, 0 warnings in modified files).
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device to verify CSS cache invalidation works correctly (books with stale caches should auto-rebuild)
|
||||
- Test classic theme continue reading card with various cover aspect ratios
|
||||
- Test image sizing with EPUBs that specify both CSS width and height on images
|
||||
27
chat-summaries/2026-02-21_00-00-summary.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# NTP Sync Clock Feature
|
||||
|
||||
## Task
|
||||
Add a "Sync Clock" action to the Clock settings category that connects to WiFi and performs NTP time synchronization.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
- **src/activities/settings/NtpSyncActivity.h** - Activity class extending `ActivityWithSubactivity` with states: WIFI_SELECTION, SYNCING, SUCCESS, FAILED
|
||||
- **src/activities/settings/NtpSyncActivity.cpp** - Full implementation:
|
||||
- Launches `WifiSelectionActivity` with auto-connect enabled
|
||||
- On WiFi connect: blocking `waitForNtpSync(8000ms)`
|
||||
- Shows synced time on success; auto-dismisses after 5 seconds
|
||||
- Failed state requires manual Back press
|
||||
- Clean WiFi teardown in `onExit()`
|
||||
|
||||
### Modified Files
|
||||
- **src/activities/settings/SettingsActivity.h** - Added `SyncClock` to `SettingAction` enum
|
||||
- **src/activities/settings/SettingsActivity.cpp** - Added include, switch case handler, and action in `rebuildClockActions()`
|
||||
- **lib/I18n/translations/*.yaml** (all 9 files) - Added `STR_SYNC_CLOCK` and `STR_TIME_SYNCED` string keys
|
||||
- **lib/I18n/I18nKeys.h, I18nStrings.h, I18nStrings.cpp** - Regenerated from YAML
|
||||
|
||||
## Build Result
|
||||
SUCCESS - RAM: 32.7%, Flash: 70.7%
|
||||
|
||||
## Follow-up Items
|
||||
- Non-English translations use English fallback values; translators can update later
|
||||
52
chat-summaries/2026-02-21_12-00-summary.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Manage Books Feature - Implementation
|
||||
|
||||
## Task Description
|
||||
Implemented the full Manage Books feature plan across 10 commits on the `mod/manage-books` branch. The feature adds Archive, Delete, Delete Cache, and Reindex book management actions, an interactive End of Book menu, and a bugfix for Clear Reading Cache.
|
||||
|
||||
## Changes Made (10 commits)
|
||||
|
||||
### COMMIT 1: `feat(hal): expose rename() on HalStorage`
|
||||
- `lib/hal/HalStorage.h` / `.cpp` — Added `rename(path, newPath)` forwarding to SDCardManager for file/directory move operations.
|
||||
|
||||
### COMMIT 2: `feat(i18n): add string keys for book management feature`
|
||||
- `lib/I18n/translations/english.yaml` — Added 15 new string keys (STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK, etc.)
|
||||
- `lib/I18n/I18nKeys.h` — Regenerated via gen_i18n.py
|
||||
|
||||
### COMMIT 3: `feat: add BookManager utility and RecentBooksStore::clear()`
|
||||
- **New:** `src/util/BookManager.h` / `.cpp` — Static utility namespace with `archiveBook`, `unarchiveBook`, `deleteBook`, `deleteBookCache`, `reindexBook`, `isArchived`, `getBookCachePath`. Archive mirrors directory structure under `/.archive/` and renames cache dirs to match new path hashes.
|
||||
- `src/RecentBooksStore.h` / `.cpp` — Added `clear()` method.
|
||||
|
||||
### COMMIT 4: `feat: add BookManageMenuActivity popup sub-activity`
|
||||
- **New:** `src/activities/home/BookManageMenuActivity.h` / `.cpp` — Contextual popup menu with Archive/Unarchive, Delete, Delete Cache Only, Reindex Book. Supports long-press on Reindex for REINDEX_FULL (includes cover/thumb regeneration).
|
||||
|
||||
### COMMIT 5: `refactor: change browse activities to ActivityWithSubactivity`
|
||||
- `HomeActivity`, `MyLibraryActivity`, `RecentBooksActivity` — Changed base class from `Activity` to `ActivityWithSubactivity`. Added `subActivity` guard at top of `loop()`.
|
||||
|
||||
### COMMIT 6: `feat: add long-press Confirm for book management in file browser and recents`
|
||||
- `MyLibraryActivity` / `RecentBooksActivity` — Long-press Confirm on a book opens BookManageMenuActivity. Actions executed via BookManager, file list refreshed afterward.
|
||||
|
||||
### COMMIT 7: `feat: add long-press on HomeActivity for book management and archive browsing`
|
||||
- `HomeActivity` — Long-press Confirm on recent book opens manage menu. Long-press on Browse Files navigates to `/.archive/`.
|
||||
- `main.cpp` — Wired `onMyLibraryOpenWithPath` callback through to HomeActivity constructor.
|
||||
|
||||
### COMMIT 8: `feat: replace Delete Book Cache with Manage Book in reader menu`
|
||||
- `EpubReaderMenuActivity` — Replaced DELETE_CACHE menu item with MANAGE_BOOK. Opens BookManageMenuActivity as sub-activity. Added ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK, REINDEX_BOOK_FULL action types.
|
||||
- `EpubReaderActivity` — Handles new actions via BookManager.
|
||||
|
||||
### COMMIT 9: `feat: add EndOfBookMenuActivity replacing static end-of-book text`
|
||||
- **New:** `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp` — Interactive popup with Archive, Delete, Back to Beginning, Close Book, Close Menu options.
|
||||
- `EpubReaderActivity` / `XtcReaderActivity` — Replaced static "End of Book" text with EndOfBookMenuActivity.
|
||||
- `TxtReaderActivity` — Added end-of-book detection (advance past last page triggers menu).
|
||||
|
||||
### COMMIT 10: `fix: ClearCacheActivity now clears txt_* caches and recents list`
|
||||
- `ClearCacheActivity.cpp` — Added `txt_*` to directory prefix check. Calls `RECENT_BOOKS.clear()` after cache wipe.
|
||||
|
||||
## New Files
|
||||
- `src/util/BookManager.h` / `.cpp`
|
||||
- `src/activities/home/BookManageMenuActivity.h` / `.cpp`
|
||||
- `src/activities/reader/EndOfBookMenuActivity.h` / `.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify all menu interactions, archive/unarchive flow, end-of-book menu behavior
|
||||
- Verify cache rename works correctly across different book formats
|
||||
- Consider adding translations for new strings in non-English language files
|
||||
24
chat-summaries/2026-02-21_16-30-summary.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Port Upstream PR #1027: ParsedText Word-Width Cache and Hyphenation Early Exit
|
||||
|
||||
## Task
|
||||
|
||||
Ported upstream PR #1027 (jpirnay) into the mod. The PR reduces `ParsedText::layoutAndExtractLines` CPU time by 5–9% via two independent optimizations.
|
||||
|
||||
## Changes Made
|
||||
|
||||
**`lib/Epub/Epub/ParsedText.cpp`** (single file):
|
||||
1. Added `#include <cstring>` for `memcpy`/`memcmp`
|
||||
2. Added 128-entry direct-mapped word-width cache in the anonymous namespace (`WordWidthCacheEntry`, FNV-1a hash, `cachedMeasureWordWidth`). 4 KB in BSS, zero heap allocation.
|
||||
3. Switched `calculateWordWidths` to use `cachedMeasureWordWidth` instead of `measureWordWidth`
|
||||
4. Added `lineBreakIndices.reserve(totalWordCount / 8 + 1)` in `computeLineBreaks`
|
||||
5. In `hyphenateWordAtIndex`: added reusable `prefix` string buffer (avoids per-iteration `substr` allocations) and early exit `break` when prefix exceeds available width (ascending byte-offset order means all subsequent candidates will also be too wide)
|
||||
|
||||
**`mod/prs/MERGED.md`**: Added PR #1027 entry (TOC link + full section with context, changes, differences, discussion).
|
||||
|
||||
## Key Adaptation
|
||||
|
||||
The upstream PR targets `std::list`-based code, but our mod already uses `std::vector` (from PR #1038). List-specific optimizations (splice in `extractLine`, `std::next` vs `std::advance`, `continuesVec` pointer sync) were not applicable. Only the algorithmic improvements were ported.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- None. Port is complete.
|
||||
24
chat-summaries/2026-02-21_17-22-summary.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Port PR #1068: Correct hyphenation of URLs
|
||||
|
||||
## Task
|
||||
|
||||
Port upstream PR [#1068](https://github.com/crosspoint-reader/crosspoint-reader/pull/1068) by Uri-Tauber into the mod fork. The PR was not yet merged upstream, so it was manually patched in.
|
||||
|
||||
## Changes made
|
||||
|
||||
1. **`lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`**: Added `case '/':` to `isExplicitHyphen` switch, treating `/` as an explicit hyphen delimiter for URL path segments.
|
||||
|
||||
2. **`lib/Epub/Epub/hyphenation/Hyphenator.cpp`**: Replaced the single combined filter in `buildExplicitBreakInfos` with a two-stage check:
|
||||
- First checks `isExplicitHyphen(cp)`
|
||||
- Then skips repeated separators (e.g., `//` in `http://`, `--`)
|
||||
- Then applies strict alphabetic-surround rule only for non-URL separators (`cp != '/' && cp != '-'`)
|
||||
|
||||
3. **`mod/prs/MERGED.md`**: Added PR #1068 entry with full documentation.
|
||||
|
||||
## Mod enhancements over upstream PR
|
||||
|
||||
- Included coderabbit's nitpick suggestion (not yet addressed in upstream) to prevent breaks between consecutive identical separators like `//` and `--`.
|
||||
|
||||
## Follow-up items
|
||||
|
||||
- None. Port is complete.
|
||||
26
chat-summaries/2026-02-21_23-15-summary.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Implement BmpViewer Activity (upstream PR #887)
|
||||
|
||||
## Task
|
||||
Port the BmpViewer feature from upstream crosspoint-reader PR #887 to the mod/master fork, enabling .bmp file viewing from the file browser.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New Files
|
||||
- **src/activities/util/BmpViewerActivity.h** -- Activity subclass declaration with file path, onGoBack callback, and loadFailed state
|
||||
- **src/activities/util/BmpViewerActivity.cpp** -- BMP loading/rendering implementation: shows loading indicator, opens file via Storage HAL, parses BMP headers, computes centered position with aspect-ratio-preserving layout, renders via `renderer.drawBitmap()`, draws localized back button hint, handles back button input in loop
|
||||
|
||||
### Modified Files
|
||||
- **src/activities/reader/ReaderActivity.h** -- Added `isBmpFile()` and `onGoToBmpViewer()` private declarations
|
||||
- **src/activities/reader/ReaderActivity.cpp** -- Added BmpViewerActivity include, `isBmpFile()` implementation, `onGoToBmpViewer()` implementation (sets currentBookPath, exits, enters BmpViewerActivity with goToLibrary callback), BMP routing in `onEnter()` before XTC/TXT checks
|
||||
- **src/activities/home/MyLibraryActivity.cpp** -- Added `.bmp` to the file extension filter in `loadFiles()`
|
||||
- **src/components/themes/lyra/LyraTheme.cpp** -- Changed `fillRect` to `fillRoundedRect` in `drawButtonHints()` for both FULL and SMALL button sizes to prevent white rectangles overflowing rounded button borders
|
||||
|
||||
## Build Result
|
||||
- PlatformIO build SUCCESS (default env)
|
||||
- RAM: 32.7% (107,308 / 327,680 bytes)
|
||||
- Flash: 71.1% (4,657,244 / 6,553,600 bytes)
|
||||
|
||||
## Follow-up Items
|
||||
- Test on hardware with various BMP files (different sizes, bit depths)
|
||||
- Consider adding image scaling for oversized BMPs (currently `drawBitmap` handles scaling)
|
||||
- Future enhancements mentioned in upstream PR: next/prev image navigation, "display and sleep" button
|
||||
27
chat-summaries/2026-02-21_port-pr1055-byte-framebuffer.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Port PR #1055: Byte-level framebuffer writes
|
||||
|
||||
**Date:** 2026-02-21
|
||||
**Task:** Port upstream PR #1055 (jpirnay) into the mod, including coderabbit review nitpick.
|
||||
|
||||
## Changes made
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.h`
|
||||
- Added two private method declarations: `fillPhysicalHSpanByte` (patterned byte-level span writer) and `fillPhysicalHSpan` (solid-fill wrapper).
|
||||
|
||||
### `lib/GfxRenderer/GfxRenderer.cpp`
|
||||
- Added `#include <cstring>` for `memset`.
|
||||
- Implemented `fillPhysicalHSpanByte`: bounds-clamped byte-level span writer with MSB-first bit packing, partial-byte masking at edges, and `memset` for aligned middle.
|
||||
- Implemented `fillPhysicalHSpan`: thin wrapper mapping `state` bool to `0x00`/`0xFF` pattern byte.
|
||||
- **`drawLine`**: Replaced per-pixel loops for axis-aligned lines with orientation-aware `fillPhysicalHSpan` calls (vertical lines in Portrait, horizontal lines in Landscape).
|
||||
- **`fillRect`**: Replaced per-row `drawLine` loop with orientation-specific byte-level fast paths for all 4 orientations.
|
||||
- **`fillRectDither`**: Replaced per-pixel `drawPixelDither` loops for DarkGray/LightGray with orientation-aware `fillPhysicalHSpanByte` using pre-computed byte patterns.
|
||||
- **`fillPolygon`** (coderabbit nitpick): Added `fillPhysicalHSpan` fast path for Landscape orientations in the scanline inner loop.
|
||||
|
||||
### `mod/prs/MERGED.md`
|
||||
- Appended PR #1055 entry with full documentation.
|
||||
|
||||
## Differences from upstream
|
||||
- `fillPolygon` landscape optimization (from coderabbit review nitpick) was applied as a mod enhancement.
|
||||
|
||||
## Follow-up items
|
||||
- None. Build verified successful.
|
||||
28
chat-summaries/2026-02-21_readme-mod-update.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# README.md Update for Mod Branch
|
||||
|
||||
**Date:** 2026-02-21
|
||||
|
||||
## Task
|
||||
|
||||
Updated `README.md` to distinguish this mod fork from upstream CrossPoint Reader, documenting all mod-exclusive features, updated feature status, upstream PR ports, and upstream compatibility notes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
**File:** `README.md`
|
||||
|
||||
1. **Header/intro** — Renamed to "CrossPoint Reader (Mod)" with a link to the upstream repo and a summary of what the mod provides. Added blockquote linking to upstream.
|
||||
2. **Motivation** — Added a paragraph explaining the mod's purpose (faster iteration on features and fixes). Fixed "truely" typo.
|
||||
3. **Features & Usage checklist** — Updated to reflect current mod capabilities:
|
||||
- Checked: image support (JPEG/PNG), table rendering, bookmarks, dictionary, book management, clock, letterbox fill, placeholder covers, screen rotation (4 orientations), end-of-book menu, silent pre-indexing
|
||||
- Added sub-items: file extensions, expandable rows
|
||||
- Still unchecked: user provided fonts, full UTF, EPUB picker with cover art
|
||||
4. **New section: "What This Mod Adds"** — Organized by category (Reading Enhancements, Home Screen & Navigation, Book Management, Reader Menu, Display & Rendering, Performance). Documents bookmarks, dictionary, tables, end-of-book menu, clock/NTP, adaptive home screen, file browser improvements, archive system, manage book menu, long-press actions, letterbox fill, landscape CCW, silent pre-indexing, placeholder covers, and 5 ported upstream performance PRs.
|
||||
5. **New section: "Upstream Compatibility"** — Documents what's missing from the mod (BmpViewer, Catalan translations), build differences (font/hyphenation omissions, version string), and links to `mod/prs/MERGED.md`.
|
||||
6. **Installing** — Replaced web flasher instructions with `pio run -e mod --target upload`. Noted `env:default` alternative.
|
||||
7. **Development** — Updated clone command to use `-b mod/master`. Added build environments table (`mod` vs `default`). Updated flash command.
|
||||
8. **Contributing** — Simplified to note this is a personal mod fork with a link to the upstream repo for contributions.
|
||||
|
||||
## Follow-up Items
|
||||
|
||||
- `USER_GUIDE.md` is out of date and does not document mod features (bookmarks, dictionary, clock, book management, etc.)
|
||||
- The data caching section in Internals could be expanded to mention mod-specific cache files (`bookmarks.bin`, `book_settings.bin`, `dictionary.cache`, image `.pxc` files)
|
||||
28
chat-summaries/2026-02-26_12-00-summary.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Boot NTP Auto-Sync Feature
|
||||
|
||||
## Task
|
||||
Add a "Auto Sync on Boot" toggle in Clock Settings that silently syncs time via WiFi/NTP during boot, with no user interaction required and graceful failure handling.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New files
|
||||
- `src/util/BootNtpSync.h` / `src/util/BootNtpSync.cpp` -- Background FreeRTOS task that scans WiFi, connects to a saved network, runs NTP sync, then tears down WiFi. Provides `start()`, `cancel()`, and `isRunning()` API.
|
||||
|
||||
### Modified files
|
||||
- `src/CrossPointSettings.h` -- Added `uint8_t autoNtpSync = 0` field
|
||||
- `src/CrossPointSettings.cpp` -- Added persistence (write/read) for the new field
|
||||
- `src/SettingsList.h` -- Added Toggle entry under Clock category
|
||||
- `lib/I18n/translations/*.yaml` (all 9 languages) -- Added `STR_AUTO_NTP_SYNC` string
|
||||
- `lib/I18n/I18nKeys.h` / `lib/I18n/I18nStrings.cpp` -- Regenerated via `gen_i18n.py`
|
||||
- `src/main.cpp` -- Calls `BootNtpSync::start()` during setup (non-blocking)
|
||||
- `src/activities/settings/NtpSyncActivity.cpp` -- Added `BootNtpSync::cancel()` guard
|
||||
- `src/activities/network/WifiSelectionActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/settings/OtaUpdateActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/settings/KOReaderAuthActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/reader/KOReaderSyncActivity.cpp` -- Added cancel guard
|
||||
- `src/activities/network/CrossPointWebServerActivity.cpp` -- Added cancel guard
|
||||
|
||||
## Follow-up Items
|
||||
- Translations: all non-English languages currently use English fallback for the new string
|
||||
- The FreeRTOS task uses 4096 bytes of stack; monitor for stack overflow if WiFi scan behavior changes
|
||||
- WiFi adds ~50-60KB RAM pressure during the sync window (temporary)
|
||||
43
chat-summaries/2026-02-26_18-46-summary.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Port Upstream PRs #1207 and #1209
|
||||
|
||||
## Task
|
||||
Ported two upstream PRs from crosspoint-reader/crosspoint-reader to the fork:
|
||||
- **PR #1207**: `fix: use HTTPClient::writeToStream for downloading files from OPDS`
|
||||
- **PR #1209**: `feat: Support for multiple OPDS servers`
|
||||
|
||||
Cherry-picking was not possible due to significant divergences (WiFiClient vs NetworkClient naming, i18n changes, binary vs JSON settings, missing JsonSettingsIO/ObfuscationUtils).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1207 (2 files modified)
|
||||
- `src/network/HttpDownloader.cpp` — Added `FileWriteStream` class, replaced manual chunked download loop with `HTTPClient::writeToStream`, improved Content-Length handling and post-download validation
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — Truncated download status text to fit screen width
|
||||
|
||||
### PR #1209 (6 files created, ~15 files modified, 2 files deleted)
|
||||
|
||||
**New files:**
|
||||
- `src/OpdsServerStore.h` / `src/OpdsServerStore.cpp` — Singleton store for up to 8 OPDS servers with MAC-based XOR+base64 password obfuscation and JSON persistence (self-contained — fork lacks JsonSettingsIO/ObfuscationUtils, so persistence was inlined)
|
||||
- `src/activities/settings/OpdsServerListActivity.h` / `.cpp` — Dual-mode activity: settings list (add/edit/delete) and server picker
|
||||
- `src/activities/settings/OpdsSettingsActivity.h` / `.cpp` — Individual server editor (name, URL, username, password, delete)
|
||||
|
||||
**Modified files:**
|
||||
- `src/network/HttpDownloader.h` / `.cpp` — Added per-call username/password parameters (default empty), removed global SETTINGS dependency
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.h` / `.cpp` — Constructor accepts `OpdsServer`, uses server-specific credentials and URL, shows server name in header
|
||||
- `src/activities/home/HomeActivity.h` / `.cpp` — `hasOpdsUrl` → `hasOpdsServers`, uses `OPDS_STORE.hasServers()`
|
||||
- `src/main.cpp` — Added OPDS_STORE loading on boot, server picker when multiple servers configured
|
||||
- `src/activities/settings/SettingsActivity.cpp` — Replaced CalibreSettingsActivity with OpdsServerListActivity
|
||||
- `src/network/CrossPointWebServer.h` / `.cpp` — Added REST endpoints: GET/POST /api/opds, POST /api/opds/delete
|
||||
- `src/network/html/SettingsPage.html` — Added OPDS server management UI (CSS + JS)
|
||||
- `src/SettingsList.h` — Removed legacy OPDS entries (opdsServerUrl, opdsUsername, opdsPassword)
|
||||
- 9 translation YAML files — Added STR_ADD_SERVER, STR_SERVER_NAME, STR_NO_SERVERS, STR_DELETE_SERVER, STR_DELETE_CONFIRM, STR_OPDS_SERVERS
|
||||
|
||||
**Deleted files:**
|
||||
- `src/activities/settings/CalibreSettingsActivity.h` / `.cpp`
|
||||
|
||||
## Key Adaptation Decisions
|
||||
- Kept `WiFiClient`/`WiFiClientSecure` naming (fork hasn't adopted `NetworkClient` rename)
|
||||
- Inlined JSON persistence and MAC-based obfuscation directly into `OpdsServerStore.cpp` (fork lacks `JsonSettingsIO` and `ObfuscationUtils` libraries)
|
||||
- Legacy single-server settings auto-migrate to new `opds.json` on first boot
|
||||
|
||||
## Build Status
|
||||
Compilation verified: SUCCESS (RAM: 32.8%, Flash: 71.4%)
|
||||
33
chat-summaries/2026-03-02_00-00-summary.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# OPDS Post-Download Prompt Screen
|
||||
|
||||
## Task
|
||||
After an OPDS download completes, instead of immediately returning to the catalog listing, show a prompt screen with two options: "Back to Listing" and "Open Book". A 5-second auto-timer executes the default action. A per-server setting controls which option is default (back to listing by default for backward compatibility).
|
||||
|
||||
## Changes Made
|
||||
|
||||
### New i18n strings
|
||||
- `lib/I18n/translations/english.yaml` — Added `STR_DOWNLOAD_COMPLETE`, `STR_OPEN_BOOK`, `STR_BACK_TO_LISTING`, `STR_AFTER_DOWNLOAD`
|
||||
- Regenerated `lib/I18n/I18nKeys.h`, `lib/I18n/I18nStrings.cpp` via `gen_i18n.py`
|
||||
|
||||
### Per-server setting
|
||||
- `src/OpdsServerStore.h` — Added `afterDownloadAction` field to `OpdsServer` (0 = back to listing, 1 = open book)
|
||||
- `src/OpdsServerStore.cpp` — Serialized/deserialized `after_download` in JSON
|
||||
|
||||
### Browser activity
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.h` — Added `DOWNLOAD_COMPLETE` state, `onGoToReader` callback, `downloadedFilePath`, `downloadCompleteTime`, `promptSelection` members, `executePromptAction()` method
|
||||
- `src/activities/browser/OpdsBookBrowserActivity.cpp` — On download success: transition to `DOWNLOAD_COMPLETE` state instead of `BROWSING`. Added loop handling (Left/Right to toggle selection, Confirm to execute, Back to go to listing, 5s auto-timer). Added render for prompt screen with bracketed selection UI. Added `executePromptAction()` helper.
|
||||
|
||||
### Callback threading
|
||||
- `src/main.cpp` — Passed `onGoToReader` callback to `OpdsBookBrowserActivity` constructor in both single-server and multi-server code paths
|
||||
|
||||
### Settings UI
|
||||
- `src/activities/settings/OpdsSettingsActivity.cpp` — Incremented `BASE_ITEMS` from 6 to 7. Added "After Download" field at index 6 (toggles between "Back to Listing" and "Open Book"). Shifted Delete to index 7.
|
||||
|
||||
### Countdown refinements (follow-up)
|
||||
- Added live countdown display using `FAST_REFRESH` -- shows "(5s)", "(4s)", etc., updating each second
|
||||
- Any button press (Left/Right to change selection, Back, Confirm) cancels the countdown entirely instead of resetting it
|
||||
- Added `countdownActive` bool and `lastCountdownSecond` int to track state without redundant redraws
|
||||
|
||||
## Follow-up Items
|
||||
- Translations for the 4 new strings in non-English languages
|
||||
- On-device testing of the full flow (download → prompt → open book / back to listing)
|
||||
18
chat-summaries/2026-03-02_06-48-summary.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# OPDS Per-Server Default Save Directory
|
||||
|
||||
## Task
|
||||
Add a configurable default download path per OPDS server. The directory picker now starts at the server's saved path instead of always at root. New servers default to "/".
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Modified files
|
||||
- **`src/OpdsServerStore.h`** -- Added `downloadPath` field (default `"/"`) to `OpdsServer` struct.
|
||||
- **`src/OpdsServerStore.cpp`** -- Serialize/deserialize `download_path` in JSON. Existing servers without the field default to `"/"`.
|
||||
- **`src/activities/settings/OpdsSettingsActivity.cpp`** -- Added "Download Path" as field index 4 (shifted Delete to 5). Uses `DirectoryPickerActivity` as subactivity for folder selection. Displays current path in row value.
|
||||
- **`src/activities/util/DirectoryPickerActivity.h`** / **`.cpp`** -- Added `initialPath` constructor parameter (default `"/"`). `onEnter()` validates the path exists on disk and falls back to `"/"` if not.
|
||||
- **`src/activities/browser/OpdsBookBrowserActivity.cpp`** -- Passes `server.downloadPath` as `initialPath` when launching the directory picker.
|
||||
- **`lib/I18n/translations/*.yaml`** (all 9 languages) -- Added `STR_DOWNLOAD_PATH` with translations.
|
||||
- **`lib/I18n/I18nKeys.h`**, **`I18nStrings.h`**, **`I18nStrings.cpp`** -- Regenerated.
|
||||
|
||||
## Follow-up Items
|
||||
- Test on device: verify settings UI shows/saves path, picker opens at saved path, fallback works when directory is removed
|
||||
46
chat-summaries/2026-03-02_12-00-summary.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# OPDS Server Reordering
|
||||
|
||||
## Task
|
||||
Add the ability to reorder OPDS servers via a `sortOrder` field, editable on-device and with up/down buttons in the web UI.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Data Model (`src/OpdsServerStore.h`)
|
||||
- Added `int sortOrder = 0` field to `OpdsServer` struct
|
||||
|
||||
### Store Logic (`src/OpdsServerStore.cpp`)
|
||||
- `saveToFile()`: persists `sort_order` to JSON
|
||||
- `loadFromFile()`: reads `sort_order`, assigns sequential defaults to servers missing it, sorts after load
|
||||
- `migrateFromSettings()`: assigns `sortOrder = 1` to migrated server
|
||||
- `addServer()`: auto-assigns `sortOrder = max(existing) + 1` when left at 0
|
||||
- `updateServer()`: re-sorts after update
|
||||
- Added `sortServers()` private helper: sorts by sortOrder ascending, ties broken case-insensitively by name (falling back to URL)
|
||||
- Added `moveServer(index, direction)`: swaps sortOrder with adjacent server, re-sorts, saves
|
||||
|
||||
### On-Device UI (`src/activities/settings/OpdsSettingsActivity.cpp`)
|
||||
- Added "Position" as first menu item (index 0), shifting all others by 1
|
||||
- Uses `NumericStepperActivity` (new) for position editing: numeric stepper with Up/Down and PageForward/PageBack to increment/decrement
|
||||
- `saveServer()` now re-locates the server by name+url after sort to keep `serverIndex` valid
|
||||
|
||||
### NumericStepperActivity (new: `src/activities/util/NumericStepperActivity.{h,cpp}`)
|
||||
- Reusable numeric stepper modeled after `SetTimezoneOffsetActivity`
|
||||
- Displays value with highlight rect and up/down arrow indicators
|
||||
- Up/PageForward increment, Down/PageBack decrement (clamped to min/max)
|
||||
- Confirm saves, Back cancels
|
||||
- Side button hints show +/-
|
||||
|
||||
### Web API (`src/network/CrossPointWebServer.{h,cpp}`)
|
||||
- `GET /api/opds`: now includes `sortOrder` in response
|
||||
- `POST /api/opds`: preserves `downloadPath` and `sortOrder` on update
|
||||
- New `POST /api/opds/reorder`: accepts `{index, direction: "up"|"down"}`, calls `moveServer()`
|
||||
|
||||
### Web UI (`src/network/html/SettingsPage.html`)
|
||||
- Added up/down arrow buttons to each OPDS server card (hidden at boundaries)
|
||||
- Added `reorderOpdsServer()` JS function calling the new API endpoint
|
||||
|
||||
### i18n
|
||||
- Added `STR_POSITION` to all 9 translation YAML files
|
||||
- Regenerated `I18nKeys.h`, `I18nStrings.h`, `I18nStrings.cpp`
|
||||
|
||||
## Follow-up Items
|
||||
- None identified; build passes cleanly.
|
||||
35
chat-summaries/2026-03-02_21-00-summary.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Port Upstream KOReader Sync PRs
|
||||
|
||||
## Task
|
||||
Port three unmerged upstream PRs into the fork:
|
||||
- PR #1185: Cache KOReader document hash
|
||||
- PR #1217: Proper KOReader XPath synchronisation
|
||||
- PR #1090: Push progress and sleep (with silent failure adaptation)
|
||||
|
||||
## Changes Made
|
||||
|
||||
### PR #1185 — Cache KOReader Document Hash
|
||||
- **lib/KOReaderSync/KOReaderDocumentId.h**: Added private static helpers `getCacheFilePath`, `loadCachedHash`, `saveCachedHash`
|
||||
- **lib/KOReaderSync/KOReaderDocumentId.cpp**: Cache lookup before hash computation, persist after; uses mtime fingerprint + file size for validation
|
||||
|
||||
### PR #1217 — Proper KOReader XPath Synchronisation
|
||||
- **lib/KOReaderSync/ChapterXPathIndexer.h/.cpp**: New files — Expat-based on-demand XHTML parsing for bidirectional XPath/progress mapping
|
||||
- **lib/KOReaderSync/ProgressMapper.h/.cpp**: XPath-first mapping in both directions, percentage fallback, DocFragment 1-based indexing fix, `std::clamp` sanitization
|
||||
- **docs/contributing/koreader-sync-xpath-mapping.md**: Design doc
|
||||
|
||||
### PR #1090 — Push Progress & Sleep (adapted)
|
||||
Adapted to fork's `ActivityWithSubactivity` + callback architecture (upstream uses `Activity` + `startActivityForResult`).
|
||||
|
||||
- **lib/I18n/translations/english.yaml** + auto-generated **I18nKeys.h**: Added `STR_PUSH_AND_SLEEP`
|
||||
- **src/activities/reader/EpubReaderMenuActivity.h**: Added `PUSH_AND_SLEEP` to `MenuAction` enum and `buildMenuItems()`
|
||||
- **src/activities/reader/KOReaderSyncActivity.h/.cpp**: Added `SyncMode::PUSH_ONLY`, `deferFinish()` mechanism, PUSH_ONLY paths in `performSync`/`performUpload`/`loop`
|
||||
- **src/activities/reader/EpubReaderActivity.h/.cpp**: Added `pendingSleep` flag, `extern void enterDeepSleep()`, `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`
|
||||
|
||||
**Silent failure**: Both `onCancel` and `onSyncComplete` callbacks set `pendingSleep = true`, so the device sleeps regardless of sync success/failure. No credentials also triggers sleep directly.
|
||||
|
||||
## Build
|
||||
Compiles cleanly on `default` environment (ESP32-C3). RAM: 32.8%, Flash: 71.7%.
|
||||
|
||||
## Follow-up Items
|
||||
- Changes are unstaged — commit when ready
|
||||
- Other language YAML files will auto-fallback to English for `STR_PUSH_AND_SLEEP`
|
||||
99
docs/contributing/koreader-sync-xpath-mapping.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# KOReader Sync XPath Mapping
|
||||
|
||||
This note documents how CrossPoint maps reading positions to and from KOReader sync payloads.
|
||||
|
||||
## Problem
|
||||
|
||||
CrossPoint internally stores position as:
|
||||
|
||||
- `spineIndex` (chapter index, 0-based)
|
||||
- `pageNumber` + `totalPages`
|
||||
|
||||
KOReader sync payload stores:
|
||||
|
||||
- `progress` (XPath-like location)
|
||||
- `percentage` (overall progress)
|
||||
|
||||
A direct 1:1 mapping is not guaranteed because page layout differs between engines/devices.
|
||||
|
||||
## DocFragment Index Convention
|
||||
|
||||
KOReader uses **1-based** XPath predicates throughout, following standard XPath conventions.
|
||||
The first EPUB spine item is `DocFragment[1]`, the second is `DocFragment[2]`, and so on.
|
||||
|
||||
CrossPoint stores spine items as 0-based indices internally. The conversion is:
|
||||
|
||||
- **Generating XPath (to KOReader):** `DocFragment[spineIndex + 1]`
|
||||
- **Parsing XPath (from KOReader):** `spineIndex = DocFragment[N] - 1`
|
||||
|
||||
Reference: [koreader/koreader#11585](https://github.com/koreader/koreader/issues/11585) confirms this
|
||||
via a KOReader contributor mapping spine items to DocFragment numbers.
|
||||
|
||||
## Current Strategy
|
||||
|
||||
### CrossPoint -> KOReader
|
||||
|
||||
Implemented in `ProgressMapper::toKOReader`.
|
||||
|
||||
1. Compute overall `percentage` from chapter/page.
|
||||
2. Attempt to compute a real element-level XPath via `ChapterXPathIndexer::findXPathForProgress`.
|
||||
3. If XPath extraction fails, fallback to synthetic chapter path:
|
||||
- `/body/DocFragment[spineIndex + 1]/body`
|
||||
|
||||
### KOReader -> CrossPoint
|
||||
|
||||
Implemented in `ProgressMapper::toCrossPoint`.
|
||||
|
||||
1. Attempt to parse `DocFragment[N]` from incoming XPath; convert N to 0-based `spineIndex = N - 1`.
|
||||
2. If valid, attempt XPath-to-offset mapping via `ChapterXPathIndexer::findProgressForXPath`.
|
||||
3. Convert resolved intra-spine progress to page estimate.
|
||||
4. If XPath path is invalid/unresolvable, fallback to percentage-based chapter/page estimation.
|
||||
|
||||
## ChapterXPathIndexer Design
|
||||
|
||||
The module reparses **one spine XHTML** on demand using Expat and builds temporary anchors:
|
||||
|
||||
Source-of-truth note: XPath anchors are built from the original EPUB spine XHTML bytes (zip item contents), not from CrossPoint's distilled section render cache. This is intentional to preserve KOReader XPath compatibility.
|
||||
|
||||
- anchor: `<xpath, textOffset>`
|
||||
- `textOffset` counts non-whitespace bytes
|
||||
- When multiple anchors exist for the same path, the one with the **smallest** textOffset is used
|
||||
(start of element), not the latest periodic anchor.
|
||||
|
||||
Forward lookup (CrossPoint → XPath): uses `upper_bound` to find the last anchor at or before the
|
||||
target text offset, ensuring the returned XPath corresponds to the element the user is currently
|
||||
inside rather than the next element.
|
||||
|
||||
Matching for reverse lookup:
|
||||
|
||||
1. exact path match — reported as `exact=yes`
|
||||
2. index-insensitive path match (`div[2]` vs `div[3]` tolerated) — reported as `exact=no`
|
||||
3. ancestor fallback — reported as `exact=no`
|
||||
|
||||
If no match is found, caller must fallback to percentage.
|
||||
|
||||
## Memory / Safety Constraints (ESP32-C3)
|
||||
|
||||
The implementation intentionally avoids full DOM storage.
|
||||
|
||||
- Parse one chapter only.
|
||||
- Keep anchors in transient vectors only for duration of call.
|
||||
- Free XML parser and chapter byte buffer on all success/failure paths.
|
||||
- No persistent cache structures are introduced by this module.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Page number on reverse mapping is still an estimate (renderer differences).
|
||||
- XPath mapping intentionally uses original spine XHTML while pagination comes from distilled renderer output, so minor roundtrip page drift is expected.
|
||||
- Image-only/low-text chapters may yield coarse anchors.
|
||||
- Extremely malformed XHTML can force fallback behavior.
|
||||
|
||||
## Operational Logging
|
||||
|
||||
`ProgressMapper` logs mapping source in reverse direction:
|
||||
|
||||
- `xpath` when XPath mapping path was used
|
||||
- `percentage` when fallback path was used
|
||||
|
||||
It also logs exactness (`exact=yes/no`) for XPath matches. Note that `exact=yes` is only set for
|
||||
a full path match with correct indices; index-insensitive and ancestor matches always log `exact=no`.
|
||||
29
lib/Epub/Epub/TableData.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "ParsedText.h"
|
||||
#include "css/CssStyle.h"
|
||||
|
||||
/// A single cell in a table row.
|
||||
struct TableCell {
|
||||
std::unique_ptr<ParsedText> content;
|
||||
bool isHeader = false; // true for <th>, false for <td>
|
||||
int colspan = 1; // number of logical columns this cell spans
|
||||
CssLength widthHint; // width hint from HTML attribute or CSS (if hasWidthHint)
|
||||
bool hasWidthHint = false;
|
||||
};
|
||||
|
||||
/// A single row in a table.
|
||||
struct TableRow {
|
||||
std::vector<TableCell> cells;
|
||||
};
|
||||
|
||||
/// Buffered table data collected during SAX parsing.
|
||||
/// The entire table must be buffered before layout because column widths
|
||||
/// depend on content across all rows.
|
||||
struct TableData {
|
||||
std::vector<TableRow> rows;
|
||||
std::vector<CssLength> colWidthHints; // width hints from <col> tags, indexed by logical column
|
||||
};
|
||||
497
lib/KOReaderSync/ChapterXPathIndexer.cpp
Normal file
@@ -0,0 +1,497 @@
|
||||
#include "ChapterXPathIndexer.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <expat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
// Anchor used for both mapping directions.
|
||||
// textOffset is counted as visible (non-whitespace) bytes from chapter start.
|
||||
// xpath points to the nearest element path at/near that offset.
|
||||
|
||||
struct XPathAnchor {
|
||||
size_t textOffset = 0;
|
||||
std::string xpath;
|
||||
std::string xpathNoIndex; // precomputed removeIndices(xpath)
|
||||
};
|
||||
|
||||
struct StackNode {
|
||||
std::string tag;
|
||||
int index = 1;
|
||||
bool hasTextAnchor = false;
|
||||
};
|
||||
|
||||
// ParserState is intentionally ephemeral and created per lookup call.
|
||||
// It holds only one spine parse worth of data to avoid retaining structures
|
||||
// that would increase long-lived heap usage on the ESP32-C3.
|
||||
struct ParserState {
|
||||
explicit ParserState(const int spineIndex) : spineIndex(spineIndex) { siblingCounters.emplace_back(); }
|
||||
|
||||
int spineIndex = 0;
|
||||
int skipDepth = -1;
|
||||
size_t totalTextBytes = 0;
|
||||
|
||||
std::vector<StackNode> stack;
|
||||
std::vector<std::unordered_map<std::string, int>> siblingCounters;
|
||||
std::vector<XPathAnchor> anchors;
|
||||
|
||||
std::string baseXPath() const { return "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body"; }
|
||||
|
||||
// Canonicalize incoming KOReader XPath before matching:
|
||||
// - remove all whitespace
|
||||
// - lowercase tags
|
||||
// - strip optional trailing /text()
|
||||
// - strip trailing slash
|
||||
static std::string normalizeXPath(const std::string& input) {
|
||||
if (input.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string out;
|
||||
out.reserve(input.size());
|
||||
for (char c : input) {
|
||||
const unsigned char uc = static_cast<unsigned char>(c);
|
||||
if (std::isspace(uc)) {
|
||||
continue;
|
||||
}
|
||||
out.push_back(static_cast<char>(std::tolower(uc)));
|
||||
}
|
||||
|
||||
const std::string textSuffix = "/text()";
|
||||
const size_t textPos = out.rfind(textSuffix);
|
||||
if (textPos != std::string::npos && textPos + textSuffix.size() == out.size()) {
|
||||
out.erase(textPos);
|
||||
}
|
||||
|
||||
while (!out.empty() && out.back() == '/') {
|
||||
out.pop_back();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// Remove bracketed numeric predicates so paths can be compared even when
|
||||
// index counters differ between parser implementations.
|
||||
static std::string removeIndices(const std::string& xpath) {
|
||||
std::string out;
|
||||
out.reserve(xpath.size());
|
||||
|
||||
bool inBracket = false;
|
||||
for (char c : xpath) {
|
||||
if (c == '[') {
|
||||
inBracket = true;
|
||||
continue;
|
||||
}
|
||||
if (c == ']') {
|
||||
inBracket = false;
|
||||
continue;
|
||||
}
|
||||
if (!inBracket) {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static int pathDepth(const std::string& xpath) {
|
||||
int depth = 0;
|
||||
for (char c : xpath) {
|
||||
if (c == '/') {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
// Resolve a path to the best anchor offset.
|
||||
// If exact node path is not found, progressively trim trailing segments and
|
||||
// match ancestors to obtain a stable approximate location.
|
||||
bool pickBestAnchorByPath(const std::string& targetPath, const bool ignoreIndices, size_t& outTextOffset,
|
||||
bool& outExact) const {
|
||||
if (targetPath.empty() || anchors.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string normalizedTarget = ignoreIndices ? removeIndices(targetPath) : targetPath;
|
||||
std::string probe = normalizedTarget;
|
||||
bool exactProbe = true;
|
||||
|
||||
while (!probe.empty()) {
|
||||
int bestDepth = -1;
|
||||
size_t bestOffset = 0;
|
||||
bool found = false;
|
||||
|
||||
for (const auto& anchor : anchors) {
|
||||
const std::string& anchorPath = ignoreIndices ? anchor.xpathNoIndex : anchor.xpath;
|
||||
if (anchorPath == probe) {
|
||||
const int depth = pathDepth(anchorPath);
|
||||
if (!found || depth > bestDepth || (depth == bestDepth && anchor.textOffset < bestOffset)) {
|
||||
found = true;
|
||||
bestDepth = depth;
|
||||
bestOffset = anchor.textOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
outTextOffset = bestOffset;
|
||||
outExact = exactProbe;
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t lastSlash = probe.find_last_of('/');
|
||||
if (lastSlash == std::string::npos || lastSlash == 0) {
|
||||
break;
|
||||
}
|
||||
probe.erase(lastSlash);
|
||||
exactProbe = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::string toLower(std::string value) {
|
||||
for (char& c : value) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Elements that should not contribute text position anchors.
|
||||
static bool isSkippableTag(const std::string& tag) { return tag == "head" || tag == "script" || tag == "style"; }
|
||||
|
||||
static bool isWhitespaceOnly(const XML_Char* text, const int len) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (!std::isspace(static_cast<unsigned char>(text[i]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Count non-whitespace bytes to keep offsets stable against formatting-only
|
||||
// differences and indentation in source XHTML.
|
||||
static size_t countVisibleBytes(const XML_Char* text, const int len) {
|
||||
size_t count = 0;
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (!std::isspace(static_cast<unsigned char>(text[i]))) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int bodyDepth() const {
|
||||
for (int i = static_cast<int>(stack.size()) - 1; i >= 0; i--) {
|
||||
if (stack[i].tag == "body") {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool insideBody() const { return bodyDepth() >= 0; }
|
||||
|
||||
std::string currentXPath() const {
|
||||
const int bodyIdx = bodyDepth();
|
||||
if (bodyIdx < 0) {
|
||||
return baseXPath();
|
||||
}
|
||||
|
||||
std::string xpath = baseXPath();
|
||||
for (size_t i = static_cast<size_t>(bodyIdx + 1); i < stack.size(); i++) {
|
||||
xpath += "/" + stack[i].tag + "[" + std::to_string(stack[i].index) + "]";
|
||||
}
|
||||
return xpath;
|
||||
}
|
||||
|
||||
// Adds first anchor for an element when text begins and periodic anchors in
|
||||
// longer runs so matching has sufficient granularity without exploding memory.
|
||||
void addAnchorIfNeeded() {
|
||||
if (!insideBody() || stack.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stack.back().hasTextAnchor) {
|
||||
const std::string xpath = currentXPath();
|
||||
anchors.push_back({totalTextBytes, xpath, removeIndices(xpath)});
|
||||
stack.back().hasTextAnchor = true;
|
||||
} else if (anchors.empty() || totalTextBytes - anchors.back().textOffset >= 192) {
|
||||
const std::string xpath = currentXPath();
|
||||
if (anchors.empty() || anchors.back().xpath != xpath) {
|
||||
anchors.push_back({totalTextBytes, xpath, removeIndices(xpath)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onStartElement(const XML_Char* rawName) {
|
||||
std::string name = toLower(rawName ? rawName : "");
|
||||
const size_t depth = stack.size();
|
||||
|
||||
if (siblingCounters.size() <= depth) {
|
||||
siblingCounters.resize(depth + 1);
|
||||
}
|
||||
const int siblingIndex = ++siblingCounters[depth][name];
|
||||
|
||||
stack.push_back({name, siblingIndex, false});
|
||||
siblingCounters.emplace_back();
|
||||
|
||||
if (skipDepth < 0 && isSkippableTag(name)) {
|
||||
skipDepth = static_cast<int>(stack.size()) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void onEndElement() {
|
||||
if (stack.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (skipDepth == static_cast<int>(stack.size()) - 1) {
|
||||
skipDepth = -1;
|
||||
}
|
||||
|
||||
stack.pop_back();
|
||||
if (!siblingCounters.empty()) {
|
||||
siblingCounters.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void onCharacterData(const XML_Char* text, const int len) {
|
||||
if (skipDepth >= 0 || len <= 0 || !insideBody() || isWhitespaceOnly(text, len)) {
|
||||
return;
|
||||
}
|
||||
|
||||
addAnchorIfNeeded();
|
||||
totalTextBytes += countVisibleBytes(text, len);
|
||||
}
|
||||
|
||||
std::string chooseXPath(const float intraSpineProgress) const {
|
||||
if (anchors.empty()) {
|
||||
return baseXPath();
|
||||
}
|
||||
if (totalTextBytes == 0) {
|
||||
return anchors.front().xpath;
|
||||
}
|
||||
|
||||
const float clampedProgress = std::max(0.0f, std::min(1.0f, intraSpineProgress));
|
||||
const size_t target = static_cast<size_t>(clampedProgress * static_cast<float>(totalTextBytes));
|
||||
|
||||
// upper_bound returns the first anchor strictly after target; step back to get
|
||||
// the last anchor at-or-before target (the element the user is currently inside).
|
||||
auto it = std::upper_bound(anchors.begin(), anchors.end(), target,
|
||||
[](const size_t value, const XPathAnchor& anchor) { return value < anchor.textOffset; });
|
||||
if (it != anchors.begin()) {
|
||||
--it;
|
||||
}
|
||||
return it->xpath;
|
||||
}
|
||||
|
||||
// Convert path -> progress ratio by matching to nearest available anchor.
|
||||
bool chooseProgressForXPath(const std::string& xpath, float& outIntraSpineProgress, bool& outExactMatch) const {
|
||||
if (anchors.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string normalized = normalizeXPath(xpath);
|
||||
if (normalized.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t matchedOffset = 0;
|
||||
bool exact = false;
|
||||
const char* matchTier = nullptr;
|
||||
|
||||
bool matched = pickBestAnchorByPath(normalized, false, matchedOffset, exact);
|
||||
if (matched) {
|
||||
matchTier = exact ? "exact" : "ancestor";
|
||||
} else {
|
||||
bool exactRaw = false;
|
||||
matched = pickBestAnchorByPath(normalized, true, matchedOffset, exactRaw);
|
||||
if (matched) {
|
||||
exact = false;
|
||||
matchTier = exactRaw ? "index-insensitive" : "index-insensitive-ancestor";
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
LOG_DBG("KOX", "Reverse: spine=%d no anchor match for '%s' (%zu anchors)", spineIndex, normalized.c_str(),
|
||||
anchors.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
outExactMatch = exact;
|
||||
if (totalTextBytes == 0) {
|
||||
outIntraSpineProgress = 0.0f;
|
||||
LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu -> progress=0.0 (no text)", spineIndex, matchTier,
|
||||
matchedOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
outIntraSpineProgress = static_cast<float>(matchedOffset) / static_cast<float>(totalTextBytes);
|
||||
outIntraSpineProgress = std::max(0.0f, std::min(1.0f, outIntraSpineProgress));
|
||||
LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f", spineIndex, matchTier, matchedOffset,
|
||||
totalTextBytes, outIntraSpineProgress);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
void XMLCALL onStartElement(void* userData, const XML_Char* name, const XML_Char**) {
|
||||
auto* state = static_cast<ParserState*>(userData);
|
||||
state->onStartElement(name);
|
||||
}
|
||||
|
||||
void XMLCALL onEndElement(void* userData, const XML_Char*) {
|
||||
auto* state = static_cast<ParserState*>(userData);
|
||||
state->onEndElement();
|
||||
}
|
||||
|
||||
void XMLCALL onCharacterData(void* userData, const XML_Char* text, const int len) {
|
||||
auto* state = static_cast<ParserState*>(userData);
|
||||
state->onCharacterData(text, len);
|
||||
}
|
||||
|
||||
void XMLCALL onDefaultHandlerExpand(void* userData, const XML_Char* text, const int len) {
|
||||
// The default handler fires for comments, PIs, DOCTYPE, and entity references.
|
||||
// Only forward entity references (&..;) to avoid skewing text offsets with
|
||||
// non-visible markup.
|
||||
if (len < 3 || text[0] != '&' || text[len - 1] != ';') {
|
||||
return;
|
||||
}
|
||||
for (int i = 1; i < len - 1; ++i) {
|
||||
if (text[i] == '<' || text[i] == '>') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto* state = static_cast<ParserState*>(userData);
|
||||
state->onCharacterData(text, len);
|
||||
}
|
||||
|
||||
// Parse one spine item and return a fully populated ParserState.
|
||||
// Returns std::nullopt if validation, I/O, or XML parse fails.
|
||||
static std::optional<ParserState> parseSpineItem(const std::shared_ptr<Epub>& epub, const int spineIndex) {
|
||||
if (!epub || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto spineItem = epub->getSpineItem(spineIndex);
|
||||
if (spineItem.href.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t chapterSize = 0;
|
||||
uint8_t* chapterBytes = epub->readItemContentsToBytes(spineItem.href, &chapterSize, false);
|
||||
if (!chapterBytes || chapterSize == 0) {
|
||||
free(chapterBytes);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ParserState state(spineIndex);
|
||||
|
||||
XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
free(chapterBytes);
|
||||
LOG_ERR("KOX", "Failed to allocate XML parser for spine=%d", spineIndex);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, &state);
|
||||
XML_SetElementHandler(parser, onStartElement, onEndElement);
|
||||
XML_SetCharacterDataHandler(parser, onCharacterData);
|
||||
XML_SetDefaultHandlerExpand(parser, onDefaultHandlerExpand);
|
||||
|
||||
const bool parseOk = XML_Parse(parser, reinterpret_cast<const char*>(chapterBytes), static_cast<int>(chapterSize),
|
||||
XML_TRUE) != XML_STATUS_ERROR;
|
||||
|
||||
if (!parseOk) {
|
||||
LOG_ERR("KOX", "XPath parse failed for spine=%d at line %lu: %s", spineIndex, XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
}
|
||||
|
||||
XML_ParserFree(parser);
|
||||
free(chapterBytes);
|
||||
|
||||
if (!parseOk) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ChapterXPathIndexer::findXPathForProgress(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||
const float intraSpineProgress) {
|
||||
const auto state = parseSpineItem(epub, spineIndex);
|
||||
if (!state) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string result = state->chooseXPath(intraSpineProgress);
|
||||
LOG_DBG("KOX", "Forward: spine=%d progress=%.3f anchors=%zu textBytes=%zu -> %s", spineIndex, intraSpineProgress,
|
||||
state->anchors.size(), state->totalTextBytes, result.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ChapterXPathIndexer::findProgressForXPath(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||
const std::string& xpath, float& outIntraSpineProgress,
|
||||
bool& outExactMatch) {
|
||||
outIntraSpineProgress = 0.0f;
|
||||
outExactMatch = false;
|
||||
|
||||
if (xpath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto state = parseSpineItem(epub, spineIndex);
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("KOX", "Reverse: spine=%d anchors=%zu textBytes=%zu for '%s'", spineIndex, state->anchors.size(),
|
||||
state->totalTextBytes, xpath.c_str());
|
||||
return state->chooseProgressForXPath(xpath, outIntraSpineProgress, outExactMatch);
|
||||
}
|
||||
|
||||
bool ChapterXPathIndexer::tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex) {
|
||||
outSpineIndex = -1;
|
||||
if (xpath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string normalized = ParserState::normalizeXPath(xpath);
|
||||
const std::string key = "/docfragment[";
|
||||
const size_t pos = normalized.find(key);
|
||||
if (pos == std::string::npos) {
|
||||
LOG_DBG("KOX", "No DocFragment in xpath: '%s'", xpath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t start = pos + key.size();
|
||||
size_t end = start;
|
||||
while (end < normalized.size() && std::isdigit(static_cast<unsigned char>(normalized[end]))) {
|
||||
end++;
|
||||
}
|
||||
|
||||
if (end == start || end >= normalized.size() || normalized[end] != ']') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string value = normalized.substr(start, end - start);
|
||||
const long parsed = std::strtol(value.c_str(), nullptr, 10);
|
||||
// KOReader uses 1-based DocFragment indices; convert to 0-based spine index.
|
||||
if (parsed < 1 || parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outSpineIndex = static_cast<int>(parsed) - 1;
|
||||
return true;
|
||||
}
|
||||
67
lib/KOReaderSync/ChapterXPathIndexer.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <Epub.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Lightweight XPath/progress bridge for KOReader sync.
|
||||
*
|
||||
* Why this exists:
|
||||
* - CrossPoint stores reading position as chapter/page.
|
||||
* - KOReader sync uses XPath + percentage.
|
||||
*
|
||||
* This utility reparses exactly one spine XHTML item with Expat and builds
|
||||
* transient text anchors (<xpath, textOffset>) so we can translate in both
|
||||
* directions without keeping a full DOM in memory.
|
||||
*
|
||||
* Design constraints (ESP32-C3):
|
||||
* - No persistent full-book structures.
|
||||
* - Parse-on-demand and free memory immediately.
|
||||
* - Keep fallback behavior deterministic if parsing/matching fails.
|
||||
*/
|
||||
class ChapterXPathIndexer {
|
||||
public:
|
||||
/**
|
||||
* Convert an intra-spine progress ratio to the nearest element-level XPath.
|
||||
*
|
||||
* @param epub Loaded EPUB instance
|
||||
* @param spineIndex Current spine item index
|
||||
* @param intraSpineProgress Position within the spine item [0.0, 1.0]
|
||||
* @return Best matching XPath for KOReader, or empty string on failure
|
||||
*/
|
||||
static std::string findXPathForProgress(const std::shared_ptr<Epub>& epub, int spineIndex, float intraSpineProgress);
|
||||
|
||||
/**
|
||||
* Resolve a KOReader XPath to an intra-spine progress ratio.
|
||||
*
|
||||
* Matching strategy:
|
||||
* 1) exact anchor path match,
|
||||
* 2) index-insensitive path match,
|
||||
* 3) ancestor fallback.
|
||||
*
|
||||
* @param epub Loaded EPUB instance
|
||||
* @param spineIndex Spine item index to parse
|
||||
* @param xpath Incoming KOReader XPath
|
||||
* @param outIntraSpineProgress Resolved position within spine [0.0, 1.0]
|
||||
* @param outExactMatch True only for full exact path match
|
||||
* @return true if any match was resolved; false means caller should fallback
|
||||
*/
|
||||
static bool findProgressForXPath(const std::shared_ptr<Epub>& epub, int spineIndex, const std::string& xpath,
|
||||
float& outIntraSpineProgress, bool& outExactMatch);
|
||||
|
||||
/**
|
||||
* Parse DocFragment index from KOReader-style path segment:
|
||||
* /body/DocFragment[N]/body/...
|
||||
*
|
||||
* KOReader uses 1-based DocFragment indices; N is converted to the 0-based
|
||||
* spine index stored in outSpineIndex (i.e. outSpineIndex = N - 1).
|
||||
*
|
||||
* @param xpath KOReader XPath
|
||||
* @param outSpineIndex 0-based spine index derived from DocFragment[N]
|
||||
* @return true when DocFragment[N] exists and N is a valid integer >= 1
|
||||
* (converted to 0-based outSpineIndex); false otherwise
|
||||
*/
|
||||
static bool tryExtractSpineIndexFromXPath(const std::string& xpath, int& outSpineIndex);
|
||||
};
|
||||
25
lib/PlaceholderCover/BookIcon.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Book icon: 48x48, 1-bit packed (MSB first)
|
||||
// 0 = black, 1 = white (same format as Logo120.h)
|
||||
static constexpr int BOOK_ICON_WIDTH = 48;
|
||||
static constexpr int BOOK_ICON_HEIGHT = 48;
|
||||
static const uint8_t BookIcon[] = {
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f,
|
||||
0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1c, 0x00, 0x00, 0x01, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1c, 0x00, 0x01, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1c, 0x00, 0x00, 0x1f, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0x9f,
|
||||
0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f,
|
||||
0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x3f,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
};
|
||||
474
lib/PlaceholderCover/PlaceholderCoverGenerator.cpp
Normal file
@@ -0,0 +1,474 @@
|
||||
#include "PlaceholderCoverGenerator.h"
|
||||
|
||||
#include <EpdFont.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
// Include the UI fonts directly for self-contained placeholder rendering.
|
||||
// These are 1-bit bitmap fonts compiled from Ubuntu TTF.
|
||||
#include "builtinFonts/ubuntu_10_regular.h"
|
||||
#include "builtinFonts/ubuntu_12_bold.h"
|
||||
|
||||
// Book icon bitmap (48x48 1-bit, generated by scripts/generate_book_icon.py)
|
||||
#include "BookIcon.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// BMP writing helpers (same format as JpegToBmpConverter)
|
||||
inline void write16(Print& out, const uint16_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32(Print& out, const uint32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
inline void write32Signed(Print& out, const int32_t value) {
|
||||
out.write(value & 0xFF);
|
||||
out.write((value >> 8) & 0xFF);
|
||||
out.write((value >> 16) & 0xFF);
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) {
|
||||
const int bytesPerRow = (width + 31) / 32 * 4;
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t fileSize = 62 + imageSize;
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize);
|
||||
write32(bmpOut, 0); // Reserved
|
||||
write32(bmpOut, 62); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height); // Negative = top-down
|
||||
write16(bmpOut, 1); // Color planes
|
||||
write16(bmpOut, 1); // Bits per pixel
|
||||
write32(bmpOut, 0); // BI_RGB
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835); // xPixelsPerMeter
|
||||
write32(bmpOut, 2835); // yPixelsPerMeter
|
||||
write32(bmpOut, 2); // colorsUsed
|
||||
write32(bmpOut, 2); // colorsImportant
|
||||
|
||||
// Palette: index 0 = black, index 1 = white
|
||||
const uint8_t palette[8] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Black
|
||||
0xFF, 0xFF, 0xFF, 0x00 // White
|
||||
};
|
||||
for (const uint8_t b : palette) {
|
||||
bmpOut.write(b);
|
||||
}
|
||||
}
|
||||
|
||||
/// 1-bit pixel buffer that can render text, icons, and shapes, then write as BMP.
|
||||
class PixelBuffer {
|
||||
public:
|
||||
PixelBuffer(int width, int height) : width(width), height(height) {
|
||||
bytesPerRow = (width + 31) / 32 * 4;
|
||||
bufferSize = bytesPerRow * height;
|
||||
buffer = static_cast<uint8_t*>(malloc(bufferSize));
|
||||
if (buffer) {
|
||||
memset(buffer, 0xFF, bufferSize); // White background
|
||||
}
|
||||
}
|
||||
|
||||
~PixelBuffer() {
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
bool isValid() const { return buffer != nullptr; }
|
||||
|
||||
/// Set a pixel to black.
|
||||
void setBlack(int x, int y) {
|
||||
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
||||
const int byteIndex = y * bytesPerRow + x / 8;
|
||||
const uint8_t bitMask = 0x80 >> (x % 8);
|
||||
buffer[byteIndex] &= ~bitMask;
|
||||
}
|
||||
|
||||
/// Set a scaled "pixel" (scale x scale block) to black.
|
||||
void setBlackScaled(int x, int y, int scale) {
|
||||
for (int dy = 0; dy < scale; dy++) {
|
||||
for (int dx = 0; dx < scale; dx++) {
|
||||
setBlack(x + dx, y + dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a filled rectangle in black.
|
||||
void fillRect(int x, int y, int w, int h) {
|
||||
for (int row = y; row < y + h && row < height; row++) {
|
||||
for (int col = x; col < x + w && col < width; col++) {
|
||||
setBlack(col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a rectangular border in black.
|
||||
void drawBorder(int x, int y, int w, int h, int thickness) {
|
||||
fillRect(x, y, w, thickness); // Top
|
||||
fillRect(x, y + h - thickness, w, thickness); // Bottom
|
||||
fillRect(x, y, thickness, h); // Left
|
||||
fillRect(x + w - thickness, y, thickness, h); // Right
|
||||
}
|
||||
|
||||
/// Draw a horizontal line in black with configurable thickness.
|
||||
void drawHLine(int x, int y, int length, int thickness = 1) { fillRect(x, y, length, thickness); }
|
||||
|
||||
/// Render a single glyph at (cursorX, baselineY) with integer scaling. Returns advance in X (scaled).
|
||||
int renderGlyph(const EpdFontData* font, uint32_t codepoint, int cursorX, int baselineY, int scale = 1) {
|
||||
const EpdFont fontObj(font);
|
||||
const EpdGlyph* glyph = fontObj.getGlyph(codepoint);
|
||||
if (!glyph) {
|
||||
glyph = fontObj.getGlyph(REPLACEMENT_GLYPH);
|
||||
}
|
||||
if (!glyph) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint8_t* bitmap = &font->bitmap[glyph->dataOffset];
|
||||
const int glyphW = glyph->width;
|
||||
const int glyphH = glyph->height;
|
||||
|
||||
for (int gy = 0; gy < glyphH; gy++) {
|
||||
const int screenY = baselineY - glyph->top * scale + gy * scale;
|
||||
for (int gx = 0; gx < glyphW; gx++) {
|
||||
const int pixelPos = gy * glyphW + gx;
|
||||
const int screenX = cursorX + glyph->left * scale + gx * scale;
|
||||
|
||||
bool isSet = false;
|
||||
if (font->is2Bit) {
|
||||
const uint8_t byte = bitmap[pixelPos / 4];
|
||||
const uint8_t bitIndex = (3 - pixelPos % 4) * 2;
|
||||
const uint8_t val = 3 - ((byte >> bitIndex) & 0x3);
|
||||
isSet = (val < 3);
|
||||
} else {
|
||||
const uint8_t byte = bitmap[pixelPos / 8];
|
||||
const uint8_t bitIndex = 7 - (pixelPos % 8);
|
||||
isSet = ((byte >> bitIndex) & 1);
|
||||
}
|
||||
|
||||
if (isSet) {
|
||||
setBlackScaled(screenX, screenY, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return glyph->advanceX * scale;
|
||||
}
|
||||
|
||||
/// Render a UTF-8 string at (x, y) where y is the top of the text line, with integer scaling.
|
||||
void drawText(const EpdFontData* font, int x, int y, const char* text, int scale = 1) {
|
||||
const int baselineY = y + font->ascender * scale;
|
||||
int cursorX = x;
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
cursorX += renderGlyph(font, cp, cursorX, baselineY, scale);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a 1-bit icon bitmap (MSB first, 0=black, 1=white) with integer scaling.
|
||||
void drawIcon(const uint8_t* icon, int iconW, int iconH, int x, int y, int scale = 1) {
|
||||
const int bytesPerIconRow = iconW / 8;
|
||||
for (int iy = 0; iy < iconH; iy++) {
|
||||
for (int ix = 0; ix < iconW; ix++) {
|
||||
const int byteIdx = iy * bytesPerIconRow + ix / 8;
|
||||
const uint8_t bitMask = 0x80 >> (ix % 8);
|
||||
// In the icon data: 0 = black (drawn), 1 = white (skip)
|
||||
if (!(icon[byteIdx] & bitMask)) {
|
||||
const int sx = x + ix * scale;
|
||||
const int sy = y + iy * scale;
|
||||
setBlackScaled(sx, sy, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the pixel buffer to a file as a 1-bit BMP.
|
||||
bool writeBmp(Print& out) const {
|
||||
if (!buffer) return false;
|
||||
writeBmpHeader1bit(out, width, height);
|
||||
out.write(buffer, bufferSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
|
||||
private:
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
size_t bufferSize;
|
||||
uint8_t* buffer;
|
||||
};
|
||||
|
||||
/// Measure the width of a UTF-8 string in pixels (at 1x scale).
|
||||
int measureTextWidth(const EpdFontData* font, const char* text) {
|
||||
const EpdFont fontObj(font);
|
||||
int w = 0, h = 0;
|
||||
fontObj.getTextDimensions(text, &w, &h);
|
||||
return w;
|
||||
}
|
||||
|
||||
/// Get the advance width of a single character.
|
||||
int getCharAdvance(const EpdFontData* font, uint32_t cp) {
|
||||
const EpdFont fontObj(font);
|
||||
const EpdGlyph* glyph = fontObj.getGlyph(cp);
|
||||
if (!glyph) return 0;
|
||||
return glyph->advanceX;
|
||||
}
|
||||
|
||||
/// Split a string into words (splitting on spaces).
|
||||
std::vector<std::string> splitWords(const std::string& text) {
|
||||
std::vector<std::string> words;
|
||||
std::string current;
|
||||
for (size_t i = 0; i < text.size(); i++) {
|
||||
if (text[i] == ' ') {
|
||||
if (!current.empty()) {
|
||||
words.push_back(current);
|
||||
current.clear();
|
||||
}
|
||||
} else {
|
||||
current += text[i];
|
||||
}
|
||||
}
|
||||
if (!current.empty()) {
|
||||
words.push_back(current);
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
/// Word-wrap text into lines that fit within maxWidth pixels at the given scale.
|
||||
std::vector<std::string> wrapText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
|
||||
std::vector<std::string> lines;
|
||||
const auto words = splitWords(text);
|
||||
if (words.empty()) return lines;
|
||||
|
||||
const int spaceWidth = getCharAdvance(font, ' ') * scale;
|
||||
std::string currentLine;
|
||||
int currentWidth = 0;
|
||||
|
||||
for (const auto& word : words) {
|
||||
const int wordWidth = measureTextWidth(font, word.c_str()) * scale;
|
||||
|
||||
if (currentLine.empty()) {
|
||||
currentLine = word;
|
||||
currentWidth = wordWidth;
|
||||
} else if (currentWidth + spaceWidth + wordWidth <= maxWidth) {
|
||||
currentLine += " " + word;
|
||||
currentWidth += spaceWidth + wordWidth;
|
||||
} else {
|
||||
lines.push_back(currentLine);
|
||||
currentLine = word;
|
||||
currentWidth = wordWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentLine.empty()) {
|
||||
lines.push_back(currentLine);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// Truncate a string with "..." if it exceeds maxWidth pixels at the given scale.
|
||||
std::string truncateText(const EpdFontData* font, const std::string& text, int maxWidth, int scale = 1) {
|
||||
if (measureTextWidth(font, text.c_str()) * scale <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string truncated = text;
|
||||
const char* ellipsis = "...";
|
||||
const int ellipsisWidth = measureTextWidth(font, ellipsis) * scale;
|
||||
|
||||
while (!truncated.empty()) {
|
||||
utf8RemoveLastChar(truncated);
|
||||
if (measureTextWidth(font, truncated.c_str()) * scale + ellipsisWidth <= maxWidth) {
|
||||
return truncated + ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
return ellipsis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PlaceholderCoverGenerator::generate(const std::string& outputPath, const std::string& title,
|
||||
const std::string& author, int width, int height) {
|
||||
LOG_DBG("PHC", "Generating placeholder cover %dx%d: \"%s\" by \"%s\"", width, height, title.c_str(), author.c_str());
|
||||
|
||||
const EpdFontData* titleFont = &ubuntu_12_bold;
|
||||
const EpdFontData* authorFont = &ubuntu_10_regular;
|
||||
|
||||
PixelBuffer buf(width, height);
|
||||
if (!buf.isValid()) {
|
||||
LOG_ERR("PHC", "Failed to allocate %dx%d pixel buffer (%d bytes)", width, height, (width + 31) / 32 * 4 * height);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Proportional layout constants based on cover dimensions.
|
||||
// The device bezel covers ~2-3px on each edge, so we pad inward from the edge.
|
||||
const int edgePadding = std::max(3, width / 48); // ~10px at 480w, ~3px at 136w
|
||||
const int borderWidth = std::max(2, width / 96); // ~5px at 480w, ~2px at 136w
|
||||
const int innerPadding = std::max(4, width / 32); // ~15px at 480w, ~4px at 136w
|
||||
|
||||
// Text scaling: 2x for full-size covers, 1x for thumbnails
|
||||
const int titleScale = (height >= 600) ? 2 : 1;
|
||||
const int authorScale = (height >= 600) ? 2 : 1; // Author also larger on full covers
|
||||
// Icon: 2x for full cover, 1x for medium thumb, skip for small
|
||||
const int iconScale = (height >= 600) ? 2 : (height >= 350 ? 1 : 0);
|
||||
|
||||
// Draw border inset from edge
|
||||
buf.drawBorder(edgePadding, edgePadding, width - 2 * edgePadding, height - 2 * edgePadding, borderWidth);
|
||||
|
||||
// Content area (inside border + inner padding)
|
||||
const int contentX = edgePadding + borderWidth + innerPadding;
|
||||
const int contentY = edgePadding + borderWidth + innerPadding;
|
||||
const int contentW = width - 2 * contentX;
|
||||
const int contentH = height - 2 * contentY;
|
||||
|
||||
if (contentW <= 0 || contentH <= 0) {
|
||||
LOG_ERR("PHC", "Cover too small for content (%dx%d)", width, height);
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
|
||||
return false;
|
||||
}
|
||||
buf.writeBmp(file);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Layout zones ---
|
||||
// Title zone: top 2/3 of content area (icon + title)
|
||||
// Author zone: bottom 1/3 of content area
|
||||
const int titleZoneH = contentH * 2 / 3;
|
||||
const int authorZoneH = contentH - titleZoneH;
|
||||
const int authorZoneY = contentY + titleZoneH;
|
||||
|
||||
// --- Separator line at the zone boundary ---
|
||||
const int separatorWidth = contentW / 3;
|
||||
const int separatorX = contentX + (contentW - separatorWidth) / 2;
|
||||
buf.drawHLine(separatorX, authorZoneY, separatorWidth);
|
||||
|
||||
// --- Icon dimensions (needed for title text wrapping) ---
|
||||
const int iconW = (iconScale > 0) ? BOOK_ICON_WIDTH * iconScale : 0;
|
||||
const int iconGap = (iconScale > 0) ? std::max(8, width / 40) : 0; // Gap between icon and title text
|
||||
const int titleTextW = contentW - iconW - iconGap; // Title wraps in narrower area beside icon
|
||||
|
||||
// --- Prepare title text (wraps within the area to the right of the icon) ---
|
||||
const std::string displayTitle = title.empty() ? "Untitled" : title;
|
||||
auto titleLines = wrapText(titleFont, displayTitle, titleTextW, titleScale);
|
||||
|
||||
constexpr int MAX_TITLE_LINES = 5;
|
||||
if (static_cast<int>(titleLines.size()) > MAX_TITLE_LINES) {
|
||||
titleLines.resize(MAX_TITLE_LINES);
|
||||
titleLines.back() = truncateText(titleFont, titleLines.back(), titleTextW, titleScale);
|
||||
}
|
||||
|
||||
// --- Prepare author text (multi-line, max 3 lines) ---
|
||||
std::vector<std::string> authorLines;
|
||||
if (!author.empty()) {
|
||||
authorLines = wrapText(authorFont, author, contentW, authorScale);
|
||||
constexpr int MAX_AUTHOR_LINES = 3;
|
||||
if (static_cast<int>(authorLines.size()) > MAX_AUTHOR_LINES) {
|
||||
authorLines.resize(MAX_AUTHOR_LINES);
|
||||
authorLines.back() = truncateText(authorFont, authorLines.back(), contentW, authorScale);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Calculate title zone layout (icon LEFT of title) ---
|
||||
// Tighter line spacing so 2-3 title lines fit within the icon height
|
||||
const int titleLineH = titleFont->advanceY * titleScale * 3 / 4;
|
||||
const int iconH = (iconScale > 0) ? BOOK_ICON_HEIGHT * iconScale : 0;
|
||||
const int numTitleLines = static_cast<int>(titleLines.size());
|
||||
// Visual height: distance from top of first line to bottom of last line's glyphs.
|
||||
// Use ascender (not full advanceY) for the last line since trailing line-gap isn't visible.
|
||||
const int titleVisualH =
|
||||
(numTitleLines > 0) ? (numTitleLines - 1) * titleLineH + titleFont->ascender * titleScale : 0;
|
||||
const int titleBlockH = std::max(iconH, titleVisualH); // Taller of icon or text
|
||||
|
||||
int titleStartY = contentY + (titleZoneH - titleBlockH) / 2;
|
||||
if (titleStartY < contentY) {
|
||||
titleStartY = contentY;
|
||||
}
|
||||
|
||||
// If title fits within icon height, center it vertically against the icon.
|
||||
// Otherwise top-align so extra lines overflow below.
|
||||
const int iconY = titleStartY;
|
||||
const int titleTextY = (iconH > 0 && titleVisualH <= iconH) ? titleStartY + (iconH - titleVisualH) / 2 : titleStartY;
|
||||
|
||||
// --- Horizontal centering: measure the widest title line, then center icon+gap+text block ---
|
||||
int maxTitleLineW = 0;
|
||||
for (const auto& line : titleLines) {
|
||||
const int w = measureTextWidth(titleFont, line.c_str()) * titleScale;
|
||||
if (w > maxTitleLineW) maxTitleLineW = w;
|
||||
}
|
||||
const int titleBlockW = iconW + iconGap + maxTitleLineW;
|
||||
const int titleBlockX = contentX + (contentW - titleBlockW) / 2;
|
||||
|
||||
// --- Draw icon ---
|
||||
if (iconScale > 0) {
|
||||
buf.drawIcon(BookIcon, BOOK_ICON_WIDTH, BOOK_ICON_HEIGHT, titleBlockX, iconY, iconScale);
|
||||
}
|
||||
|
||||
// --- Draw title lines (to the right of the icon) ---
|
||||
const int titleTextX = titleBlockX + iconW + iconGap;
|
||||
int currentY = titleTextY;
|
||||
for (const auto& line : titleLines) {
|
||||
buf.drawText(titleFont, titleTextX, currentY, line.c_str(), titleScale);
|
||||
currentY += titleLineH;
|
||||
}
|
||||
|
||||
// --- Draw author lines (centered vertically in bottom 1/3, centered horizontally) ---
|
||||
if (!authorLines.empty()) {
|
||||
const int authorLineH = authorFont->advanceY * authorScale;
|
||||
const int authorBlockH = static_cast<int>(authorLines.size()) * authorLineH;
|
||||
int authorStartY = authorZoneY + (authorZoneH - authorBlockH) / 2;
|
||||
if (authorStartY < authorZoneY + 4) {
|
||||
authorStartY = authorZoneY + 4; // Small gap below separator
|
||||
}
|
||||
|
||||
for (const auto& line : authorLines) {
|
||||
const int lineWidth = measureTextWidth(authorFont, line.c_str()) * authorScale;
|
||||
const int lineX = contentX + (contentW - lineWidth) / 2;
|
||||
buf.drawText(authorFont, lineX, authorStartY, line.c_str(), authorScale);
|
||||
authorStartY += authorLineH;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Write to file ---
|
||||
FsFile file;
|
||||
if (!Storage.openFileForWrite("PHC", outputPath, file)) {
|
||||
LOG_ERR("PHC", "Failed to open output file: %s", outputPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool success = buf.writeBmp(file);
|
||||
file.close();
|
||||
|
||||
if (success) {
|
||||
LOG_DBG("PHC", "Placeholder cover written to %s", outputPath.c_str());
|
||||
} else {
|
||||
LOG_ERR("PHC", "Failed to write placeholder BMP");
|
||||
Storage.remove(outputPath.c_str());
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
14
lib/PlaceholderCover/PlaceholderCoverGenerator.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/// Generates simple 1-bit BMP placeholder covers with title/author text
|
||||
/// for books that have no embedded cover image.
|
||||
class PlaceholderCoverGenerator {
|
||||
public:
|
||||
/// Generate a placeholder cover BMP with title and author text.
|
||||
/// The BMP is written to outputPath as a 1-bit black-and-white image.
|
||||
/// Returns true if the file was written successfully.
|
||||
static bool generate(const std::string& outputPath, const std::string& title, const std::string& author, int width,
|
||||
int height);
|
||||
};
|
||||
11
mod/book-closed-svgrepo-com.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
|
||||
<path fill="#231F20" d="M60,52V4c0-2.211-1.789-4-4-4H14v51v3h42v8H10c-2.209,0-4-1.791-4-4s1.791-4,4-4h2v-3V0H8
|
||||
C5.789,0,4,1.789,4,4v54c0,3.313,2.687,6,6,6h49c0.553,0,1-0.447,1-1s-0.447-1-1-1h-1v-8C59.104,54,60,53.104,60,52z M23,14h12
|
||||
c0.553,0,1,0.447,1,1s-0.447,1-1,1H23c-0.553,0-1-0.447-1-1S22.447,14,23,14z M42,28H23c-0.553,0-1-0.447-1-1s0.447-1,1-1h19
|
||||
c0.553,0,1,0.447,1,1S42.553,28,42,28z M49,22H23c-0.553,0-1-0.447-1-1s0.447-1,1-1h26c0.553,0,1,0.447,1,1S49.553,22,49,22z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 944 B |
BIN
mod/book_icon_48x48.png
Normal file
|
After Width: | Height: | Size: 210 B |
9
mod/docs/bookmark-svgrepo-com.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg version="1.1" id="Uploaded to svgrepo.com" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 32 32" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.puchipuchi_een{fill:#111918;}
|
||||
</style>
|
||||
<path class="puchipuchi_een" d="M23.293,30.705l-6.586-6.586c-0.391-0.391-1.024-0.391-1.414,0l-6.586,6.586
|
||||
C8.077,31.335,7,30.889,7,29.998V3c0-1.105,0.895-2,2-2h14c1.105,0,2,0.895,2,2v26.998C25,30.889,23.923,31.335,23.293,30.705z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 630 B |
280
mod/docs/ci-build-and-code-style.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# CI, Build, Release & Code Style
|
||||
|
||||
This document covers the CrossPoint Reader build system, CI pipeline, release process, code formatting rules, static analysis, and contribution guidelines.
|
||||
|
||||
## Build System
|
||||
|
||||
The project uses **PlatformIO** with the Arduino framework targeting the ESP32-C3.
|
||||
|
||||
### Build Environments
|
||||
|
||||
Defined in `platformio.ini`:
|
||||
|
||||
| Environment | Purpose | Version String |
|
||||
|---|---|---|
|
||||
| `default` | Local development builds | `1.0.0-dev` |
|
||||
| `gh_release` | Official tagged releases | `1.0.0` |
|
||||
| `gh_release_rc` | Release candidates | `1.0.0-rc+{7-char SHA}` |
|
||||
|
||||
### Build Command
|
||||
|
||||
```bash
|
||||
# Development build
|
||||
pio run
|
||||
|
||||
# Release build
|
||||
pio run -e gh_release
|
||||
|
||||
# Release candidate build (requires CROSSPOINT_RC_HASH env var)
|
||||
CROSSPOINT_RC_HASH=abc1234 pio run -e gh_release_rc
|
||||
```
|
||||
|
||||
### Build Flags
|
||||
|
||||
All environments share a common set of flags (`[base]` section):
|
||||
|
||||
| Flag | Purpose |
|
||||
|---|---|
|
||||
| `-std=c++2a` | C++20 standard |
|
||||
| `-DARDUINO_USB_MODE=1` | USB mode selection |
|
||||
| `-DARDUINO_USB_CDC_ON_BOOT=1` | Enable USB CDC serial on boot |
|
||||
| `-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1` | Avoid miniz/zlib symbol conflicts |
|
||||
| `-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1` | Single frame buffer (saves RAM) |
|
||||
| `-DDISABLE_FS_H_WARNING=1` | Suppress Arduino FS.h warning |
|
||||
| `-DXML_GE=0` | Disable expat general entity expansion |
|
||||
| `-DXML_CONTEXT_BYTES=1024` | Expat context buffer size |
|
||||
| `-DUSE_UTF8_LONG_NAMES=1` | Enable UTF-8 long filenames in SdFat |
|
||||
|
||||
### Pre-Build Step
|
||||
|
||||
`scripts/build_html.py` runs before compilation (configured via `extra_scripts = pre:scripts/build_html.py`). It:
|
||||
|
||||
1. Finds all `.html` files under `src/`.
|
||||
2. Minifies them (strips comments, collapses whitespace, preserves `<pre>`, `<code>`, `<textarea>`, `<script>`, `<style>` blocks).
|
||||
3. Generates `.generated.h` files containing `constexpr char ...Html[] PROGMEM = R"rawliteral(...)rawliteral";` strings.
|
||||
|
||||
### Dependencies
|
||||
|
||||
**SDK libraries** (symlinked from `open-x4-sdk` submodule):
|
||||
- `BatteryMonitor`
|
||||
- `InputManager`
|
||||
- `EInkDisplay`
|
||||
- `SDCardManager`
|
||||
|
||||
**External libraries** (managed by PlatformIO):
|
||||
- `bblanchon/ArduinoJson @ 7.4.2` -- JSON parsing
|
||||
- `ricmoo/QRCode @ 0.0.1` -- QR code generation
|
||||
- `links2004/WebSockets @ 2.7.3` -- WebSocket server
|
||||
|
||||
### Tool Versions
|
||||
|
||||
| Tool | Version | Notes |
|
||||
|---|---|---|
|
||||
| PlatformIO | Latest (via pip) | `espressif32 @ 6.12.0` platform |
|
||||
| Python | 3.14 | Used in CI and build scripts |
|
||||
| clang-format | 21 | From LLVM apt repository |
|
||||
| cppcheck | Latest (via PlatformIO) | Static analysis |
|
||||
|
||||
## CI Pipeline
|
||||
|
||||
All CI workflows are in `.github/workflows/`.
|
||||
|
||||
### `ci.yml` -- Main CI
|
||||
|
||||
**Triggers:** Push to `master`, all pull requests.
|
||||
|
||||
Runs **4 jobs in parallel** (the first 3 are independent; the 4th aggregates results):
|
||||
|
||||
#### 1. `clang-format` -- Code Formatting Check
|
||||
|
||||
1. Installs `clang-format-21` from the LLVM apt repository.
|
||||
2. Runs `bin/clang-format-fix` on the full codebase.
|
||||
3. Checks `git diff --exit-code` -- fails if any file was reformatted.
|
||||
|
||||
#### 2. `cppcheck` -- Static Analysis
|
||||
|
||||
1. Installs PlatformIO.
|
||||
2. Runs `pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high`.
|
||||
3. Fails on any low, medium, or high severity defect.
|
||||
|
||||
#### 3. `build` -- Compilation
|
||||
|
||||
1. Installs PlatformIO.
|
||||
2. Runs `pio run` (default environment).
|
||||
3. Extracts RAM and Flash usage stats into the GitHub step summary.
|
||||
4. Uploads `firmware.bin` as a build artifact.
|
||||
|
||||
#### 4. `test-status` -- PR Gate
|
||||
|
||||
- Depends on all three jobs above.
|
||||
- Fails if any dependency failed or was cancelled.
|
||||
- This is the required status check for pull requests, decoupling CI steps from PR merge requirements.
|
||||
|
||||
### `pr-formatting-check.yml` -- PR Title Validation
|
||||
|
||||
**Triggers:** Pull request `opened`, `reopened`, `edited` events.
|
||||
|
||||
Uses `amannn/action-semantic-pull-request@v6` to enforce semantic PR title format (e.g., `feat: add dark mode`, `fix: correct page numbering`).
|
||||
|
||||
### `release.yml` -- Official Release
|
||||
|
||||
**Trigger:** Push of any git tag.
|
||||
|
||||
1. Builds using the `gh_release` environment.
|
||||
2. Uploads release artifacts named `CrossPoint-{tag}`:
|
||||
- `bootloader.bin`
|
||||
- `firmware.bin`
|
||||
- `firmware.elf`
|
||||
- `firmware.map`
|
||||
- `partitions.bin`
|
||||
|
||||
### `release_candidate.yml` -- RC Build
|
||||
|
||||
**Trigger:** Manual `workflow_dispatch`, restricted to branches matching `release/*`.
|
||||
|
||||
1. Extracts the 7-character short SHA and branch suffix (e.g., `v1.0.0` from `release/v1.0.0`).
|
||||
2. Sets `CROSSPOINT_RC_HASH` env var.
|
||||
3. Builds using the `gh_release_rc` environment.
|
||||
4. Uploads artifacts named `CrossPoint-RC-{suffix}`.
|
||||
|
||||
## Release Process
|
||||
|
||||
### Official Release
|
||||
|
||||
1. Create and push a git tag (e.g., `v1.0.0`).
|
||||
2. The `release.yml` workflow triggers automatically.
|
||||
3. Artifacts are uploaded to the GitHub Actions run.
|
||||
|
||||
### Release Candidate
|
||||
|
||||
1. Create a branch named `release/{identifier}` (e.g., `release/v1.0.0`).
|
||||
2. Navigate to Actions in GitHub and manually trigger `Compile Release Candidate` on that branch.
|
||||
3. The RC version string includes the commit SHA for traceability.
|
||||
|
||||
### Version Scheme
|
||||
|
||||
```
|
||||
1.0.0-dev # Local development (default env)
|
||||
1.0.0 # Official release (gh_release env)
|
||||
1.0.0-rc+a1b2c3d # Release candidate (gh_release_rc env)
|
||||
```
|
||||
|
||||
The version base is set in `platformio.ini` under `[crosspoint] version = 1.0.0`.
|
||||
|
||||
## Code Style
|
||||
|
||||
### Formatting: clang-format
|
||||
|
||||
The project uses **clang-format 21** with a `.clang-format` config at the repository root. Key rules:
|
||||
|
||||
| Setting | Value | Meaning |
|
||||
|---|---|---|
|
||||
| `IndentWidth` | `2` | 2-space indentation |
|
||||
| `TabWidth` / `UseTab` | `8` / `Never` | Spaces only (no tabs) |
|
||||
| `ColumnLimit` | `120` | Maximum line width |
|
||||
| `BreakBeforeBraces` | `Attach` | K&R brace style (opening brace on same line) |
|
||||
| `PointerAlignment` | `Left` | `int* ptr` not `int *ptr` |
|
||||
| `ReferenceAlignment` | `Pointer` | References follow pointer style |
|
||||
| `ContinuationIndentWidth` | `4` | 4-space continuation indent |
|
||||
| `AllowShortFunctionsOnASingleLine` | `All` | Short functions may be single-line |
|
||||
| `AllowShortIfStatementsOnASingleLine` | `WithoutElse` | `if (x) return;` allowed |
|
||||
| `AllowShortLoopsOnASingleLine` | `true` | Short loops may be single-line |
|
||||
| `BreakBeforeTernaryOperators` | `true` | `?` and `:` start new lines |
|
||||
| `BreakBeforeBinaryOperators` | `None` | Binary operators stay at end of line |
|
||||
| `SortIncludes` | `Enabled` | Includes sorted lexicographically |
|
||||
| `IncludeBlocks` | `Regroup` | Includes grouped by category |
|
||||
| `ReflowComments` | `Always` | Long comments are rewrapped |
|
||||
| `SpacesBeforeTrailingComments` | `2` | Two spaces before `// comment` |
|
||||
| `LineEnding` | `DeriveLF` | Unix-style line endings |
|
||||
|
||||
### Include Order
|
||||
|
||||
Includes are regrouped into priority categories:
|
||||
|
||||
1. **Priority 1:** System headers with `.h` extension (`<foo.h>`)
|
||||
2. **Priority 2:** Other system headers (`<foo>`) and extension headers (`<ext/foo.h>`)
|
||||
3. **Priority 3:** Project-local headers (`"foo.h"`)
|
||||
|
||||
### Running the Formatter
|
||||
|
||||
```bash
|
||||
# Format all tracked source files
|
||||
./bin/clang-format-fix
|
||||
|
||||
# Format only modified (staged or unstaged) files
|
||||
./bin/clang-format-fix -g
|
||||
```
|
||||
|
||||
The script formats `.c`, `.cpp`, `.h`, `.hpp` files tracked by git, **excluding** `lib/EpdFont/builtinFonts/` (script-generated font headers).
|
||||
|
||||
### Static Analysis: cppcheck
|
||||
|
||||
Configuration in `platformio.ini`:
|
||||
|
||||
```
|
||||
check_tool = cppcheck
|
||||
check_flags = --enable=all
|
||||
--suppress=missingIncludeSystem
|
||||
--suppress=unusedFunction
|
||||
--suppress=unmatchedSuppression
|
||||
--suppress=*:*/.pio/*
|
||||
--inline-suppr
|
||||
```
|
||||
|
||||
- `--enable=all` enables all checks.
|
||||
- Suppressed: missing system includes, unused functions (common in library code), PlatformIO build directory.
|
||||
- `--inline-suppr` allows `// cppcheck-suppress` comments in source.
|
||||
- CI fails on **any** defect severity (low, medium, or high).
|
||||
|
||||
Run locally:
|
||||
|
||||
```bash
|
||||
pio check
|
||||
```
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
### PR Requirements
|
||||
|
||||
1. **Semantic PR title** -- enforced by CI. Must follow conventional format:
|
||||
- `feat: ...`, `fix: ...`, `refactor: ...`, `docs: ...`, `chore: ...`, etc.
|
||||
|
||||
2. **PR template** (`.github/PULL_REQUEST_TEMPLATE.md`) must be filled out:
|
||||
- **Summary:** Goal and changes included.
|
||||
- **Additional Context:** Performance implications, risks, focus areas.
|
||||
- **AI Usage:** Transparent disclosure -- `YES`, `PARTIALLY`, or `NO`.
|
||||
|
||||
3. **Code formatting** -- run `bin/clang-format-fix` before pushing. CI will reject unformatted code.
|
||||
|
||||
4. **Static analysis** -- ensure `pio check` passes without low/medium/high defects.
|
||||
|
||||
5. **Build** -- confirm `pio run` compiles without errors.
|
||||
|
||||
### Project Scope
|
||||
|
||||
All contributions must align with the project's core mission: **focused reading on the Xteink X4**. See `SCOPE.md` for full details.
|
||||
|
||||
**In scope:** UX improvements, document rendering (EPUB), typography, e-ink driver refinement, library management, local file transfer, language support.
|
||||
|
||||
**Out of scope:** Interactive apps, active connectivity (RSS/news/browsers), media playback, complex reader features (highlighting, notes, dictionary).
|
||||
|
||||
When unsure, open a Discussion before coding.
|
||||
|
||||
### Governance
|
||||
|
||||
From `GOVERNANCE.md`:
|
||||
|
||||
- **Assume good intent** in technical discussions.
|
||||
- **Critique code, not people.**
|
||||
- **Public by default** -- decisions happen in Issues, PRs, and Discussions.
|
||||
- Maintainers guide technical direction for ESP32-C3 hardware constraints.
|
||||
- Report harassment privately to `@daveallie`.
|
||||
|
||||
### Checklist Before Submitting
|
||||
|
||||
- [ ] Code compiles: `pio run`
|
||||
- [ ] Code is formatted: `bin/clang-format-fix`
|
||||
- [ ] Static analysis passes: `pio check`
|
||||
- [ ] PR title follows semantic format
|
||||
- [ ] PR template filled out (summary, context, AI disclosure)
|
||||
- [ ] Changes align with project scope (`SCOPE.md`)
|
||||
BIN
mod/docs/cover_worldgrain.jpg
Normal file
|
After Width: | Height: | Size: 66 KiB |
101144
mod/docs/dictionary.dict
Executable file
BIN
mod/docs/dictionary.idx
Executable file
11
mod/docs/dictionary.ifo
Executable file
@@ -0,0 +1,11 @@
|
||||
StarDict's dict ifo file
|
||||
version=3.0.0
|
||||
bookname=reader.dict EN
|
||||
wordcount=894535
|
||||
idxfilesize=17310252
|
||||
sametypesequence=h
|
||||
synwordcount=504743
|
||||
website=https://www.reader-dict.com
|
||||
date=2026-01-01
|
||||
description=© reader.dict 2026
|
||||
lang=en-en
|
||||
306
mod/docs/file-structure.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# CrossPoint Reader -- Project File Structure
|
||||
|
||||
This document maps the repository layout and describes where key pieces of code can be found.
|
||||
|
||||
## Root Directory
|
||||
|
||||
```
|
||||
crosspoint-reader-mod/
|
||||
├── platformio.ini # PlatformIO build config (environments, flags, deps)
|
||||
├── partitions.csv # ESP32 flash partition table
|
||||
├── .clang-format # C++ code formatting rules (clang-format 21)
|
||||
├── .clangd # Language server configuration
|
||||
├── .gitmodules # Git submodule reference (open-x4-sdk)
|
||||
├── README.md # Project overview and getting started
|
||||
├── LICENSE # MIT License
|
||||
├── GOVERNANCE.md # Community governance principles
|
||||
├── SCOPE.md # Project vision and feature scope
|
||||
├── USER_GUIDE.md # End-user manual
|
||||
├── bin/ # Developer scripts
|
||||
├── docs/ # Upstream documentation
|
||||
├── lib/ # Project libraries (HAL, EPUB, fonts, etc.)
|
||||
├── src/ # Main application source code
|
||||
├── scripts/ # Build and utility scripts (Python)
|
||||
├── test/ # Unit/evaluation tests
|
||||
├── open-x4-sdk/ # Hardware SDK (git submodule)
|
||||
├── include/ # PlatformIO include directory (empty)
|
||||
├── mod/ # Local modifications workspace
|
||||
└── .github/ # CI workflows, templates, funding
|
||||
```
|
||||
|
||||
## `src/` -- Application Source
|
||||
|
||||
The main firmware application code. Entry point is `main.cpp`.
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.cpp # Arduino setup()/loop(), activity routing, font init
|
||||
├── CrossPointSettings.cpp/.h # User settings (load/save to SD, defaults)
|
||||
├── CrossPointState.cpp/.h # Runtime application state (open book, sleep tracking)
|
||||
├── MappedInputManager.cpp/.h # User-remappable button abstraction over HalGPIO
|
||||
├── Battery.h # Global BatteryMonitor instance
|
||||
├── fontIds.h # Numeric font family identifiers
|
||||
├── SettingsList.h # Setting definitions and enums
|
||||
├── RecentBooksStore.cpp/.h # Recently opened books persistence
|
||||
├── WifiCredentialStore.cpp/.h # Saved WiFi network credentials
|
||||
│
|
||||
├── activities/ # UI screens (Activity pattern, see below)
|
||||
├── components/ # Shared UI components and themes
|
||||
├── network/ # Web server, HTTP, OTA updater
|
||||
├── images/ # Logo assets (SVG, PNG, compiled header)
|
||||
└── util/ # String and URL utility functions
|
||||
```
|
||||
|
||||
### `src/activities/` -- UI Activity System
|
||||
|
||||
Each screen is an `Activity` subclass with `onEnter()`, `onExit()`, and `loop()` lifecycle methods. Activities are swapped by the routing functions in `main.cpp`.
|
||||
|
||||
```
|
||||
activities/
|
||||
├── Activity.h # Base Activity interface
|
||||
├── ActivityWithSubactivity.cpp/.h # Composition: activity that hosts a child
|
||||
│
|
||||
├── boot_sleep/
|
||||
│ ├── BootActivity.cpp/.h # Boot splash screen
|
||||
│ └── SleepActivity.cpp/.h # Sleep screen (cover image, clock, etc.)
|
||||
│
|
||||
├── browser/
|
||||
│ └── OpdsBookBrowserActivity.cpp/.h # OPDS catalog browsing and download
|
||||
│
|
||||
├── home/
|
||||
│ ├── HomeActivity.cpp/.h # Main menu / home screen
|
||||
│ ├── MyLibraryActivity.cpp/.h # File browser for books on SD card
|
||||
│ └── RecentBooksActivity.cpp/.h # Recently opened books list
|
||||
│
|
||||
├── network/
|
||||
│ ├── CrossPointWebServerActivity.cpp/.h # Web server file transfer mode
|
||||
│ ├── CalibreConnectActivity.cpp/.h # Calibre wireless device connection
|
||||
│ ├── NetworkModeSelectionActivity.cpp/.h # WiFi/AP/Calibre mode picker
|
||||
│ └── WifiSelectionActivity.cpp/.h # WiFi network scanner and connector
|
||||
│
|
||||
├── reader/
|
||||
│ ├── ReaderActivity.cpp/.h # Format dispatcher (EPUB, TXT, XTC)
|
||||
│ ├── EpubReaderActivity.cpp/.h # EPUB reading engine
|
||||
│ ├── EpubReaderMenuActivity.cpp/.h # In-reader menu overlay
|
||||
│ ├── EpubReaderChapterSelectionActivity.cpp/.h # Table of contents navigation
|
||||
│ ├── EpubReaderPercentSelectionActivity.cpp/.h # Jump-to-percentage navigation
|
||||
│ ├── TxtReaderActivity.cpp/.h # Plain text reader
|
||||
│ ├── XtcReaderActivity.cpp/.h # XTC format reader
|
||||
│ ├── XtcReaderChapterSelectionActivity.cpp/.h # XTC chapter navigation
|
||||
│ └── KOReaderSyncActivity.cpp/.h # KOReader reading progress sync
|
||||
│
|
||||
├── settings/
|
||||
│ ├── SettingsActivity.cpp/.h # Main settings screen
|
||||
│ ├── ButtonRemapActivity.cpp/.h # Front button remapping UI
|
||||
│ ├── CalibreSettingsActivity.cpp/.h # Calibre server configuration
|
||||
│ ├── ClearCacheActivity.cpp/.h # Cache clearing utility
|
||||
│ ├── KOReaderAuthActivity.cpp/.h # KOReader auth credential entry
|
||||
│ ├── KOReaderSettingsActivity.cpp/.h # KOReader sync settings
|
||||
│ └── OtaUpdateActivity.cpp/.h # Over-the-air firmware update UI
|
||||
│
|
||||
└── util/
|
||||
├── FullScreenMessageActivity.cpp/.h # Full-screen status/error messages
|
||||
└── KeyboardEntryActivity.cpp/.h # On-screen keyboard for text input
|
||||
```
|
||||
|
||||
### `src/components/` -- UI Components and Themes
|
||||
|
||||
```
|
||||
components/
|
||||
├── UITheme.cpp/.h # Theme manager singleton
|
||||
└── themes/
|
||||
├── BaseTheme.cpp/.h # Default theme implementation
|
||||
└── lyra/
|
||||
└── LyraTheme.cpp/.h # Alternative "Lyra" theme
|
||||
```
|
||||
|
||||
### `src/network/` -- Networking
|
||||
|
||||
```
|
||||
network/
|
||||
├── CrossPointWebServer.cpp/.h # HTTP web server (file management, upload, settings)
|
||||
├── HttpDownloader.cpp/.h # HTTP/HTTPS file downloader
|
||||
├── OtaUpdater.cpp/.h # OTA firmware update client
|
||||
└── html/
|
||||
├── HomePage.html # Web UI home page
|
||||
├── FilesPage.html # Web UI file browser / upload page
|
||||
└── SettingsPage.html # Web UI settings page
|
||||
```
|
||||
|
||||
HTML files are minified at build time by `scripts/build_html.py` into `PROGMEM` C++ headers (`.generated.h`).
|
||||
|
||||
### `src/util/` -- Utilities
|
||||
|
||||
```
|
||||
util/
|
||||
├── StringUtils.cpp/.h # String manipulation helpers
|
||||
└── UrlUtils.cpp/.h # URL encoding/decoding
|
||||
```
|
||||
|
||||
## `lib/` -- Project Libraries
|
||||
|
||||
PlatformIO-managed libraries local to the project.
|
||||
|
||||
### `lib/hal/` -- Hardware Abstraction Layer
|
||||
|
||||
The HAL wraps the SDK drivers behind a stable API the rest of the firmware consumes.
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `HalDisplay.cpp/.h` | E-ink display control (refresh, buffer ops, grayscale, sleep) |
|
||||
| `HalGPIO.cpp/.h` | Button input, battery reading, USB detection, deep sleep, wakeup |
|
||||
| `HalStorage.cpp/.h` | SD card file operations (`Storage` singleton) |
|
||||
|
||||
### `lib/Epub/` -- EPUB Library
|
||||
|
||||
The core document parsing and rendering engine.
|
||||
|
||||
```
|
||||
Epub/
|
||||
├── Epub.cpp/.h # Top-level EPUB interface
|
||||
├── Epub/
|
||||
│ ├── Page.cpp/.h # Single rendered page
|
||||
│ ├── Section.cpp/.h # Book section (chapter)
|
||||
│ ├── ParsedText.cpp/.h # Parsed text representation
|
||||
│ ├── BookMetadataCache.cpp/.h # Metadata caching to SD card
|
||||
│ │
|
||||
│ ├── blocks/
|
||||
│ │ ├── Block.h # Base block type
|
||||
│ │ ├── BlockStyle.h # Block styling
|
||||
│ │ └── TextBlock.cpp/.h # Text block rendering
|
||||
│ │
|
||||
│ ├── css/
|
||||
│ │ ├── CssParser.cpp/.h # CSS subset parser
|
||||
│ │ └── CssStyle.h # Parsed CSS style representation
|
||||
│ │
|
||||
│ ├── hyphenation/
|
||||
│ │ ├── Hyphenator.cpp/.h # Main hyphenation API
|
||||
│ │ ├── LiangHyphenation.cpp/.h # Liang algorithm implementation
|
||||
│ │ ├── HyphenationCommon.cpp/.h # Shared types
|
||||
│ │ ├── LanguageHyphenator.h # Per-language interface
|
||||
│ │ ├── LanguageRegistry.cpp/.h # Language detection and routing
|
||||
│ │ ├── SerializedHyphenationTrie.h # Trie data format
|
||||
│ │ └── generated/ # Pre-built hyphenation tries
|
||||
│ │ ├── hyph-de.trie.h # German
|
||||
│ │ ├── hyph-en.trie.h # English
|
||||
│ │ ├── hyph-es.trie.h # Spanish
|
||||
│ │ ├── hyph-fr.trie.h # French
|
||||
│ │ └── hyph-ru.trie.h # Russian
|
||||
│ │
|
||||
│ └── parsers/
|
||||
│ ├── ChapterHtmlSlimParser.cpp/.h # Chapter HTML content parser
|
||||
│ ├── ContainerParser.cpp/.h # META-INF/container.xml parser
|
||||
│ ├── ContentOpfParser.cpp/.h # content.opf (spine, manifest) parser
|
||||
│ ├── TocNavParser.cpp/.h # EPUB3 nav TOC parser
|
||||
│ └── TocNcxParser.cpp/.h # EPUB2 NCX TOC parser
|
||||
```
|
||||
|
||||
### `lib/EpdFont/` -- Font Rendering
|
||||
|
||||
```
|
||||
EpdFont/
|
||||
├── EpdFont.cpp/.h # Font rendering engine
|
||||
├── EpdFontData.h # Raw font data structure
|
||||
├── EpdFontFamily.cpp/.h # Font family (regular, bold, italic, bold-italic)
|
||||
├── builtinFonts/
|
||||
│ ├── all.h # Aggregate include for all built-in fonts
|
||||
│ ├── bookerly_*.h # Bookerly at sizes 12, 14, 16, 18
|
||||
│ ├── notosans_*.h # Noto Sans at sizes 8, 12, 14, 16, 18
|
||||
│ ├── opendyslexic_*.h # OpenDyslexic at sizes 8, 10, 12, 14
|
||||
│ ├── ubuntu_*.h # Ubuntu at sizes 10, 12 (UI font)
|
||||
│ └── source/ # Original TTF/OTF font files
|
||||
└── scripts/
|
||||
├── fontconvert.py # TTF/OTF to compiled header converter
|
||||
├── convert-builtin-fonts.sh # Batch conversion script
|
||||
├── build-font-ids.sh # Generate fontIds.h
|
||||
└── requirements.txt # Python dependencies for font tools
|
||||
```
|
||||
|
||||
### Other Libraries
|
||||
|
||||
| Library | Path | Purpose |
|
||||
|---|---|---|
|
||||
| `expat/` | `lib/expat/` | XML parser (used by EPUB parsers) |
|
||||
| `miniz/` | `lib/miniz/` | Compression/decompression (ZIP support) |
|
||||
| `ZipFile/` | `lib/ZipFile/` | ZIP file reading (EPUB is a ZIP) |
|
||||
| `picojpeg/` | `lib/picojpeg/` | Lightweight JPEG decoder |
|
||||
| `JpegToBmpConverter/` | `lib/JpegToBmpConverter/` | JPEG to bitmap conversion for display |
|
||||
| `GfxRenderer/` | `lib/GfxRenderer/` | Graphics renderer, bitmap helpers |
|
||||
| `Txt/` | `lib/Txt/` | Plain text (.txt) file reader |
|
||||
| `Xtc/` | `lib/Xtc/` | XTC format parser and types |
|
||||
| `OpdsParser/` | `lib/OpdsParser/` | OPDS feed XML parser and streaming |
|
||||
| `KOReaderSync/` | `lib/KOReaderSync/` | KOReader sync client, credential store, progress mapping |
|
||||
| `Utf8/` | `lib/Utf8/` | UTF-8 string utilities |
|
||||
| `Serialization/` | `lib/Serialization/` | Binary serialization helper |
|
||||
| `FsHelpers/` | `lib/FsHelpers/` | File system utility functions |
|
||||
|
||||
## `open-x4-sdk/` -- Hardware SDK (Git Submodule)
|
||||
|
||||
Community SDK providing low-level hardware drivers. Referenced in `.gitmodules` from `https://github.com/open-x4-epaper/community-sdk.git`.
|
||||
|
||||
Supplies four libraries linked as symlinks in `platformio.ini`:
|
||||
|
||||
| Library | SDK Path | Purpose |
|
||||
|---|---|---|
|
||||
| `BatteryMonitor` | `libs/hardware/BatteryMonitor` | Battery voltage ADC reading |
|
||||
| `InputManager` | `libs/hardware/InputManager` | Physical button GPIO management |
|
||||
| `EInkDisplay` | `libs/display/EInkDisplay` | E-ink display SPI driver |
|
||||
| `SDCardManager` | `libs/hardware/SDCardManager` | SD card SPI initialization |
|
||||
|
||||
## `scripts/` -- Build and Utility Scripts
|
||||
|
||||
| Script | Language | Purpose |
|
||||
|---|---|---|
|
||||
| `build_html.py` | Python | **Pre-build step.** Minifies HTML files in `src/` into `PROGMEM` C++ headers (`.generated.h`). Strips comments, collapses whitespace, preserves `<pre>`, `<code>`, `<script>`, `<style>` content. |
|
||||
| `generate_hyphenation_trie.py` | Python | Generates serialized hyphenation trie headers from TeX hyphenation patterns. |
|
||||
| `debugging_monitor.py` | Python | Enhanced serial monitor for debugging output. |
|
||||
|
||||
## `test/` -- Tests
|
||||
|
||||
```
|
||||
test/
|
||||
├── README # PlatformIO test directory info
|
||||
├── run_hyphenation_eval.sh # Test runner script
|
||||
└── hyphenation_eval/
|
||||
├── HyphenationEvaluationTest.cpp # Hyphenation accuracy evaluation
|
||||
└── resources/
|
||||
├── english_hyphenation_tests.txt
|
||||
├── french_hyphenation_tests.txt
|
||||
├── german_hyphenation_tests.txt
|
||||
├── russian_hyphenation_tests.txt
|
||||
├── spanish_hyphenation_tests.txt
|
||||
└── generate_hyphenation_test_data.py
|
||||
```
|
||||
|
||||
## `docs/` -- Upstream Documentation
|
||||
|
||||
| File | Topic |
|
||||
|---|---|
|
||||
| `comparison.md` | Feature comparison with stock firmware |
|
||||
| `file-formats.md` | Supported document formats |
|
||||
| `hyphenation-trie-format.md` | Hyphenation trie binary format specification |
|
||||
| `troubleshooting.md` | Common issues and fixes |
|
||||
| `webserver.md` | Web server feature guide |
|
||||
| `webserver-endpoints.md` | Web server HTTP API reference |
|
||||
| `images/` | Screenshots and cover image |
|
||||
|
||||
## `.github/` -- GitHub Configuration
|
||||
|
||||
```
|
||||
.github/
|
||||
├── workflows/
|
||||
│ ├── ci.yml # Main CI: format check, cppcheck, build
|
||||
│ ├── release.yml # Tag-triggered release build
|
||||
│ ├── release_candidate.yml # Manual RC build (release/* branches)
|
||||
│ └── pr-formatting-check.yml # Semantic PR title validation
|
||||
├── PULL_REQUEST_TEMPLATE.md # PR template (summary, context, AI usage)
|
||||
├── ISSUE_TEMPLATE/
|
||||
│ └── bug_report.yml # Bug report form
|
||||
└── FUNDING.yml # GitHub Sponsors configuration
|
||||
```
|
||||
|
||||
## `bin/` -- Developer Tools
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `clang-format-fix` | Runs `clang-format` on all tracked `.c`, `.cpp`, `.h`, `.hpp` files (excluding generated font headers). Pass `-g` to format only modified files. |
|
||||
174
mod/docs/hardware.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Xteink X4 Hardware Capabilities
|
||||
|
||||
This document describes the hardware present on the Xteink X4 e-ink device and what the CrossPoint Reader firmware can leverage.
|
||||
|
||||
## CPU / Microcontroller
|
||||
|
||||
- **MCU:** ESP32-C3 (RISC-V, single-core)
|
||||
- **Usable RAM:** ~380 KB
|
||||
- **Architecture:** Single-core -- background tasks (WiFi, etc.) compete with the main application loop for CPU time.
|
||||
|
||||
> Source: `platformio.ini` (`board = esp32-c3-devkitm-1`), `README.md`
|
||||
|
||||
## Flash Memory
|
||||
|
||||
| Region | Offset | Size | Purpose |
|
||||
|---|---|---|---|
|
||||
| NVS | `0x9000` | 20 KB | Non-volatile key/value storage (settings, credentials) |
|
||||
| OTA Data | `0xE000` | 8 KB | OTA boot-selection metadata |
|
||||
| App Partition 0 (ota_0) | `0x10000` | 6.25 MB | Primary firmware slot |
|
||||
| App Partition 1 (ota_1) | `0x650000` | 6.25 MB | Secondary firmware slot (OTA target) |
|
||||
| SPIFFS | `0xC90000` | 3.375 MB | On-flash file system |
|
||||
| Core Dump | `0xFF0000` | 64 KB | Crash dump storage |
|
||||
|
||||
- **Total Flash:** 16 MB
|
||||
- **Flash Mode:** DIO (Dual I/O)
|
||||
- **Dual OTA partitions** enable over-the-air firmware updates: one slot runs the active firmware while the other receives the new image.
|
||||
|
||||
> Source: `partitions.csv`, `platformio.ini`
|
||||
|
||||
## Display
|
||||
|
||||
- **Type:** E-ink (E-paper)
|
||||
- **Resolution:** 480 x 800 pixels (portrait orientation)
|
||||
- **Color Depth:** 2-bit grayscale (4 levels: white, light gray, dark gray, black)
|
||||
- **Frame Buffer Size:** 48,000 bytes (480/8 x 800), single-buffer mode
|
||||
|
||||
### Refresh Modes
|
||||
|
||||
| Mode | Description |
|
||||
|---|---|
|
||||
| `FULL_REFRESH` | Complete waveform -- best quality, slowest |
|
||||
| `HALF_REFRESH` | Balanced quality/speed (~1720 ms) |
|
||||
| `FAST_REFRESH` | Custom LUT -- fastest, used as the default |
|
||||
|
||||
### Grayscale Support
|
||||
|
||||
The display driver exposes separate LSB and MSB grayscale buffers (`copyGrayscaleBuffers`, `copyGrayscaleLsbBuffers`, `copyGrayscaleMsbBuffers`) for 2-bit (4-level) grayscale rendering.
|
||||
|
||||
### SPI Pin Mapping
|
||||
|
||||
| Signal | GPIO | Description |
|
||||
|---|---|---|
|
||||
| `EPD_SCLK` | 8 | SPI Clock |
|
||||
| `EPD_MOSI` | 10 | SPI MOSI (Master Out Slave In) |
|
||||
| `EPD_CS` | 21 | Chip Select |
|
||||
| `EPD_DC` | 4 | Data/Command |
|
||||
| `EPD_RST` | 5 | Reset |
|
||||
| `EPD_BUSY` | 6 | Busy signal |
|
||||
| `SPI_MISO` | 7 | SPI MISO (shared with SD card) |
|
||||
|
||||
> Source: `lib/hal/HalDisplay.h`, `lib/hal/HalGPIO.h`
|
||||
|
||||
## Buttons / Input
|
||||
|
||||
The device has **7 physical buttons**:
|
||||
|
||||
| Index | Constant | Location | Notes |
|
||||
|---|---|---|---|
|
||||
| 0 | `BTN_BACK` | Front | Remappable |
|
||||
| 1 | `BTN_CONFIRM` | Front | Remappable |
|
||||
| 2 | `BTN_LEFT` | Front | Page navigation, Remappable |
|
||||
| 3 | `BTN_RIGHT` | Front | Page navigation, Remappable |
|
||||
| 4 | `BTN_UP` | Side | Page navigation, Remappable |
|
||||
| 5 | `BTN_DOWN` | Side | Page navigation, Remappable |
|
||||
| 6 | `BTN_POWER` | Side | Wakes from deep sleep |
|
||||
|
||||
### Input Features
|
||||
|
||||
- Press, release, and hold-time detection via `InputManager` (SDK) and `HalGPIO`.
|
||||
- The 4 front buttons and 2 side buttons are user-remappable through `MappedInputManager`.
|
||||
- `MappedInputManager` adds logical button aliases (`PageBack`, `PageForward`) and a label-mapping system for UI hints.
|
||||
- Side button layout can be swapped for left-handed use.
|
||||
- Configurable power button hold duration for sleep/wake.
|
||||
|
||||
> Source: `lib/hal/HalGPIO.h`, `src/MappedInputManager.h`
|
||||
|
||||
## Storage (SD Card)
|
||||
|
||||
- **Interface:** SPI (shares the SPI bus with the display; `SPI_MISO` on GPIO 7 is common)
|
||||
- **File System Library:** SdFat with UTF-8 long filename support (`USE_UTF8_LONG_NAMES=1`)
|
||||
- **Driver:** `SDCardManager` from `open-x4-sdk`
|
||||
- **Abstraction:** `HalStorage` provides a singleton (`Storage`) for all file operations -- listing, reading, writing, streaming, and directory management.
|
||||
- **Usage:** EPUB storage, metadata caching to SD, settings persistence, book progress, and file transfer via web server.
|
||||
|
||||
> Source: `lib/hal/HalStorage.h`, `platformio.ini` build flags
|
||||
|
||||
## Battery
|
||||
|
||||
- **Monitoring Pin:** GPIO 0 (`BAT_GPIO0`)
|
||||
- **Library:** `BatteryMonitor` from `open-x4-sdk`
|
||||
- **Output:** Battery percentage (0-100%)
|
||||
- **Access:** Via `HalGPIO::getBatteryPercentage()` or the global `battery` instance in `Battery.h`.
|
||||
|
||||
> Source: `src/Battery.h`, `lib/hal/HalGPIO.h`
|
||||
|
||||
## WiFi / Networking
|
||||
|
||||
- **Radio:** Built-in ESP32-C3 WiFi (802.11 b/g/n, 2.4 GHz)
|
||||
- **Supported Modes:**
|
||||
- **Station (STA):** Connect to an existing wireless network.
|
||||
- **Access Point (AP):** Create a local hotspot for direct device connection.
|
||||
|
||||
### Network Features
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| Web Server | File management UI, book upload/download (`CrossPointWebServer`) |
|
||||
| OTA Updates | HTTPS firmware download and flash (`OtaUpdater`) |
|
||||
| WebSocket | Real-time communication with the web UI (`WebSockets @ 2.7.3`) |
|
||||
| Calibre Connect | Direct wireless connection to Calibre library software |
|
||||
| KOReader Sync | Reading progress sync with KOReader-compatible servers |
|
||||
| OPDS Browser | Browse and download books from OPDS catalog feeds |
|
||||
|
||||
### Power Considerations
|
||||
|
||||
WiFi is power-hungry on the single-core ESP32-C3. The firmware disables WiFi sleep during active transfers and yields CPU time to the network stack when a web server activity is running (`skipLoopDelay()`).
|
||||
|
||||
> Source: `src/network/`, `src/activities/network/`, `platformio.ini` lib_deps
|
||||
|
||||
## USB
|
||||
|
||||
- **Connector:** USB-C
|
||||
- **Connection Detection:** `UART0_RXD` (GPIO 20) reads HIGH when USB is connected.
|
||||
- **Firmware Upload Speed:** 921,600 baud
|
||||
- **Serial Monitor:** 115,200 baud (only initialized when USB is detected)
|
||||
- **CDC on Boot:** Enabled (`ARDUINO_USB_CDC_ON_BOOT=1`)
|
||||
|
||||
> Source: `lib/hal/HalGPIO.h`, `platformio.ini`
|
||||
|
||||
## Power Management
|
||||
|
||||
### Deep Sleep
|
||||
|
||||
- Entered via `HalGPIO::startDeepSleep()` after a configurable inactivity timeout or power button hold.
|
||||
- Wakeup is triggered by the power button GPIO.
|
||||
- Before sleeping, application state (open book, reader position) is persisted to SD card.
|
||||
|
||||
### Wakeup Reasons
|
||||
|
||||
The firmware distinguishes between wakeup causes to decide boot behavior:
|
||||
|
||||
| Reason | Behavior |
|
||||
|---|---|
|
||||
| `PowerButton` | Normal boot -- verifies hold duration before proceeding |
|
||||
| `AfterFlash` | Post-flash boot -- proceeds directly |
|
||||
| `AfterUSBPower` | USB plug caused cold boot -- returns to sleep immediately |
|
||||
| `Other` | Fallback -- proceeds to boot |
|
||||
|
||||
> Source: `lib/hal/HalGPIO.h`, `src/main.cpp`
|
||||
|
||||
## SDK Hardware Libraries
|
||||
|
||||
The `open-x4-sdk` git submodule (community SDK) provides the low-level drivers that the HAL wraps:
|
||||
|
||||
| Library | Path in SDK | Purpose |
|
||||
|---|---|---|
|
||||
| `BatteryMonitor` | `libs/hardware/BatteryMonitor` | Battery voltage reading |
|
||||
| `InputManager` | `libs/hardware/InputManager` | Physical button input |
|
||||
| `EInkDisplay` | `libs/display/EInkDisplay` | E-ink display driver |
|
||||
| `SDCardManager` | `libs/hardware/SDCardManager` | SD card SPI management |
|
||||
|
||||
These are linked into the build as symlinks via `platformio.ini` `lib_deps`.
|
||||
|
||||
> Source: `.gitmodules`, `platformio.ini`
|
||||
216
mod/docs/project-summary.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# CrossPoint Reader -- Project Summary
|
||||
|
||||
> This document is a condensed reference for quickly re-familiarizing with the CrossPoint Reader firmware project. It consolidates hardware specs, codebase layout, build/CI details, architecture patterns, and current mod work. For full detail see `hardware.md`, `file-structure.md`, and `ci-build-and-code-style.md` in this directory.
|
||||
|
||||
## What This Is
|
||||
|
||||
CrossPoint Reader is an open-source, community-driven firmware replacement for the **Xteink X4** e-paper reader. It is **not affiliated with Xteink**. The project's sole mission is focused, high-quality ebook reading -- no apps, games, browsers, or audio. See `SCOPE.md` for the full boundary definition.
|
||||
|
||||
Repository: `https://github.com/crosspoint-reader/crosspoint-reader`
|
||||
License: MIT
|
||||
Governance: `GOVERNANCE.md` (critique code not people, public-by-default decisions, `@daveallie` for moderation)
|
||||
|
||||
## Hardware at a Glance
|
||||
|
||||
| Component | Spec |
|
||||
|---|---|
|
||||
| MCU | ESP32-C3, RISC-V, **single-core** |
|
||||
| RAM | ~380 KB usable (aggressive SD caching required) |
|
||||
| Flash | 16 MB total, DIO mode |
|
||||
| Display | 480x800 e-ink, 2-bit grayscale (4 levels), SPI |
|
||||
| Refresh | FULL (best quality), HALF (~1720ms), FAST (default, custom LUT) |
|
||||
| Frame buffer | 48,000 bytes, single-buffer mode |
|
||||
| Buttons | 7 total: 4 front (remappable), 2 side (page nav), 1 power (deep-sleep wake) |
|
||||
| Storage | SD card via SPI (shared bus with display), SdFat, UTF-8 long filenames |
|
||||
| Battery | GPIO 0, BatteryMonitor SDK lib, 0-100% |
|
||||
| WiFi | 802.11 b/g/n 2.4GHz, STA + AP modes |
|
||||
| USB | USB-C, CDC on boot, connection detection via GPIO 20 |
|
||||
| Power | Deep sleep with GPIO wakeup, configurable inactivity timeout |
|
||||
|
||||
### Flash Partition Layout
|
||||
|
||||
```
|
||||
NVS 0x9000 20KB Key/value storage
|
||||
OTA Data 0xE000 8KB Boot selection
|
||||
ota_0 0x10000 6.25MB Primary firmware
|
||||
ota_1 0x650000 6.25MB Secondary firmware (OTA)
|
||||
SPIFFS 0xC90000 3.375MB On-flash filesystem
|
||||
Coredump 0xFF0000 64KB Crash dumps
|
||||
```
|
||||
|
||||
### Key GPIO Assignments
|
||||
|
||||
Display SPI: SCLK=8, MOSI=10, CS=21, DC=4, RST=5, BUSY=6, MISO=7 (shared with SD)
|
||||
Battery: GPIO 0 | USB detect: GPIO 20
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Application Loop
|
||||
|
||||
Standard Arduino `setup()` / `loop()` in `src/main.cpp`. The loop polls buttons via `HalGPIO`, delegates to the current `Activity`, handles auto-sleep timeout, and power button hold detection. Memory stats are logged every 10s over serial (when USB connected).
|
||||
|
||||
### Activity System
|
||||
|
||||
UI screens are `Activity` subclasses with lifecycle methods `onEnter()`, `onExit()`, `loop()`. Only one activity is active at a time. Routing functions in `main.cpp` (`onGoHome()`, `onGoToReader()`, etc.) manage transitions by deleting the old activity and creating the new one.
|
||||
|
||||
`ActivityWithSubactivity` provides composition for activities that host child activities (menus, overlays).
|
||||
|
||||
### Hardware Abstraction Layer (lib/hal/)
|
||||
|
||||
Three classes wrap the SDK drivers:
|
||||
- `HalDisplay` -- display refresh, buffer ops, grayscale, deep sleep
|
||||
- `HalGPIO` -- buttons, battery, USB detection, deep sleep, wakeup reason
|
||||
- `HalStorage` -- SD card file ops via singleton `Storage`
|
||||
|
||||
### Rendering Pipeline
|
||||
|
||||
`GfxRenderer` wraps `HalDisplay` for drawing operations. Fonts are registered by ID at startup from compiled headers in `lib/EpdFont/builtinFonts/`. The renderer supports a `fadingFix` setting.
|
||||
|
||||
### Data Caching
|
||||
|
||||
EPUB data is aggressively cached to SD under `.crosspoint/epub_<hash>/` to conserve RAM:
|
||||
- `progress.bin` -- reading position
|
||||
- `cover.bmp` -- extracted cover image
|
||||
- `book.bin` -- metadata (title, author, spine, TOC)
|
||||
- `sections/*.bin` -- pre-parsed chapter layout data
|
||||
|
||||
Cache is NOT auto-cleaned on book deletion. Moving a book file resets its cache/progress.
|
||||
|
||||
## Key Source Locations
|
||||
|
||||
### Where to find things
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| Entry point | `src/main.cpp` -- `setup()`, `loop()`, activity routing, font init |
|
||||
| User settings | `src/CrossPointSettings.cpp/.h` -- load/save to SD |
|
||||
| App state | `src/CrossPointState.cpp/.h` -- open book path, sleep tracking |
|
||||
| Button mapping | `src/MappedInputManager.cpp/.h` -- remappable buttons, labels |
|
||||
| Boot screen | `src/activities/boot_sleep/BootActivity.cpp/.h` |
|
||||
| Sleep screen | `src/activities/boot_sleep/SleepActivity.cpp/.h` |
|
||||
| Home screen | `src/activities/home/HomeActivity.cpp/.h` |
|
||||
| Library browser | `src/activities/home/MyLibraryActivity.cpp/.h` |
|
||||
| EPUB reader | `src/activities/reader/EpubReaderActivity.cpp/.h` |
|
||||
| Reader menu | `src/activities/reader/EpubReaderMenuActivity.cpp/.h` |
|
||||
| TXT reader | `src/activities/reader/TxtReaderActivity.cpp/.h` |
|
||||
| XTC reader | `src/activities/reader/XtcReaderActivity.cpp/.h` |
|
||||
| Settings | `src/activities/settings/SettingsActivity.cpp/.h` |
|
||||
| Button remap | `src/activities/settings/ButtonRemapActivity.cpp/.h` |
|
||||
| OTA update UI | `src/activities/settings/OtaUpdateActivity.cpp/.h` |
|
||||
| Web server | `src/network/CrossPointWebServer.cpp/.h` |
|
||||
| OTA client | `src/network/OtaUpdater.cpp/.h` |
|
||||
| HTTP download | `src/network/HttpDownloader.cpp/.h` |
|
||||
| Web UI HTML | `src/network/html/{HomePage,FilesPage,SettingsPage}.html` |
|
||||
| Themes | `src/components/themes/BaseTheme.cpp/.h`, `lyra/LyraTheme.cpp/.h` |
|
||||
| Theme manager | `src/components/UITheme.cpp/.h` |
|
||||
|
||||
### Library code (lib/)
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| EPUB engine | `lib/Epub/` -- parsing, rendering, sections, pages |
|
||||
| EPUB parsers | `lib/Epub/Epub/parsers/` -- container, OPF, HTML, TOC (nav+ncx) |
|
||||
| CSS parser | `lib/Epub/Epub/css/CssParser.cpp/.h` |
|
||||
| Hyphenation | `lib/Epub/Epub/hyphenation/` -- Liang algorithm, 5 language tries |
|
||||
| Display HAL | `lib/hal/HalDisplay.cpp/.h` |
|
||||
| GPIO HAL | `lib/hal/HalGPIO.cpp/.h` |
|
||||
| Storage HAL | `lib/hal/HalStorage.cpp/.h` |
|
||||
| Font rendering | `lib/EpdFont/EpdFont.cpp/.h` |
|
||||
| Built-in fonts | `lib/EpdFont/builtinFonts/` -- Bookerly, NotoSans, OpenDyslexic, Ubuntu |
|
||||
| Graphics | `lib/GfxRenderer/` -- renderer, bitmap, bitmap helpers |
|
||||
| XML parsing | `lib/expat/` |
|
||||
| ZIP/compression | `lib/miniz/`, `lib/ZipFile/` |
|
||||
| JPEG decoding | `lib/picojpeg/`, `lib/JpegToBmpConverter/` |
|
||||
| OPDS parsing | `lib/OpdsParser/` |
|
||||
| KOReader sync | `lib/KOReaderSync/` |
|
||||
| UTF-8 utils | `lib/Utf8/` |
|
||||
|
||||
### SDK (open-x4-sdk/ submodule)
|
||||
|
||||
Provides: `BatteryMonitor`, `InputManager`, `EInkDisplay`, `SDCardManager`
|
||||
Linked as symlinks in `platformio.ini` lib_deps.
|
||||
|
||||
## Build & CI Quick Reference
|
||||
|
||||
### Build locally
|
||||
|
||||
```bash
|
||||
pio run # Dev build (version: 1.0.0-dev)
|
||||
pio run -e gh_release # Release build (version: 1.0.0)
|
||||
pio run --target upload # Build and flash via USB
|
||||
pio check # Run cppcheck static analysis
|
||||
```
|
||||
|
||||
### Code formatting
|
||||
|
||||
```bash
|
||||
./bin/clang-format-fix # Format all tracked C/C++ files
|
||||
./bin/clang-format-fix -g # Format only modified files
|
||||
```
|
||||
|
||||
Key style rules: 2-space indent, 120-char column limit, K&R braces (Attach), pointer-left (`int* x`), sorted/regrouped includes, spaces not tabs, LF line endings.
|
||||
|
||||
Excludes `lib/EpdFont/builtinFonts/` (generated).
|
||||
|
||||
### CI (GitHub Actions)
|
||||
|
||||
On push to `master` or any PR, 4 parallel jobs run:
|
||||
1. **clang-format** -- formatting check (clang-format-21)
|
||||
2. **cppcheck** -- static analysis (fails on low/medium/high)
|
||||
3. **build** -- compilation + firmware.bin artifact
|
||||
4. **test-status** -- aggregated PR gate
|
||||
|
||||
PR titles must follow semantic format (`feat:`, `fix:`, `refactor:`, etc.).
|
||||
|
||||
### Releases
|
||||
|
||||
- **Official:** Push a git tag -> `release.yml` builds `gh_release` env -> uploads bootloader, firmware, ELF, map, partitions
|
||||
- **RC:** Manually trigger `release_candidate.yml` on a `release/*` branch -> embeds commit SHA in version
|
||||
|
||||
### Version scheme
|
||||
|
||||
```
|
||||
1.0.0-dev # default env (local dev)
|
||||
1.0.0 # gh_release env (tagged release)
|
||||
1.0.0-rc+a1b2c3d # gh_release_rc env (release candidate)
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Dependency | Version | Purpose |
|
||||
|---|---|---|
|
||||
| espressif32 | 6.12.0 | PlatformIO platform |
|
||||
| ArduinoJson | 7.4.2 | JSON parsing |
|
||||
| QRCode | 0.0.1 | QR code generation |
|
||||
| WebSockets | 2.7.3 | WebSocket server |
|
||||
|
||||
### Pre-build step
|
||||
|
||||
`scripts/build_html.py` minifies `src/network/html/*.html` -> `.generated.h` PROGMEM strings.
|
||||
|
||||
### Debugging
|
||||
|
||||
```bash
|
||||
python3 scripts/debugging_monitor.py # Linux
|
||||
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101 # macOS
|
||||
```
|
||||
|
||||
Requires: `pyserial`, `colorama`, `matplotlib`.
|
||||
|
||||
## Supported Document Formats
|
||||
|
||||
- **EPUB** (primary) -- EPUB 2 and EPUB 3, CSS subset, hyphenation (EN, DE, ES, FR, RU)
|
||||
- **TXT** -- plain text
|
||||
- **XTC** -- proprietary format with chapter support
|
||||
- Image support within EPUB is listed as incomplete (per README checklist)
|
||||
|
||||
## Constraints to Remember
|
||||
|
||||
- **~380KB RAM** -- everything large must be cached to SD card
|
||||
- **Single-core CPU** -- WiFi and main loop compete for cycles; no true background tasks
|
||||
- **6.25MB firmware slot** -- maximum firmware binary size
|
||||
- **48KB frame buffer** -- 1-bit per pixel in normal mode, 2-bit for grayscale
|
||||
- **Shared SPI bus** -- display and SD card share MISO; cannot access simultaneously
|
||||
- **No PSRAM** -- all memory is internal SRAM
|
||||
- **Font headers are generated** -- do not hand-edit `lib/EpdFont/builtinFonts/` (excluded from formatting)
|
||||
- **HTML headers are generated** -- `.generated.h` files in `src/network/html/` are auto-built from `.html` files
|
||||
123
mod/docs/session-summary.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Session Context Dump (2026-02-09)
|
||||
|
||||
> Everything below is a snapshot of the conversation context at the time of writing. It captures what was explored, what was read, what was produced, and what the current state of work is. Use this to resume without re-exploring.
|
||||
|
||||
## Branch State
|
||||
|
||||
- **Current branch:** `mod/sleep-screen-tweaks`
|
||||
- **Git status:** `.gitignore` modified (unstaged). No other tracked changes. All `mod/docs/` files are new/untracked (the `mod/` directory is gitignored).
|
||||
|
||||
## What Was Done This Session
|
||||
|
||||
1. **Explored the entire project structure** using parallel agents. Mapped every directory, identified all source files, read key headers and configs.
|
||||
|
||||
2. **Created 3 documentation files in `mod/docs/`:**
|
||||
- `hardware.md` -- Full hardware capabilities (CPU, flash partitions, display specs with GPIO pins, buttons with indices, SD card, battery, WiFi, USB, power management, SDK libraries)
|
||||
- `file-structure.md` -- Complete directory tree with descriptions of every file group (src/ activities, components, network; lib/ HAL, EPUB engine, fonts, supporting libs; scripts, tests, docs, .github CI)
|
||||
- `ci-build-and-code-style.md` -- Build system (3 PlatformIO environments), all 4 CI workflows with triggers and steps, release process, clang-format rules table, cppcheck config, PR requirements, contribution guidelines
|
||||
|
||||
3. **Created `project-summary.md`** -- condensed reference consolidating all three docs plus architecture patterns, caching behavior, and constraints.
|
||||
|
||||
## Files Read During Exploration
|
||||
|
||||
The following files were read in full and their contents informed the documentation:
|
||||
|
||||
**Config/root:**
|
||||
- `platformio.ini` -- all build environments, flags, dependencies, SDK symlinks
|
||||
- `partitions.csv` -- flash partition table (6 rows)
|
||||
- `.clang-format` -- 332 lines of formatting rules
|
||||
- `.clangd` -- C++2a standard setting
|
||||
- `.gitmodules` -- open-x4-sdk submodule reference
|
||||
- `README.md` -- project overview, features checklist, install instructions, internals (caching), contributing
|
||||
- `SCOPE.md` -- in-scope/out-of-scope feature boundaries
|
||||
- `GOVERNANCE.md` -- community principles, moderation
|
||||
|
||||
**HAL layer:**
|
||||
- `lib/hal/HalDisplay.h` -- display constants (480x800, buffer size 48000), refresh modes enum, grayscale buffer methods
|
||||
- `lib/hal/HalGPIO.h` -- GPIO pin defines (EPD_SCLK=8, EPD_MOSI=10, EPD_CS=21, EPD_DC=4, EPD_RST=5, EPD_BUSY=6, SPI_MISO=7, BAT_GPIO0=0, UART0_RXD=20), button indices (0-6), WakeupReason enum
|
||||
- `lib/hal/HalStorage.h` -- Storage singleton, file ops API, SDCardManager wrapper
|
||||
|
||||
**Application:**
|
||||
- `src/main.cpp` -- 413 lines: setup/loop, activity routing (onGoHome, onGoToReader, etc.), font initialization (Bookerly 12/14/16/18, NotoSans 12/14/16/18, OpenDyslexic 8/10/12/14, Ubuntu 10/12), power button verification, deep sleep entry, auto-sleep timeout, memory logging
|
||||
- `src/MappedInputManager.h` -- Button enum (Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward), Labels struct, mapLabels()
|
||||
- `src/Battery.h` -- global `BatteryMonitor battery(BAT_GPIO0)` instance
|
||||
|
||||
**CI/GitHub:**
|
||||
- `.github/workflows/ci.yml` -- 4 jobs (clang-format, cppcheck, build, test-status), triggers, steps
|
||||
- `.github/workflows/release.yml` -- tag trigger, gh_release env, 5 artifact files
|
||||
- `.github/workflows/release_candidate.yml` -- workflow_dispatch, release/* branch gate, SHORT_SHA extraction
|
||||
- `.github/workflows/pr-formatting-check.yml` -- semantic PR title check
|
||||
- `.github/PULL_REQUEST_TEMPLATE.md` -- summary, context, AI usage disclosure
|
||||
|
||||
**Scripts:**
|
||||
- `bin/clang-format-fix` -- bash script, git ls-files, grep for C/C++ extensions, excludes builtinFonts/, -g flag for modified-only
|
||||
|
||||
**Directory listings obtained:**
|
||||
- Root, `src/`, `src/Activities/` (full recursive), `lib/` (all subdirectories), `lib/Epub/` (full recursive), `docs/`, `scripts/`, `test/`
|
||||
|
||||
## Current Mod Work Context
|
||||
|
||||
From `mod/docs/todo.md`, two tasks are planned for the sleep screen on branch `mod/sleep-screen-tweaks`:
|
||||
|
||||
**Task 1 -- Gradient fill for letterboxed images:**
|
||||
When a sleep screen image doesn't match the 480x800 display aspect ratio, void/letterbox areas should be filled with a gradient sampled from the nearest ~20 pixels of the image edge. Relevant files:
|
||||
- `src/activities/boot_sleep/SleepActivity.cpp/.h` -- sleep screen rendering logic
|
||||
- `lib/GfxRenderer/BitmapHelpers.cpp/.h` -- bitmap manipulation utilities
|
||||
- `lib/GfxRenderer/Bitmap.cpp/.h` -- bitmap data structure
|
||||
- `lib/hal/HalDisplay.h` -- display dimensions (480x800), buffer operations
|
||||
|
||||
**Task 2 -- Fix "Fit" mode for small images:**
|
||||
In "Fit" mode, images smaller than the display should be scaled UP to fit (maintaining aspect ratio). The current implementation only scales down larger images. Same relevant files as Task 1.
|
||||
|
||||
Neither task has been started yet -- only documentation/exploration was done this session.
|
||||
|
||||
## Key Architectural Details Observed in Source
|
||||
|
||||
**Activity lifecycle (from main.cpp):**
|
||||
- `exitActivity()` calls `onExit()` then `delete` on the current activity
|
||||
- `enterNewActivity()` sets `currentActivity` and calls `onEnter()`
|
||||
- Activities receive `GfxRenderer&` and `MappedInputManager&` in constructors
|
||||
- `ReaderActivity` also receives the epub path and callback functions for navigation
|
||||
|
||||
**Boot sequence (from main.cpp setup()):**
|
||||
1. `gpio.begin()` -- init GPIO/SPI
|
||||
2. Serial init only if USB connected
|
||||
3. `Storage.begin()` -- SD card init (shows error screen on failure)
|
||||
4. Load settings, KOReader store, UI theme
|
||||
5. Check wakeup reason: PowerButton verifies hold duration, AfterUSBPower goes back to sleep, AfterFlash/Other proceed
|
||||
6. Display + font setup
|
||||
7. Show BootActivity (splash)
|
||||
8. Load app state + recent books
|
||||
9. Route to HomeActivity or ReaderActivity based on saved state
|
||||
|
||||
**Main loop (from main.cpp loop()):**
|
||||
1. Poll GPIO
|
||||
2. Apply fadingFix setting to renderer
|
||||
3. Log memory every 10s (when serial active)
|
||||
4. Track activity timer, auto-sleep on configurable timeout
|
||||
5. Check power button hold for manual sleep
|
||||
6. Run `currentActivity->loop()`
|
||||
7. Delay 10ms normally, or `yield()` if activity requests fast response (e.g. web server)
|
||||
|
||||
**Settings system:**
|
||||
- `CrossPointSettings` loaded from SD at boot via `SETTINGS` macro
|
||||
- Includes: `shortPwrBtn` behavior, `getPowerButtonDuration()`, `getSleepTimeoutMs()`, `fadingFix`
|
||||
- `CrossPointState` (`APP_STATE`) tracks: `openEpubPath`, `lastSleepFromReader`, `readerActivityLoadCount`
|
||||
|
||||
**Display details (from HalDisplay.h):**
|
||||
- `DISPLAY_WIDTH` and `DISPLAY_HEIGHT` are `constexpr` sourced from `EInkDisplay`
|
||||
- `DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8` (480/8 = 60)
|
||||
- `BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT` (60 * 800 = 48000)
|
||||
- Grayscale uses separate LSB/MSB buffers
|
||||
- `displayBuffer()` and `refreshDisplay()` take `RefreshMode` and optional `turnOffScreen`
|
||||
- `drawImage()` supports PROGMEM source flag
|
||||
|
||||
## Companion Documentation Files
|
||||
|
||||
All in `mod/docs/`:
|
||||
- `todo.md` -- active mod task list (sleep screen tweaks)
|
||||
- `hardware.md` -- full hardware reference (175 lines)
|
||||
- `file-structure.md` -- complete file tree with descriptions (307 lines)
|
||||
- `ci-build-and-code-style.md` -- build/CI/style reference (281 lines)
|
||||
- `project-summary.md` -- condensed project reference (217 lines)
|
||||
- `session-summary.md` -- this file
|
||||
16
mod/docs/todo.md
Normal file
@@ -0,0 +1,16 @@
|
||||
- [X] Sleep screen tweaks
|
||||
- [ ] Better home screen (covers/recents)
|
||||
- [ ] Firmware flashing screen
|
||||
- [X] Process/render all covers/thumbs when opening book for first time
|
||||
- [ ] Companion Android app
|
||||
- [ ] Smart fill letterbox
|
||||
- [X] Bookmark skeleton
|
||||
- [X] Dictionary skeleton
|
||||
- [ ] Quick menu
|
||||
- [X] Bookmarks
|
||||
- [X] Dictionary
|
||||
- [X] Rotate screen
|
||||
- [ ] Archive/Delete book
|
||||
|
||||
Optional?
|
||||
- [ ] Device bezel offsets
|
||||
335
mod/docs/upstream-sync.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Upstream Sync Guide
|
||||
|
||||
This document describes how to keep `mod/master` synchronized with `upstream/master` (the [crosspoint-reader/crosspoint-reader](https://github.com/crosspoint-reader/crosspoint-reader) repository) without creating duplicate code or divergence that becomes painful to resolve.
|
||||
|
||||
## Branch Model
|
||||
|
||||
```
|
||||
upstream/master ──────●────●────●────●────●────●──── (upstream development)
|
||||
│ │
|
||||
│ │
|
||||
mod/master ──────┴──M1──M2──M3───────────┴──M1'──M2'──M3'────
|
||||
▲ ▲
|
||||
mod features mod features
|
||||
on old base re-applied on new base
|
||||
```
|
||||
|
||||
- **`master`**: Mirror of `upstream/master`. Updated with `git fetch upstream && git merge upstream/master`. Never commit mod code here.
|
||||
- **`mod/master`**: The mod branch. Based on `upstream/master` with mod-exclusive patches applied on top.
|
||||
- **`mod/backup-*`**: Snapshot branches created before each major sync for safety.
|
||||
|
||||
## The Golden Rule
|
||||
|
||||
> **Never cherry-pick individual upstream PRs into `mod/master`.**
|
||||
|
||||
Instead, sync the full `upstream/master` branch and rebase/replay mod patches on top. Cherry-picking creates shadow copies of upstream commits that:
|
||||
|
||||
1. Produce false conflicts when the real PR is later merged upstream
|
||||
2. Diverge silently if upstream amends the PR after merge (e.g., via a follow-up fix)
|
||||
3. Make `git log --left-right` unreliable for tracking what's mod-only vs. upstream
|
||||
4. Accumulate -- after 50 cherry-picks, you have 50 duplicate commits that must be identified and dropped during every future sync
|
||||
|
||||
If an upstream PR is not yet merged and you want its functionality, port the *feature* (adapted to the mod's codebase) as a mod-exclusive commit with a clear commit message referencing the upstream PR number. Do not copy the upstream commit verbatim. Example:
|
||||
|
||||
```
|
||||
feat: port upstream word-width cache optimization
|
||||
|
||||
Adapted from upstream PR #1027 (not yet merged).
|
||||
Re-implemented against mod/master's current text layout code.
|
||||
If/when #1027 is merged upstream, this commit should be dropped
|
||||
during the next sync and the upstream version used instead.
|
||||
```
|
||||
|
||||
## Sync Procedure
|
||||
|
||||
### When to Sync
|
||||
|
||||
- **Weekly** during active upstream development periods
|
||||
- **Immediately** after a major upstream refactor is merged (e.g., ActivityManager, settings migration)
|
||||
- **Before** porting any new upstream PR -- sync first, then port against the latest base
|
||||
|
||||
### Step-by-Step
|
||||
|
||||
#### 1. Prepare
|
||||
|
||||
```bash
|
||||
# Ensure clean working tree
|
||||
git stash # if needed
|
||||
|
||||
# Fetch latest upstream
|
||||
git fetch upstream
|
||||
|
||||
# Update the master mirror
|
||||
git checkout master
|
||||
git merge upstream/master # should always fast-forward
|
||||
git push origin master
|
||||
|
||||
# Create a backup of current mod/master
|
||||
git branch mod/backup-pre-sync-$(date +%Y-%m-%d) mod/master
|
||||
```
|
||||
|
||||
#### 2. Identify What Changed
|
||||
|
||||
```bash
|
||||
# How many new upstream commits since last sync?
|
||||
git rev-list --count mod/master..upstream/master
|
||||
|
||||
# What are they?
|
||||
git log --oneline mod/master..upstream/master
|
||||
|
||||
# Which mod commits are ahead of upstream?
|
||||
git log --oneline upstream/master..mod/master
|
||||
|
||||
# Divergence summary
|
||||
git rev-list --left-right --count mod/master...upstream/master
|
||||
# Output: LEFT RIGHT (LEFT = mod-only commits, RIGHT = new upstream commits)
|
||||
```
|
||||
|
||||
#### 3. Choose a Sync Strategy
|
||||
|
||||
**If divergence is small (< 20 new upstream commits, < 5 mod commits ahead):**
|
||||
|
||||
```bash
|
||||
# Interactive rebase mod/master onto upstream/master
|
||||
git checkout mod/master
|
||||
git rebase -i upstream/master
|
||||
# Resolve conflicts, drop any commits that are now in upstream
|
||||
git push origin mod/master --force-with-lease
|
||||
```
|
||||
|
||||
**If divergence is moderate (20-50 upstream commits, some conflicts expected):**
|
||||
|
||||
```bash
|
||||
# Merge upstream into mod/master
|
||||
git checkout mod/master
|
||||
git merge upstream/master
|
||||
# Resolve conflicts, favoring upstream for any cherry-picked PRs
|
||||
git push origin mod/master
|
||||
```
|
||||
|
||||
**If divergence is severe (50+ upstream commits, major refactors):**
|
||||
|
||||
Use the "fresh replay" approach. This is the nuclear option -- create a new branch from `upstream/master` and manually re-apply every mod feature. It produces the cleanest result but requires the most effort.
|
||||
|
||||
##### Fresh Replay Procedure
|
||||
|
||||
**a. Assessment**
|
||||
|
||||
Before starting, quantify the problem:
|
||||
|
||||
```bash
|
||||
# Divergence stats
|
||||
git rev-list --left-right --count mod/master...upstream/master
|
||||
|
||||
# Conflict preview (count conflict hunks without actually merging)
|
||||
git merge-tree $(git merge-base mod/master upstream/master) mod/master upstream/master \
|
||||
| grep -c "<<<<<<<"
|
||||
|
||||
# Files that both sides modified (highest conflict risk)
|
||||
git merge-tree $(git merge-base mod/master upstream/master) mod/master upstream/master \
|
||||
| grep "^changed in both" | wc -l
|
||||
```
|
||||
|
||||
**b. Classify all mod commits**
|
||||
|
||||
Separate mod-only commits from upstream-ported commits. Each mod commit falls into one of three buckets:
|
||||
|
||||
1. **Upstream duplicate** -- cherry-picked/ported from a PR that is now merged in `upstream/master`. These are dropped entirely; the upstream version takes precedence.
|
||||
2. **Upstream port (still unmerged)** -- ported from a PR that is still open/unmerged upstream. These must be re-applied, adapted to the new upstream code.
|
||||
3. **Mod-exclusive** -- original mod features with no upstream equivalent. These must be re-applied.
|
||||
|
||||
```bash
|
||||
# List all mod-only commits
|
||||
git log --oneline upstream/master..mod/master
|
||||
|
||||
# Find commits referencing upstream PR numbers
|
||||
git log --oneline upstream/master..mod/master | grep -iE "(#[0-9]+|port|upstream)"
|
||||
|
||||
# For each referenced PR, check if it's now in upstream
|
||||
git log upstream/master --oneline | grep "#XXXX"
|
||||
```
|
||||
|
||||
**c. Identify upstream architectural changes**
|
||||
|
||||
Check for major refactors that will affect how mod code must be re-applied. Common high-impact changes include:
|
||||
|
||||
- Activity system changes (e.g., ActivityManager migration -- see `docs/contributing/ActivityManager.md`)
|
||||
- Settings format changes (e.g., binary to JSON migration)
|
||||
- Theme system overhauls
|
||||
- Font rendering pipeline changes
|
||||
- Library replacements (e.g., image decoder swaps)
|
||||
- Class/file renames
|
||||
|
||||
```bash
|
||||
# See what upstream changed (summary)
|
||||
git diff --stat $(git merge-base mod/master upstream/master)...upstream/master | tail -5
|
||||
|
||||
# Look for large refactors
|
||||
git log --oneline upstream/master --not mod/master | grep -iE "(refactor|rename|migrate|replace|remove)"
|
||||
```
|
||||
|
||||
**d. Create the replay branch**
|
||||
|
||||
```bash
|
||||
# Backup current mod/master
|
||||
git branch mod/backup-pre-sync-$(date +%Y-%m-%d) mod/master
|
||||
|
||||
# Create fresh branch from upstream/master
|
||||
git checkout -b mod/master-resync upstream/master
|
||||
```
|
||||
|
||||
**e. Replay in phases (low-risk to high-risk)**
|
||||
|
||||
Work through the mod features in order of conflict risk:
|
||||
|
||||
*Phase 1 -- New files (low risk):* Bring over mod-exclusive files that don't exist in upstream. These are purely additive and rarely conflict. Copy them from the backup branch:
|
||||
|
||||
```bash
|
||||
# Example: bring over a new mod file
|
||||
git checkout mod/backup-pre-sync-YYYY-MM-DD -- src/activities/settings/NtpSyncActivity.cpp
|
||||
git checkout mod/backup-pre-sync-YYYY-MM-DD -- src/activities/settings/NtpSyncActivity.h
|
||||
```
|
||||
|
||||
New files will need adaptation if they depend on APIs that changed upstream (e.g., Activity base class, settings system). Fix compilation errors as you go.
|
||||
|
||||
*Phase 2 -- Core file modifications (high risk):* Re-apply modifications to files that also changed upstream. Do NOT cherry-pick or copy entire files from the old mod branch -- instead, read the old mod's diff for each file and manually re-apply the mod's *intent* against the new upstream code. Key files (in typical priority order):
|
||||
|
||||
- `platformio.ini` -- mod build flags
|
||||
- `src/main.cpp` -- mod activity registration
|
||||
- Settings files -- mod settings in the new format
|
||||
- Activity files modified by mod (HomeActivity, EpubReaderActivity, menus, etc.)
|
||||
- Renderer and HAL files
|
||||
- Theme files
|
||||
|
||||
```bash
|
||||
# See what the mod changed in a specific file vs the merge-base
|
||||
git diff $(git merge-base mod/master upstream/master)...mod/backup-pre-sync-YYYY-MM-DD \
|
||||
-- src/activities/home/HomeActivity.cpp
|
||||
|
||||
# See what upstream changed in that same file
|
||||
git diff $(git merge-base mod/master upstream/master)...upstream/master \
|
||||
-- src/activities/home/HomeActivity.cpp
|
||||
```
|
||||
|
||||
*Phase 3 -- Re-port unmerged upstream PRs:* For upstream PRs that were ported into the mod but aren't yet merged upstream, re-apply each one against the new codebase. Check the ported PR tracking table in `mod/prs/MERGED.md` for context on each port.
|
||||
|
||||
**f. Verify and finalize**
|
||||
|
||||
```bash
|
||||
# Build
|
||||
pio run
|
||||
|
||||
# Check for conflict markers
|
||||
grep -r "<<<<<<" src/ lib/ --include="*.cpp" --include="*.h"
|
||||
|
||||
# Run clang-format
|
||||
./bin/clang-format-fix
|
||||
|
||||
# Verify divergence: LEFT should be mod-only, RIGHT should be 0
|
||||
git rev-list --left-right --count mod/master-resync...upstream/master
|
||||
|
||||
# When satisfied, update mod/master
|
||||
git checkout mod/master
|
||||
git reset --hard mod/master-resync
|
||||
git push origin mod/master --force-with-lease
|
||||
```
|
||||
|
||||
**g. Post-sync cleanup**
|
||||
|
||||
- Update `mod/prs/MERGED.md` -- remove entries for PRs now in upstream, update status for remaining ports
|
||||
- Update mod README with new base commit
|
||||
- Delete the backup branch after confirming everything works on-device
|
||||
|
||||
#### 4. Handle Previously-Ported Upstream PRs
|
||||
|
||||
During rebase or merge, you will encounter conflicts where the mod cherry-picked an upstream PR that has since been merged natively. Resolution:
|
||||
|
||||
- **If the upstream PR is now in `upstream/master`**: Drop the mod's cherry-pick commit entirely. The upstream version takes precedence because it may have been amended by follow-up commits.
|
||||
- **If the upstream PR is still unmerged**: Keep the mod's port, but verify it still applies cleanly against the updated codebase.
|
||||
|
||||
To check which mod commits reference upstream PRs:
|
||||
|
||||
```bash
|
||||
# List mod-only commits that reference upstream PR numbers
|
||||
git log --oneline upstream/master..mod/master | grep -iE "(port|upstream|PR #)"
|
||||
```
|
||||
|
||||
To check if a specific upstream PR has been merged:
|
||||
|
||||
```bash
|
||||
# Check if PR #XXXX is in upstream/master
|
||||
git log upstream/master --oneline | grep "#XXXX"
|
||||
```
|
||||
|
||||
#### 5. Verify
|
||||
|
||||
```bash
|
||||
# Build
|
||||
pio run
|
||||
|
||||
# Check for leftover conflict markers
|
||||
grep -r "<<<<<<" src/ lib/ --include="*.cpp" --include="*.h"
|
||||
|
||||
# Run clang-format
|
||||
./bin/clang-format-fix
|
||||
|
||||
# Verify the divergence is what you expect
|
||||
git rev-list --left-right --count mod/master...upstream/master
|
||||
# LEFT should be only mod-exclusive commits, RIGHT should be 0
|
||||
```
|
||||
|
||||
#### 6. Document
|
||||
|
||||
After a successful sync, update the mod README with the new base upstream commit and note any dropped or reworked ports.
|
||||
|
||||
## Commit Message Conventions for Mod Commits
|
||||
|
||||
Use these prefixes to make mod commits easy to identify and filter during future syncs:
|
||||
|
||||
- `mod:` -- Mod-specific feature or change (e.g., `mod: add clock settings tab`)
|
||||
- `feat:` / `fix:` / `perf:` -- Standard prefixes, but for mod-original work
|
||||
- `port:` -- Feature ported from an unmerged upstream PR (e.g., `port: upstream PR #1027 word-width cache`)
|
||||
|
||||
Always include in the commit body:
|
||||
- Which upstream PR it's based on (if any)
|
||||
- Whether it should be dropped when that PR merges upstream
|
||||
|
||||
## Tracking Ported PRs
|
||||
|
||||
Detailed documentation for each ported PR lives in [`mod/prs/MERGED.md`](../prs/MERGED.md). That file contains full context on what was changed, how it differs from the upstream PR, and notable discussion.
|
||||
|
||||
Additionally, keep the quick-reference status table below up to date during each sync. This table answers the question every sync needs answered: "which ports are still relevant?"
|
||||
|
||||
| Upstream PR | Description | Upstream Status | Competing/Related PRs | Action on Next Sync |
|
||||
|---|---|---|---|---|
|
||||
| #857 | Dictionary word lookup | OPEN | None | Keep until merged upstream |
|
||||
| #1003 | Image placeholders during decode | OPEN | #1291 (MERGED, user image display setting) | Evaluate -- #1291 may cover this |
|
||||
| #1019 | File extensions in file browser | OPEN | #1260 (MERGED, MyLibrary->FileBrowser rename) | Keep, adapt to rename |
|
||||
| #1027 | Word-width cache + hyphenation early exit | OPEN | #1168 (MERGED, fixed-point layout), #873 (MERGED, kerning) | Needs complete rework against new text layout |
|
||||
| #1038 | std::list to std::vector in text layout | MERGED | -- | DROP on next sync (now in upstream) |
|
||||
| #1045 | Shorten "Forget Wifi" labels | MERGED | -- | DROP on next sync (now in upstream) |
|
||||
| #1037 | Decomposed character hyphenation/rendering | MERGED | -- | DROP on next sync (now in upstream) |
|
||||
| #1055 | Byte-level framebuffer writes | OPEN | #1141 (MERGED, wrapped text in GfxRender) | Keep, adapt to GfxRenderer changes |
|
||||
| #1068 | URL hyphenation fix | OPEN | None | Keep until merged upstream |
|
||||
| #1090 | KOReader push progress + sleep | OPEN | #946 (OPEN, sync streamlining) | Evaluate overlap with #946 |
|
||||
| #1185 | KOReader document hash cache | OPEN | #1286 (OPEN, OPDS filename matching) | Keep until merged upstream |
|
||||
| #1209 | Multiple OPDS servers | OPEN | #1214 (OPEN, author folders) | Keep until merged upstream |
|
||||
| #1217 | KOReader sync improvements | OPEN | #946 (OPEN, sync streamlining) | Evaluate overlap with #946 |
|
||||
|
||||
*Last updated: 2026-03-07*
|
||||
|
||||
### How to update this table during a sync
|
||||
|
||||
1. For each row, check if the upstream PR has been merged: `git log upstream/master --oneline | grep "#XXXX"`
|
||||
2. If merged: change Action to "DROP" and remove the port commit during the sync
|
||||
3. If still open: check if competing/related PRs have merged that affect the port
|
||||
4. After the sync: remove dropped rows, add any new ports, update the date
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
1. **Don't cherry-pick upstream commits.** See golden rule above.
|
||||
2. **Don't let mod/master fall more than ~2 weeks behind upstream/master.** The longer you wait, the harder the sync.
|
||||
3. **Don't resolve conflicts by "taking ours" for upstream files without understanding why.** The upstream version is almost always correct for code that isn't mod-specific.
|
||||
4. **Don't merge `mod/master` into `master`.** The `master` branch is a clean mirror of upstream.
|
||||
5. **Don't port an upstream PR without first syncing.** You'll be porting against a stale base, making the next sync harder.
|
||||
6. **Don't create mod feature branches off of `master`.** Always branch from `mod/master`.
|
||||
BIN
mod/mockup.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
mod/preview_cover_480x800.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
mod/preview_thumb_136x226.png
Normal file
|
After Width: | Height: | Size: 397 B |
BIN
mod/preview_thumb_240x400.png
Normal file
|
After Width: | Height: | Size: 701 B |
257
mod/prs/MERGED.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# Merged Upstream PRs
|
||||
|
||||
Tracking document for upstream PRs ported into this mod.
|
||||
|
||||
- [PR #1038](#pr-1038-replace-stdlist-with-stdvector-in-text-layout) — Replace std::list with std::vector in text layout (znelson)
|
||||
- [PR #1045](#pr-1045-shorten-forget-wifi-button-labels-to-fit-on-button) — Shorten "Forget Wifi" button labels (lukestein)
|
||||
- [PR #1037](#pr-1037-fix-hyphenation-and-rendering-of-decomposed-characters) — Fix hyphenation and rendering of decomposed characters (jpirnay)
|
||||
- [PR #1019](#pr-1019-display-file-extensions-in-file-browser) — Display file extensions in File Browser (CaptainFrito)
|
||||
- [PR #1055](#pr-1055-byte-level-framebuffer-writes-for-fillrect-and-axis-aligned-drawline) — Byte-level framebuffer writes for fillRect and drawLine (jpirnay)
|
||||
- [PR #1027](#pr-1027-reduce-parsedtext-layout-time-79-via-word-width-cache-and-hyphenation-early-exit) — Reduce ParsedText layout time 7–9% via word-width cache and hyphenation early exit (jpirnay)
|
||||
- [PR #1068](#pr-1068-correct-hyphenation-of-urls) — Correct hyphenation of URLs (Uri-Tauber)
|
||||
|
||||
---
|
||||
|
||||
## PR #1038: Replace std::list with std::vector in text layout
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1038](https://github.com/crosspoint-reader/crosspoint-reader/pull/1038)
|
||||
- **Author:** znelson (revision of BlindBat's #802)
|
||||
- **Status in upstream:** Open (approved by osteotek, awaiting second review from daveallie)
|
||||
- **Method:** Manual port (partial -- incremental fixes only)
|
||||
|
||||
### Context
|
||||
|
||||
The core list-to-vector conversion was already performed in a prior mod sync. This port only brings in two targeted fixes from the PR that were missing locally.
|
||||
|
||||
### Changes applied
|
||||
|
||||
1. **Erase consumed words in `layoutAndExtractLines`** (`lib/Epub/Epub/ParsedText.cpp`): Added `.erase()` calls after the line extraction loop to remove consumed words from `words`, `wordStyles`, `wordContinues`, and `forceBreakAfter` vectors. Without this, the 750-word early flush threshold fires on every subsequent `addWord` call instead of only ~3 times per large paragraph, causing ~1,670 redundant flushes.
|
||||
2. **Fix `wordContinues` flag in `hyphenateWordAtIndex`** (`lib/Epub/Epub/ParsedText.cpp`): Corrected the attach-to-previous flag handling when splitting a word at a hyphenation point. The prefix now keeps its original flag and the remainder gets `false`, instead of the previous behavior that cleared the prefix's flag and transferred it to the remainder. This fix was identified by coderabbit review and accepted in commit `9bbe994`.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
- The `forceBreakAfter` vector erase was added (mod-only vector not present in upstream) alongside the three upstream vectors.
|
||||
- The upstream PR's full list-to-vector conversion (TextBlock.h/.cpp, ParsedText.h/.cpp) was already done in a prior mod sync. Only the two fixes above were new.
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- znelson posted detailed benchmarks comparing list vs vector on ESP32-C3 with "Intermezzo" by Sally Rooney (chapter 14, 2,420-word paragraph): 11% faster chapter parse time, 89% more heap headroom (~50KB saved), 10-14% faster layout for medium paragraphs.
|
||||
- The erase cost is ~90-133 microseconds per flush (0.1% of total flush time).
|
||||
|
||||
---
|
||||
|
||||
## PR #1045: Shorten "Forget Wifi" button labels to fit on button
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1045](https://github.com/crosspoint-reader/crosspoint-reader/pull/1045)
|
||||
- **Author:** lukestein
|
||||
- **Status in upstream:** Open (approved by osteotek)
|
||||
- **Resolves:** upstream issue #1035
|
||||
- **Method:** Manual port (direct string changes)
|
||||
|
||||
### Changes applied
|
||||
|
||||
Updated `STR_FORGET_BUTTON` in all 9 translation yaml files under `lib/I18n/translations/`:
|
||||
|
||||
|
||||
| Language | Before | After |
|
||||
| ---------- | ------------------- | ------------ |
|
||||
| Czech | "Zapomenout na síť" | "Zapomenout" |
|
||||
| English | "Forget network" | "Forget" |
|
||||
| French | "Oublier le réseau" | "Oublier" |
|
||||
| German | "WLAN entfernen" | "Entfernen" |
|
||||
| Portuguese | "Esquecer rede" | "Esquecer" |
|
||||
| Romanian | "Uitaţi reţeaua" | "Uitaţi" |
|
||||
| Russian | "Забыть сеть" | "Забыть" |
|
||||
| Spanish | "Olvidar la red" | "Olvidar" |
|
||||
| Swedish | "Glöm nätverk" | "Glöm" |
|
||||
|
||||
|
||||
### Prerequisite
|
||||
|
||||
Romanian translation file (`romanian.yaml`) was pulled from `upstream/master` (merged PR #987 by ariel-lindemann) as it was missing locally.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
None. Changes are identical to the upstream PR.
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- ariel-lindemann requested the Romanian shortening be included (was added in a follow-up commit by lukestein).
|
||||
- Translations were verified via Google Translate (lukestein is not a native speaker of non-English languages).
|
||||
|
||||
---
|
||||
|
||||
## PR #1037: Fix hyphenation and rendering of decomposed characters
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1037](https://github.com/crosspoint-reader/crosspoint-reader/pull/1037)
|
||||
- **Author:** jpirnay
|
||||
- **Status in upstream:** Open (no approvals yet; coderabbit reviewed with one actionable comment, resolved)
|
||||
- **Related to:** upstream issue #998 (Minor hyphenation and text flow issues)
|
||||
- **Method:** Manual port (4 files)
|
||||
|
||||
### Changes applied
|
||||
|
||||
1. `**lib/Utf8/Utf8.h`**: Added `utf8IsCombiningMark(uint32_t)` inline function that identifies Unicode combining diacritical marks (ranges: 0x0300-0x036F, 0x1DC0-0x1DFF, 0x20D0-0x20FF, 0xFE20-0xFE2F).
|
||||
2. `**lib/EpdFont/EpdFont.cpp**`: Added combining mark positioning in `getTextBounds`. Tracks last base glyph state (`lastBaseX`, `lastBaseAdvance`, `lastBaseTop`, `hasBaseGlyph`). Combining marks are centered over the base glyph's advance width and raised with a minimum 1px gap. Cursor does not advance for combining marks.
|
||||
3. `**lib/Epub/Epub/hyphenation/HyphenationCommon.cpp**`: Added NFC-like precomposition in `collectCodepoints()`. When a combining diacritic follows a base character and a precomposed Latin-1/Latin-Extended scalar exists (grave, acute, circumflex, tilde, diaeresis, cedilla), the base is replaced with the precomposed form and the combining mark is skipped. This allows Liang hyphenation patterns to match decomposed text correctly.
|
||||
4. `**lib/GfxRenderer/GfxRenderer.cpp**`: Added combining mark handling to `drawText`, `drawTextRotated90CW`, `drawTextRotated90CCW`, and `getTextAdvanceX`. Each text drawing function tracks base glyph state and renders combining marks at adjusted (raised, centered) positions without advancing the cursor. `getTextAdvanceX` skips combining marks entirely.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
- `**drawTextRotated90CCW**`: This is a mod-only function (not present in upstream). Combining mark handling was added here following the same pattern as the CW rotation, with coordinate adjustments inverted for CCW direction (`+raiseBy` on X, `+lastBaseAdvance/2` on Y instead of negatives).
|
||||
- The upstream PR initially had a duplicate local `isCombiningMark` function in `EpdFont.cpp` (anonymous namespace) which was flagged by coderabbit and replaced with the shared `utf8IsCombiningMark` from `Utf8.h`. Our port uses the shared function directly throughout.
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- jpirnay noted this is not a 100% bullet-proof implementation -- it maps common base+combining sequences rather than implementing full Unicode NFC normalization.
|
||||
- The render fix is more universal: it properly x-centers the compound glyph over the previous one and uses at least 1pt visual distance in Y.
|
||||
- lukestein specifically expressed interest in the fix for hyphenation of already-hyphenated words (e.g., "US-Satellitensystem" was exclusively broken at the existing hyphen).
|
||||
- osteotek approved the approach of breaking already-hyphenated words at additional Liang pattern points.
|
||||
|
||||
---
|
||||
|
||||
## PR #1019: Display file extensions in File Browser
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1019](https://github.com/crosspoint-reader/crosspoint-reader/pull/1019)
|
||||
- **Author:** CaptainFrito
|
||||
- **Status in upstream:** Open (no approvals; Eloren1 asked about long filename behavior)
|
||||
- **Method:** Manual port (1 file)
|
||||
|
||||
### Changes applied
|
||||
|
||||
In `src/activities/home/MyLibraryActivity.cpp`:
|
||||
|
||||
1. Added `getFileExtension(std::string)` helper function near the existing `getFileName`. Returns the file extension including the dot (e.g., ".epub"), or empty string for directories or files without extensions.
|
||||
2. Updated the `GUI.drawList` call to pass the extension lambda as the `rowValue` parameter and `false` for `highlightValue`. The `drawList` signature already supported these parameters with defaults.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
None. The implementation is functionally identical.
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- CaptainFrito questioned whether the multiple lambda approach (each independently indexing into the files array) is optimal, suggesting a single function returning a struct might be better.
|
||||
- Eloren1 asked how long filenames look with extensions displayed. This remains an open question in the upstream PR. **See mod enhancement below.**
|
||||
- The PR was force-pushed to squash commits.
|
||||
|
||||
### Mod enhancement: Expandable selected row for long filenames
|
||||
|
||||
Addresses Eloren1's concern about long filenames. When the selected row's filename overflows the available text width, the row expands to 2 lines with smart text wrapping. The file extension moves to the bottom-right of the expanded area, baseline-aligned with the last text line. Non-selected rows retain single-line truncation. If the filename still overflows after 2 lines, the second line is truncated with "...".
|
||||
|
||||
**Files modified:**
|
||||
|
||||
- `src/components/themes/BaseTheme.cpp`: Added `wrapTextToLines` utility function with 3-tier break logic and `truncateWithEllipsis` helper. Modified `drawList` to detect selected-row overflow, draw expanded selection highlight at 2x row height, render wrapped title with row-height line spacing, and position the file extension on line 2 (right-aligned).
|
||||
- `src/components/themes/lyra/LyraTheme.cpp`: Same helpers and analogous `drawList` modifications with Lyra-specific styling (rounded-rect selection highlight, icon aligned with line 1, scroll bar awareness).
|
||||
|
||||
**Design decisions:**
|
||||
|
||||
- Only the selected/highlighted row expands; other rows with long filenames continue to truncate normally.
|
||||
- Expansion triggers when the title overflows the value-reduced text width (i.e., when it would be truncated in single-line mode). For titles that overflow the value area but still fit the full width, the expanded row shows the full title on line 1 with the extension on line 2 below.
|
||||
- 3-tier text wrapping for natural line breaks:
|
||||
1. Preferred delimiters: breaks at " -- ", " - ", en-dash, or em-dash separators (common "Title - Author" naming convention). Uses last occurrence to maximize line 1 content.
|
||||
2. Word boundaries: breaks at last space or hyphen that fits on line 1.
|
||||
3. Character-level fallback: for long unbroken tokens without spaces or hyphens.
|
||||
- Each wrapped line is placed at the same Y position as a normal list row (row-height spacing between lines), giving natural visual separation that matches the list's vertical rhythm.
|
||||
- File extension is always positioned on line 2 of the expanded area (right-aligned), regardless of how many text lines are produced by wrapping.
|
||||
- Icons in LyraTheme are aligned with line 1 (not centered in the expanded area).
|
||||
- Pagination uses `effectivePageItems` (pageItems - 1 when expanding) for totalPages, scroll indicators, and page boundaries. This ensures hidden items appear on the next page with proper scroll indicators. To prevent items from the previous page "leaking" into view, the page start is clamped to never go before the original (non-expanded) page boundary. Edge case: when the selected item is the last on the original page, the start shifts forward minimally to keep it visible.
|
||||
- Boundary item duplication: when navigating to a "real" page boundary (non-expanding path), if the item just before the page start would need expansion when selected, it is included at the top of the current page (the page start is decremented by 1). This prevents the "bumped" item from vanishing when the user navigates from it to the next page. The guard `selectedIndex < pageStartIndex + pageItems - 1` ensures the selected item isn't pushed off the page by this adjustment.
|
||||
|
||||
---
|
||||
|
||||
## PR #1055: Byte-level framebuffer writes for fillRect and axis-aligned drawLine
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1055](https://github.com/crosspoint-reader/crosspoint-reader/pull/1055)
|
||||
- **Author:** jpirnay
|
||||
- **Status in upstream:** Open (coderabbit reviewed, no approvals yet)
|
||||
- **Method:** Manual port (2 files)
|
||||
|
||||
### Context
|
||||
|
||||
Eliminates per-pixel `drawPixel` calls for solid fills, axis-aligned lines, and dithered fills by writing directly to the framebuffer at byte granularity. Exploits the fact that one logical dimension always maps to a contiguous physical row in the framebuffer, allowing entire spans to be written with byte masking and `memset` instead of individual read-modify-write cycles per pixel. Measured 232–470x speedups on ESP32-C3 hardware for full-screen operations.
|
||||
|
||||
### Changes applied
|
||||
|
||||
1. **`lib/GfxRenderer/GfxRenderer.h`**: Added two new private methods: `fillPhysicalHSpanByte(int phyY, int phyX_start, int phyX_end, uint8_t patternByte)` for writing a patterned horizontal span with byte-level edge blending, and `fillPhysicalHSpan(int phyY, int phyX_start, int phyX_end, bool state)` as a thin solid-fill wrapper.
|
||||
2. **`lib/GfxRenderer/GfxRenderer.cpp`**: Added `#include <cstring>` for `memset`. Implemented `fillPhysicalHSpanByte` (bounds clamping, MSB-first bit packing, partial-byte masking at edges, memset for aligned middle) and `fillPhysicalHSpan` wrapper.
|
||||
3. **`drawLine` (axis-aligned cases)**: Logical vertical lines in Portrait/PortraitInverted now route through `fillPhysicalHSpan` instead of per-pixel loops. Logical horizontal lines in Landscape orientations similarly use `fillPhysicalHSpan`. Bresenham diagonal path is unchanged.
|
||||
4. **`fillRect`**: Replaced the per-row `drawLine` loop with orientation-specific fast paths. Each orientation iterates over the logical dimension that maps to a constant physical row, writing the perpendicular extent as a single `fillPhysicalHSpan` call.
|
||||
5. **`fillRectDither`**: Replaced per-pixel `drawPixelDither` loops for DarkGray and LightGray with orientation-aware `fillPhysicalHSpanByte` calls using pre-computed byte patterns. DarkGray uses checkerboard `0xAA`/`0x55` alternating by physical row parity. LightGray uses 1-in-4 pattern with row-skipping optimization for all-white rows.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
- **`fillPolygon` landscape optimization (coderabbit nitpick)**: The upstream PR's coderabbit review noted that `fillPolygon`'s horizontal scanline inner loop could benefit from `fillPhysicalHSpan` for Landscape orientations. This was noted as a "future optimization opportunity" in the review and not implemented in the PR. We applied it here: for `LandscapeCounterClockwise` and `LandscapeClockwise`, the scanline fill now uses `fillPhysicalHSpan` with appropriate coordinate transforms, falling back to per-pixel for Portrait orientations (where horizontal logical lines map to vertical physical lines).
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- jpirnay posted hardware benchmarks: `fillRect(480×800)` went from 125,577 µs to 519 µs (242× speedup), vertical `drawLine(800px)` from 261 µs to 3 µs (87×), `fillRectDither` DarkGray 234× speedup, LightGray 470× speedup.
|
||||
- coderabbit review approved all core changes with 8 LGTM comments; the only nitpick was the `fillPolygon` optimization opportunity (applied in this port).
|
||||
- The `fillRectDither` LightGray change has a subtle semantic difference from upstream: the original `drawPixelDither<LightGray>` only wrote dark pixels (leaving white untouched), while the new span-based approach explicitly writes the full pattern. For `fillRectDither` (which fills a complete rectangle) this is semantically equivalent.
|
||||
|
||||
---
|
||||
|
||||
## PR #1027: Reduce ParsedText layout time 7–9% via word-width cache and hyphenation early exit
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1027](https://github.com/crosspoint-reader/crosspoint-reader/pull/1027)
|
||||
- **Author:** jpirnay
|
||||
- **Status in upstream:** Open (no approvals; osteotek requested separate benchmarks for cache vs early exit)
|
||||
- **Method:** Manual port (1 file, adapted for vector-based code)
|
||||
|
||||
### Context
|
||||
|
||||
Reduces CPU time spent in `ParsedText::layoutAndExtractLines`, the hot path for every page render on the ESP32. Two independent optimizations contribute: a direct-mapped word-width cache and an early exit in the hyphenation breakpoint loop. Benchmarked on device (50 iterations, 474 px justified viewport) showing 5–9% improvement depending on corpus and layout mode.
|
||||
|
||||
### Changes applied
|
||||
|
||||
1. **Word-width cache** (`lib/Epub/Epub/ParsedText.cpp`): Added a 128-entry, 4 KB static array (BSS, not heap) that caches `getTextAdvanceX` results keyed by `(word, fontId, style)`. Lookup is O(1) via FNV-1a hash + bitmask slot selection. Words >= 24 bytes bypass the cache (uncommon, unlikely to repeat). Hyphen-fragment measurements (never repeated) skip the cache entirely. `calculateWordWidths` now calls `cachedMeasureWordWidth` instead of `measureWordWidth`.
|
||||
2. **Hyphenation early exit** (`lib/Epub/Epub/ParsedText.cpp`): In `hyphenateWordAtIndex`, when a candidate prefix is wider than the available space, the loop now `break`s instead of `continue`ing. `Hyphenator::breakOffsets` returns candidates in ascending byte-offset order, so prefix widths are non-decreasing -- all subsequent candidates will also be too wide. A reusable `std::string prefix` buffer replaces per-iteration `substr` allocations.
|
||||
3. **Reserve hint** (`lib/Epub/Epub/ParsedText.cpp`): Added `lineBreakIndices.reserve(totalWordCount / 8 + 1)` in `computeLineBreaks` to avoid repeated reallocation.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
- **List-specific optimizations not applicable**: The upstream PR includes `std::list` splice optimizations in `extractLine` and iterator changes (`std::advance` to `std::next`) throughout. Our mod already uses `std::vector` (from PR #1038), so these changes don't apply -- vector index access and move iterators are already in place.
|
||||
- **`continuesVec` sync removed**: The upstream PR updates a separate `continuesVec` pointer in `hyphenateWordAtIndex`. Our mod modifies `wordContinues` directly (it's already a vector), so this indirection is unnecessary.
|
||||
- **Benchmark infrastructure excluded**: The PR's final commit removed `ParsedTextLegacy.h/.cpp`, `ParsedTextBenchmark.h/.cpp`, and `main.cpp` benchmark hooks. These were development-only files not part of the deliverable.
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- osteotek noted he had previously tried a word-width cache with "almost zero on-device improvements" and requested separate benchmarks for each optimization.
|
||||
- jpirnay posted individual contribution data: the cache dominates (7–8% for DP layout, 3–4% for greedy) while the early exit contributes 1–2%. The cache saves a `getTextAdvanceX` call on every word in `calculateWordWidths` (56–61 calls/iteration), whereas the early exit only fires on the handful of words per paragraph that trigger hyphenation.
|
||||
- jpirnay's benchmark table (50 iterations, 474 px justified viewport):
|
||||
|
||||
| Scenario | Early exit only | Cache only (derived) | Both combined |
|
||||
| --------------- | --------------- | -------------------- | ------------- |
|
||||
| German DP | −1% | ~−7% | −8% |
|
||||
| English DP | −1% | ~−8% | −9% |
|
||||
| Combined DP | −1% | ~−8% | −9% |
|
||||
| German greedy | −2% | ~−3% | −5% |
|
||||
| English greedy | −2% | ~−4% | −6% |
|
||||
| Combined greedy | −2% | ~−3% | −5% |
|
||||
|
||||
---
|
||||
|
||||
## PR #1068: Correct hyphenation of URLs
|
||||
|
||||
- **URL:** [https://github.com/crosspoint-reader/crosspoint-reader/pull/1068](https://github.com/crosspoint-reader/crosspoint-reader/pull/1068)
|
||||
- **Author:** Uri-Tauber
|
||||
- **Status in upstream:** Open (coderabbit reviewed with one nitpick, no approvals yet)
|
||||
- **Related to:** upstream issue #1066 (Some epubs crash Crosspoint — long URLs cause rendering issues)
|
||||
- **Method:** Manual port (2 files)
|
||||
|
||||
### Context
|
||||
|
||||
Long URLs in EPUBs could not be line-wrapped because `buildExplicitBreakInfos` required alphabetic characters on both sides of an explicit hyphen marker. URLs contain `/`, digits, dots, and other non-alphabetic characters, so path separators were never recognized as break points.
|
||||
|
||||
### Changes applied
|
||||
|
||||
1. **`lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`**: Added `case '/':` to `isExplicitHyphen`, treating the URL path separator as an explicit hyphen delimiter alongside existing hyphen/dash characters.
|
||||
2. **`lib/Epub/Epub/hyphenation/Hyphenator.cpp`**: Split the single combined filter in `buildExplicitBreakInfos` into a two-stage check. The `isExplicitHyphen` test is applied first. Then, for `/` and `-` specifically, the strict requirement that both adjacent codepoints must be alphabetic is relaxed — these separators can break next to digits, dots, or other URL characters. All other explicit hyphens retain the original TeX-style alphabetic-surround rule.
|
||||
|
||||
### Differences from upstream PR
|
||||
|
||||
- **Repeated-separator guard (mod enhancement)**: Added a guard from the coderabbit review nitpick (not yet addressed in the upstream PR) that skips break points between consecutive identical separators. This prevents a line break from being inserted between the two slashes in `http://` or between double hyphens like `--`.
|
||||
|
||||
### Notable PR discussion
|
||||
|
||||
- coderabbit approved the `isExplicitHyphen` change and flagged the repeated-separator edge case as a nitpick. The PR author has not yet addressed it.
|
||||
- The PR resolves the URL portion of issue #1066, where MIT Press EPUBs with long URLs caused rendering problems.
|
||||
48
mod/prs/sleep-screen-tweaks.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# feat: Sleep screen letterbox fill and image upscaling
|
||||
|
||||
**Branch:** `mod/sleep-screen-tweaks`
|
||||
|
||||
## Summary
|
||||
|
||||
* **What is the goal of this PR?**
|
||||
Improve the sleep screen experience for cover images that don't match the display's aspect ratio, and fix Fit mode for images smaller than the screen.
|
||||
|
||||
* **What changes are included?**
|
||||
- Configurable letterbox fill for sleep screen cover images. Four fill modes:
|
||||
- **Solid** — computes the dominant (average) shade from the image edge and fills the entire letterbox area with that single dithered color.
|
||||
- **Blended** — fills using per-pixel sampled edge colors with noise dithering (no distance-based interpolation), producing a smooth edge-matching fill that varies along the image border.
|
||||
- **Gradient** — interpolates per-pixel edge colors toward a configurable target color (white or black) over distance, creating a fade effect.
|
||||
- **None** — plain white letterbox (original behavior).
|
||||
- Image upscaling in Fit mode: images smaller than the display are now scaled up to fit while preserving aspect ratio. Previously, small images were simply centered without scaling.
|
||||
- Edge data caching: the edge-sampling pass (which reads the full bitmap) is performed once per cover and cached as a compact binary file (~1KB) alongside the cover BMP in `.crosspoint/`. Subsequent sleeps load from cache, skipping the bitmap scan entirely. Cache is validated against screen dimensions and auto-regenerated when stale.
|
||||
- Two new user-facing settings:
|
||||
- **Letterbox Fill** — None / Solid / Blended / Gradient
|
||||
- **Gradient Direction** — To White / To Black
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `lib/GfxRenderer/GfxRenderer.cpp` | Unified block-fill scaling in `drawBitmap`/`drawBitmap1Bit` supporting both upscaling and downscaling; added `drawPixelGray` helper |
|
||||
| `lib/GfxRenderer/GfxRenderer.h` | `drawPixelGray` declaration |
|
||||
| `lib/GfxRenderer/BitmapHelpers.cpp` | Added `quantizeNoiseDither` — hash-based noise dithering for gradient/solid fills |
|
||||
| `lib/GfxRenderer/BitmapHelpers.h` | `quantizeNoiseDither` declaration |
|
||||
| `src/activities/boot_sleep/SleepActivity.cpp` | Edge sampling (`sampleBitmapEdges`), letterbox fill rendering (`drawLetterboxFill`), edge data cache (`loadEdgeCache`/`saveEdgeCache`), integration in `renderBitmapSleepScreen`/`renderCoverSleepScreen`; unconditional aspect-ratio scaling for Fit mode |
|
||||
| `src/activities/boot_sleep/SleepActivity.h` | Updated `renderBitmapSleepScreen` signature with optional `edgeCachePath` parameter |
|
||||
| `src/CrossPointSettings.h` | New enums `SLEEP_SCREEN_LETTERBOX_FILL` (4 values) and `SLEEP_SCREEN_GRADIENT_DIR`; new fields `sleepScreenLetterboxFill`, `sleepScreenGradientDir` |
|
||||
| `src/CrossPointSettings.cpp` | Serialization of new settings fields (incremented `SETTINGS_COUNT`) |
|
||||
| `src/SettingsList.h` | New "Letterbox Fill" and "Gradient Direction" setting entries in Display category |
|
||||
|
||||
## Additional Context
|
||||
|
||||
* **Performance:** The edge sampling pass reads the full bitmap row-by-row (~48KB for a full-screen 2-bit image). This is done once per cover image and cached. Subsequent sleeps for the same cover skip this pass entirely, loading ~1KB from the cache file instead.
|
||||
* **Memory:** Edge sampling uses temporary `uint32_t` accumulator arrays (~10KB peak) which are freed immediately after processing. Final edge data arrays (~1.6KB max) persist only for the duration of rendering. The cache file is ~970 bytes for a 480-wide image.
|
||||
* **Upscaling approach:** Nearest-neighbor via block-fill — each source pixel maps to a rectangle of destination pixels. No interpolation, which is appropriate for the 4-level grayscale e-ink display.
|
||||
* **Cache invalidation:** Validated by cache version byte and screen dimensions (width × height). Orientation changes trigger regeneration. Each cover mode (Fit vs Crop) produces a different BMP and thus a different cache path. Cache files live in the book-specific `.crosspoint/epub_<hash>/` directory and are naturally scoped per-book.
|
||||
* **Potential risks:** The `drawBitmap`/`drawBitmap1Bit` scaling refactor affects all bitmap rendering, not just sleep screens. The logic is functionally equivalent for downscaling (≤1.0) and 1:1 cases; upscaling (>1.0) is new behavior only triggered when both `maxWidth` and `maxHeight` are provided.
|
||||
|
||||
---
|
||||
|
||||
### AI Usage
|
||||
|
||||
Did you use AI tools to help write this code? _**YES**_
|
||||