vTaskDelete(nullptr) does not unwind the C++ stack, so the RAII
HalPowerManager::Lock destructor never ran -- permanently holding the
lock and preventing low-power mode. Fix by wrapping the lock in a block
scope that exits before vTaskDelete().
Revert the setPowerSaving(false) calls in the clock refresh block; the
RenderLock already handles CPU frequency during display updates.
Made-with: Cursor
BootNtpSync now acquires a HalPowerManager::Lock for the entire WiFi/NTP
task lifecycle, keeping the CPU at full speed during scan, connect, sync,
and teardown. The clock refresh logic in the main loop now explicitly
restores CPU frequency and resets the activity timer before requesting a
render, preventing display SPI operations from running at 10 MHz.
Made-with: Cursor
In landscape CW/CCW the physical buttons are on the left/right side,
not the bottom. Reserve a horizontal gutter (sideButtonHintsWidth)
on the appropriate side and remove the bottom buttonHintsHeight
padding, following the established pattern from other activities.
Made-with: Cursor
In landscape orientation the cover is pinned on the left panel
(filling the content height) while metadata fields scroll
independently on the right. Portrait layout is unchanged.
Made-with: Cursor
Draw header after content with a white fill over the header zone,
so scrolled cover images and text slide behind the header instead
of rendering on top of it. Removed incorrect maxHeight clamping
that caused the cover to shrink rather than scroll.
Made-with: Cursor
Draw the standard header (clock, battery, title) via GUI.drawHeader().
Replace hardcoded MARGIN with theme metrics for content positioning.
Content area now starts below the header and stops above button hints
so the last lines are never obscured.
Made-with: Cursor
Replace 13 per-accessor static std::string blank locals with a single
file-scope kBlank (~384 bytes DRAM saved). Add Epub::getMetadata()
returning the full BookMetadata struct. Refactor buildLayout from 14
individual parameters to a single BookMetadata const ref + fileSize.
Made-with: Cursor
Add parsing for dc:publisher, dc:date, dc:subject, dc:rights,
dc:contributor, dc:identifier (prefers ISBN scheme), and
calibre:rating. All new fields serialized in BookMetadataCache
(version bumped to 7) and displayed in BookInfoActivity with
rating shown as N/5 scale.
Made-with: Cursor
RecentBooksActivity was passing initialSkipRelease=false when opening
BookManageMenuActivity via long-press Confirm. The button release
event was consumed by the menu as a selection of the first item
(Book Info). Pass true to match HomeActivity's existing behavior.
Made-with: Cursor
When BookInfo is opened for a book with no existing cache,
epub.load(true, true) triggers a 1-2s full parse. Show a
"Loading..." popup with progress bar so the device doesn't
appear frozen. Popup only appears on the fallback path —
cached books load silently. Same pattern for XTC books.
Made-with: Cursor
- Fix BookInfo buttons: Left/Right front = scroll down/up, Confirm = no-op,
side buttons retained. Separate Up/Down hints on btn3/btn4.
- Fallback load: try epub.load(true, true) when cache-only load fails,
so Book Info works for unopened books.
- Add "Book Info" to ManageBook menu (BOOK_INFO action) with handlers in
all 4 result sites (Home, Recent, FileBrowser, Reader).
- Fix HomeActivity cover regen: call generateCoverBmp(false) + validate
with isValidThumbnailBmp before falling to placeholder, matching the
reader's multi-tier fallback pipeline. Same for XTC branch.
Made-with: Cursor
Ports upstream PR #1342 (feat: Add Book Info screen, richer metadata,
and safer file-browser controls) with mod-specific adaptations:
- Parse and cache series, seriesIndex, description from EPUB OPF
- Bump book.bin cache version to 6 for new metadata fields
- Add BookInfoActivity (new screen) accessible via Right button in FileBrowser
- Add ManageBook menu via Left button in FileBrowser (replaces upstream hidden delete)
- Guard all delete/archive actions with ConfirmationActivity (10 call sites)
- Add inputArmed gating to ConfirmationActivity to prevent accidental confirmation
- Safe deserialization: readString now returns bool with MAX_STRING_LENGTH guard
- Add series field to RecentBooksStore with JSON and binary serialization
- Add i18n keys: STR_BOOK_INFO, STR_AUTHOR, STR_SERIES, STR_FILE_SIZE, etc.
Made-with: Cursor
PR #1311: Replace separate spaceWidth + getSpaceKernAdjust() with a
single getSpaceAdvance() that combines space glyph advance and kerning
in fixed-point before snapping to pixels, eliminating +/-1 px rounding
drift in text layout.
PR #1322: Add early exit to fillUncompressedSizes() once all target
entries are matched, avoiding unnecessary central directory traversal.
Also updates tracking docs and verifies PR #1329 (reader utils refactor)
matches upstream after merge.
Made-with: Cursor
Re-add DynamicEnum entries for preferredPortrait/preferredLandscape in
Settings with JSON persistence. Restore long-press Confirm on the
reader menu's orientation toggle to open an inline sub-menu with all
4 orientation options.
Made-with: Cursor
1. Clear ignoreNextConfirmRelease after transferring state to child
activity, so the next Confirm press isn't silently consumed.
2. Add conditional FOOTNOTES entry to reader menu when the current
book has footnotes.
3. Guard clock-minute requestUpdate() with !isReaderActivity() to
prevent full e-ink re-renders every minute while reading.
Made-with: Cursor
Add detailed entries to MERGED.md for all 5 ported PRs with context,
changes applied, and differences from upstream. Update upstream-sync.md
tracking table with new entries and sync date.
Made-with: Cursor
When loading a section, proactively indexes all spine items belonging
to the same TOC chapter so page-turning across spine boundaries within
a chapter is instant. Uses Section::readCachedPageCount() to skip
already-cached sections and shows an "Indexing (x/y)" progress popup.
Ported from upstream PR #1172, adapted to mod architecture.
Made-with: Cursor
Long-press chapter skip now walks by TOC entries instead of spine
indices, enabling finer navigation in books with multi-chapter spines.
Status bar chapter title now uses section-level getTocIndexForPage()
for accurate subchapter display. Chapter selection passes tocIndex
back so the reader can jump directly to the right page within a spine.
Add pendingTocIndex to EpubReaderActivity for deferred cross-spine
TOC navigation, resolved after the target section loads.
Ported from upstream PRs #1143 and #1172, adapted to mod architecture.
Made-with: Cursor
Extend Section with TOC boundary tracking: buildTocBoundaries(),
getTocIndexForPage(), getPageForTocIndex(), getPageRangeForTocIndex(),
readAnchorMap(), and readCachedPageCount() for lightweight cache queries.
ChapterHtmlSlimParser now accepts a tocAnchors set and forces page breaks
at TOC anchor boundaries so each chapter starts on a fresh page.
Increment SECTION_FILE_VERSION to 19 for new TOC boundary data.
Ported from upstream PRs #1143 and #1172, adapted to mod architecture.
Made-with: Cursor
Adapted from upstream PR #1325 (not yet merged).
When focused on the tab bar (selectedSettingIndex == 0), the confirm
button label now shows the name of the next category instead of
the generic "Toggle" text.
If/when #1325 is merged upstream, this commit should be dropped
during the next sync and the upstream version used instead.
Made-with: Cursor
Adapted from upstream PR #1320 (not yet merged).
Replaces scattered free()/delete cleanup with RAII Cleanup struct
that guarantees resource release on all return paths. Changes
rowCount from uint16_t* to uint32_t* to prevent overflow on
large images.
If/when #1320 is merged upstream, this commit should be dropped
during the next sync and the upstream version used instead.
Made-with: Cursor
Adapted from upstream PR #1329 (not yet merged).
Adds ReaderUtils.h with shared orientation, page-turn detection,
refresh cycle, and anti-aliased rendering utilities. Refactors
EpubReaderActivity and TxtReaderActivity to use shared implementations
instead of duplicated inline code.
If/when #1329 is merged upstream, this commit should be dropped
during the next sync and the upstream version used instead.
Made-with: Cursor
Full upstream resync: mod/master-resync was rebuilt from upstream/master
with all mod features re-applied to the new ActivityManager architecture.
This merge records the history connection but keeps the resync content.
- Fix fp4 fixed-point misuse in PlaceholderCoverGenerator (advanceX is 12.4
fixed-point, not pixels) causing only first letter of each word to render
- Remove duplicate silentIndexNextChapterIfNeeded() call from loop() that
blocked UI before render, preventing the indexing indicator from showing
- Fix indexing icon Y position to align within the status bar
- Add ignoreNextConfirmRelease to EpubReaderChapterSelectionActivity so
long-press confirm release doesn't immediately select the first TOC item
- Reload recent books after cache deletion in HomeActivity and clear stale
ignoreNextConfirmRelease flag to fix "no open books" and double-press bugs
Made-with: Cursor
- Add isValidThumbnailBmp(), generateInvalidFormatCoverBmp(), and
generateInvalidFormatThumbBmp() methods to Epub class for validating
BMP files and generating X-pattern marker images when cover extraction
fails (e.g., progressive JPG).
- Restore prerender block in EpubReaderActivity::onEnter() that checks
for missing cover BMPs (fit + cropped) and thumbnail BMPs at each
PRERENDER_THUMB_HEIGHTS size, showing a "Preparing book..." popup
with progress. Falls back to PlaceholderCoverGenerator, then to
invalid-format marker BMPs as last resort.
Made-with: Cursor
- Move Letterbox Fill setting to immediately after Sleep Screen Cover
Filter in the Display section where it is more relevant.
- Add Indexing Display setting to Display section with three modes:
Popup (default), Status Bar Text ("Indexing..."), and Status Bar Icon
(hourglass).
- Restore silent indexing logic in EpubReaderActivity::render() that
proactively indexes the next chapter when on a text-only page near
the end of the current chapter (non-popup mode only).
- Draw indexing indicator in renderStatusBar() when silentIndexingActive
is set and the user has chosen text or icon mode.
Made-with: Cursor
- Replace scattered book management actions (Archive, Delete, Reindex,
Delete Cache) with single "Manage Book" entry that opens
BookManageMenuActivity as a submenu.
- Replace scattered dictionary actions (Lookup Word, Lookup History,
Delete Dict Cache) with single "Dictionary" entry that opens new
DictionaryMenuActivity submenu.
- Add long-press Confirm (700ms) to open Table of Contents directly
from the reader, bypassing the menu.
- Add STR_DICTIONARY i18n key and regenerate I18nKeys.h/I18nStrings.h.
Made-with: Cursor
- Add clock rendering to BaseTheme::drawHeader() and LyraTheme::drawHeader()
after battery, before title. Respects clockFormat (OFF/AM-PM/24H) and
clockSize (Small/Medium/Large) settings.
- Fix PlaceholderCoverGenerator splitWords() to treat newlines, tabs, and
carriage returns as whitespace delimiters (not just spaces), preventing
one-character-per-line output from EPUB metadata with embedded newlines.
- Remove drawBorder() from placeholder covers since the UI already draws
its own frame around book cards.
Made-with: Cursor
- Update open-x4-sdk submodule to 9f76376 (BatteryMonitor ESP-IDF 5.x compat)
- Add RTC_NOINIT bounds check for logHead in Logging.cpp
- Add drawTextRotated90CCW to GfxRenderer for dictionary UI
- Add getWordXpos() accessor to TextBlock for dictionary word selection
- Fix bare include paths (ActivityResult.h, RenderLock.h) across 10 files
- Fix rvalue ref binding in setResult() lambdas (std::move pattern)
- Fix std::max type mismatch (uint8_t vs int) in EpubReaderActivity
- Fix FsFile forward declaration conflict in Dictionary.h
- Restore StringUtils::checkFileExtension() and sortFileList()
- Restore RecentBooksStore::removeBook()
Made-with: Cursor
Re-add KOReaderSyncActivity PUSH_ONLY mode (PR #1090):
- SyncMode enum with INTERACTIVE/PUSH_ONLY, deferFinish pattern
- Push & Sleep menu action in EpubReaderMenuActivity
- ActivityManager::requestSleep() for activity-initiated sleep
- main.cpp checks isSleepRequested() each loop iteration
Wire EndOfBookMenuActivity into EpubReaderActivity:
- pendingEndOfBookMenu deferred flag avoids render-lock deadlock
- Handles all 6 actions: ARCHIVE, DELETE, TABLE_OF_CONTENTS,
BACK_TO_BEGINNING, CLOSE_BOOK, CLOSE_MENU
Add book management to reader menu:
- ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK actions with handlers
Port silent next-chapter pre-indexing:
- silentIndexNextChapterIfNeeded() proactively indexes next chapter
when user is near end of current one, eliminating load screens
Add per-book letterbox fill toggle in reader menu:
- LETTERBOX_FILL cycles Default/Dithered/Solid/None
- Loads/saves per-book override via BookSettings
- bookCachePath constructor param added to EpubReaderMenuActivity
Made-with: Cursor
- Add drawPixelGray to GfxRenderer for letterbox fill rendering
- Add PRERENDER_THUMB_HEIGHTS to UITheme for placeholder cover generation
- Add [env:mod] build environment to platformio.ini
- Implement sleep screen letterbox fill (solid/dithered) with edge
caching in SleepActivity, including placeholder cover fallback
- Add Clock settings category to SettingsActivity with timezone,
NTP sync, and set-time actions; replace CalibreSettings with
OpdsServerListActivity; add DynamicEnum rendering support
- Add long-press book management to RecentBooksActivity
Made-with: Cursor
HomeActivity: Add mod features on top of upstream ActivityManager pattern:
- Multi-server OPDS support (OpdsServerStore instead of single URL)
- Long-press recent book for BookManageMenuActivity
- Long-press Browse Files to open archive folder
- Placeholder cover generation for books without covers
- startActivityForResult pattern for manage menu
EpubReaderMenuActivity: Replace upstream menu items with mod menu:
- Add/Remove Bookmark, Lookup Word, Go to Bookmark, Lookup History
- Table of Contents, Toggle Orientation, Toggle Font Size
- Close Book, Delete Dictionary Cache
- Pass isBookmarked and currentFontSize to constructor
- Show current orientation/font size value inline
EpubReaderActivity: Add mod action handlers:
- Bookmark add/remove via BookmarkStore
- Go to Bookmark via EpubReaderBookmarkSelectionActivity
- Dictionary word lookup via DictionaryWordSelectActivity
- Lookup history via LookedUpWordsActivity
- Delete dictionary cache
- Font size toggle with section re-layout
- Close Book action
ActivityResult: Add fontSize field to MenuResult
Made-with: Cursor
## Summary
Follow-up
https://github.com/crosspoint-reader/crosspoint-reader/pull/1145
- Fix log not being record without USB connected
- Bump release log to INFO for more logging details
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary: Enable footnote anchor navigation in EPUB reader
This PR extracts the core anchor-to-page mapping mechanism from PR #1143
(TOC fragment navigation) to provide immediate footnote navigation
support. By merging this focused subset first, users get a complete
footnote experience now while simplifying the eventual review and merge
of the full #1143 PR.
---
## What this extracts from PR #1143
PR #1143 implements comprehensive TOC fragment navigation for EPUBs with
multi-chapter spine files. This PR takes only the anchor resolution
infrastructure:
- Anchor-to-page mapping in section cache: During page layout,
ChapterHtmlSlimParser records which page each HTML id attribute lands
on, serializing the map into the .bin cache file.
- Anchor resolution in `EpubReaderActivity`: When navigating to a
footnote link with a fragment (e.g., `chapter2.xhtml#note1`), the reader
resolves the anchor to a page number and jumps directly to it.
- Section file format change: Bumped to version 15, adds anchor map
offset in header.
---
## Simplified scope vs. PR #1143
To minimize conflicts and complexity, this PR differs from #1143 in key
ways:
* **Anchors tracked**
* **Origin:** Only TOC anchors (passed via `std::set`)
* **This branch:** All `id` attributes
* **Page breaks**
* **Origin**: Forces new page at TOC chapter boundaries
* **This branch:** None — natural flow
* **TOC integration**
* **Origin**: `tocBoundaries`, `getTocIndexForPage()`, chapter skip
* **This branch:** None — just footnote links
* **Bug fix**
* **This branch:** Fixed anchor page off-by-1/2 bug
The anchor recording bug (recording page number before `makePages()`
flushes previous block) was identified and fixed during this extraction.
The fix uses a deferred `pendingAnchorId` pattern that records the
anchor after page completion.
---
## Positioning for future merge
Changes are structured to minimize conflicts when #1143 eventually
merges:
- `ChapterHtmlSlimParser.cpp` `startElement()`: Both branches rewrite
the same if `(!idAttr.empty())` block. The merged version will combine
both approaches (TOC anchors get page breaks + immediate recording;
footnote anchors get deferred recording).
- `EpubReaderActivity.cpp` `render()`: The `pendingAnchor` resolution
block is positioned at the exact same insertion point where #1143 places
its `pendingTocIndex` block (line 596, right after `nextPageNumber`
assignment). During merge, both blocks will sit side-by-side.
---
## Why merge separately?
1. Immediate user value: Footnote navigation works now without waiting
for the full TOC overhaul
2. Easier review: ~100 lines vs. 500+ lines in #1143
3. Bug fix included: The page recording bug is fixed here and will carry
into #1143
4. Minimal conflicts: Structured for clean merge — both PRs touch the
same files but in complementary ways
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_ Done by
Claude Opus 4.6
## Summary
* **What is the goal of this PR?** Potential stack buffer overflow from
untrusted ZIP entry name length
* **What changes are included?** If nameLen >= 256 , this writes past
the stack buffer. Risk: memory corruption/crash on malformed EPUB/ZIP.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _** PARTIALLY **_ Issue
identified by AI
Description
This Pull Request introduces Turkish language support to CrossPoint
Reader firmware.
Key Changes:
- Translation File: Added lib/I18n/translations/turkish.yaml with 315
translated
string keys, covering all system UI elements.
- I18N Script Update: Modified scripts/gen_i18n.py to include the "TR"
abbreviation mapping for Turkish.
- System Integration: Regenerated I18N C++ files to include the new
Language::TR
enum and STRINGS_TR array.
- UI Availability: The language is now selectable in the Settings menu
and
correctly handles Turkish-specific characters (ç, ğ, ı, ö, ş, ü).
- Documentation: Updated docs/i18n.md to include Turkish in the list of
supported languages.
Testing:
- Verified the build locally with PlatformIO.
- Flashed the firmware to an Xteink X4 device and confirmed the Turkish
UI
renders correctly.
---
### AI Usage
Did you use AI tools to help write this code? Yes Gemini
---------
Co-authored-by: Baris Albayrak <baris@Bariss-MacBook-Pro.local>
Co-authored-by: Barış Albayrak <barisa@pop-os.lan>
## Summary
* **What is the goal of this PR?**
* Improve and add the latest missing Swedish translations.
* **What changes are included?**
* Added missing Swedish translations in
`lib\I18n\translations\swedish.yaml`
## Additional Context
* (none)
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
added Romanian ranslations from recent commits.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
**What is the goal of this PR?**
This change avoids the pattern of creating a `std::string` using
`.substr` in order to compare against a file extension literal.
```c++
std::string path;
if (path.length() >= 4 && path.substr(path.length() - 4) == ".ext")
```
The `checkFileExtension` utility has moved from StringUtils to
FsHelpers, to be available to code in lib/. The signature now accepts a
`std::string_view` instead of `std::string`, which makes the single
implementation reusable for Arduino `String`.
Added utility functions for commonly repeated extensions.
These changes **save about 2 KB of flash (5,999,427 to 5,997,343)**.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
Quick follow up to #1291, adding Polish translations suggested by
@th0m4sek
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?**
Update relative paths to correctly navigate from .skills/ directory to
project root by adding ../ prefix to file references.
* **What changes are included?**
.skills/SKILL.md
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?**
Add missing Catalan strings.
* **What changes are included?**
Changes on catalan.yaml file only.
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? PARTIALLY
## Summary
**What is the goal of this PR?**
Avoid building cache path strings twice, once to check existence of the
file and a second time to delete the file.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Extend missing / amend existing
German translations
* **What changes are included?** German.yaml
## Additional Context
---
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
**What is the goal of this PR?** Add a user setting to decide image
support: display, show placeholder instead, supress fully
Fixes#1289
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
**Goal of this PR**
Add an **"In-scope — technically not supported"** section to `SCOPE.md`.
This clarifies hardware/UX limitations (e.g., clock support) and is
intended to reduce recurring feature requests on topics already
discussed.
Based on discussions in #287 and #626.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* Enable `DESTRUCTOR_CLOSES_FILE` flag
* We're never intending to not close files, so if we accidentally leave
them open as they're destructured, this will help close them.
## Additional Context
* As spotted in
https://github.com/crosspoint-reader/crosspoint-reader/pull/869, there
are cases where we were accidentally not closing files
Looks to use about 5K of flash.
```
RAM: [=== ] 31.5% (used 103100 bytes from 327680 bytes)
Flash: [======= ] 68.9% (used 4513220 bytes from 6553600 bytes)
```
```
RAM: [=== ] 31.5% (used 103100 bytes from 327680 bytes)
Flash: [======= ] 68.9% (used 4518498 bytes from 6553600 bytes)
```
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
* **What changes are included?**
New Ukrainian localization strings
## Additional Context
auto turn functionality
https://github.com/crosspoint-reader/crosspoint-reader/pull/1219
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO **_
The folder picker had no visible way to navigate to a parent directory.
Add a ".." list item (shown when not at root) so users can go up by
selecting it, matching standard file-picker conventions.
Made-with: Cursor
After an OPDS download completes, show a prompt screen instead of
immediately returning to the catalog. The user can choose to open the
book for reading or go back to the listing. A live countdown (5s, fast
refresh) auto-selects the configured default; any button press cancels
the timer. A per-server "After Download" setting controls the default
action (back to listing for backward compatibility).
Made-with: Cursor
Servers are sorted by a persistent sortOrder field (ties broken
alphabetically). On-device editing uses a new NumericStepperActivity
with side buttons for ±1 and face buttons for ±10. The web UI gets
up/down arrow buttons and a POST /api/opds/reorder endpoint.
Made-with: Cursor
## Summary
Ref discussion:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1222#discussion_r2865402110
Important note that this is a bug-for-bug fix. In reality, this branch
`WiFi.status() == WL_CONNECTED` is pretty much a dead code because the
entry point of these 2 activities don't use wifi.
It is better to refactor the management of network though, but it's
better to be a dedicated PR.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
## Summary
* **What is the goal of this PR?**
- Improve and add the latest missing Spanish translations
* **What changes are included?**
- Add missing spaces and remove extra unneeded ones (spaces at the end
of certain strings and others, i.e. the one introduced in the string
`Smart Device`; actually, `SmartDevice` is the correct Calibre plugin
name)
- Normalise the use of caps in certain strings
- Adapting the translation to the one found in related third-party
software (i.e. Spanish translation for the word `plugin` in Calibre is
`complemento`)
- Shortening some translations to make them smaller and fit better in
screen
- Rewording ambiguous translations (i.e. `Volver a inicio` could mean to
go back to Home, but also to go back to the first page of the current
book, so I changed it for a more specific action, `Volver al menú
Inicio`)
## Additional Context
* **Missing spaces caused a lack of clarity**
- My main motivation for this PR was the following:
<details>
<summary>Screenshots:</summary>
In English:

In Spanish:

</details>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
* Custom sleep screen images now load from /.sleep directory
(preferred), falling back to /sleep for backwards compatibility. The
dot-prefix keeps the directory hidden from the file browser.
* Rewrote User Guide section 3.6 to document all six sleep screen modes,
cover settings, and the updated custom image setup.
## Additional Context
* The sleep directoy entry while browsing files was distracting.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _NO_
## Summary
* **What is the goal of this PR?** Fixing two independent CSS rendering
bugs combined to make hanging-indent list styles
(e.g. margin-left:3em; text-indent:-1em) render incorrectly:
* **What changes are included?**
1. Negative text-indent was silently ignored
Three guards in ParsedText.cpp (computeLineBreaks,
computeHyphenatedLineBreaks,
extractLine) conditioned firstLineIndent on blockStyle.textIndent > 0,
so any
negative value collapsed to zero. Additionally, wordXpos was uint16_t,
which
cannot represent negative offsets — a cast of e.g. −18 would wrap to
65518 and
render the word far off-screen.
2. extraParagraphSpacing suppressed hanging indents
Even after removing the > 0 guard, the existing !extraParagraphSpacing
condition
would still suppress all text-indent when that setting is on (its
default). Positive
text-indent is a decorative paragraph indent that the user can
reasonably replace with
vertical spacing — negative text-indent is structural (it positions the
list marker)
and must always apply.
3. em unit was calibrated against line height, not font size
emSize was computed as getLineHeight() * lineCompression (the full line
advance).
CSS em units are defined relative to the font-size, which corresponds to
the
ascender height — not the line height. Using line height makes every
em-based
margin/indent ~20–30% wider than a browser would render it, and is
especially
noticeable for CSS that uses font-size: small (which we do not
implement).
## Additional Context
Test case
```
.lsl1 { margin-left: 3em; text-indent: -1em; }
<div class="lsl1">• First list item that wraps across lines</div>
<div class="lsl1">• Short item</div>
```
Before: all lines of all items started at 3 em from the left edge
(indent ignored).
After: the bullet marker hangs at 2 em; continuation lines align at 3
em.
<img width="240" alt="before"
src="https://github.com/user-attachments/assets/9dcbf3e0-fcd9-4af8-b451-a90ba4d2fb75"
/>
<img width="240" alt="after"
src="https://github.com/user-attachments/assets/1ffdcf56-a180-4267-9590-c60d7ac44707"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Port three unmerged upstream PRs with adaptations for the fork's
callback-based ActivityWithSubactivity architecture:
- PR #1185: Cache KOReader document hash using mtime fingerprint +
file size validation to avoid repeated MD5 computation on sync.
- PR #1217: Proper KOReader XPath synchronisation via new
ChapterXPathIndexer (Expat-based on-demand XHTML parsing) with
XPath-first mapping and percentage fallback in ProgressMapper.
- PR #1090: Push Progress & Sleep menu option with PUSH_ONLY sync
mode. Adapted to fork's callback pattern with deferFinish() for
thread-safe completion. Modified to sleep silently on any failure
(hash, upload, no credentials) rather than returning to reader.
Made-with: Cursor
## Summary
* Renames MyLibrary component to FileBrowser, as it better reflects what
it is, in my opinion
## Additional Context
* Frees the Library name for possible future library component that can
cache metadata, provide other ways of browsing than filesystem
structure, etc
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_
When downloading a book via OPDS, a directory picker now lets the user
choose the save location instead of always saving to the SD root. Each
OPDS server has a configurable default download path (persisted in
opds.json) that the picker opens to. Falls back to "/" if the saved
path no longer exists on disk.
- Add DirectoryPickerActivity (browse-only directory view with "Save Here")
- Add PICKING_DIRECTORY state to OpdsBookBrowserActivity
- Add downloadPath field to OpdsServer with JSON serialization
- Add Download Path setting to OPDS server edit screen
- Extract sortFileList() to StringUtils for shared use
- Add i18n strings: STR_SAVE_HERE, STR_SELECT_FOLDER, STR_DOWNLOAD_PATH
Made-with: Cursor
## Summary
**What is the goal of this PR?**
Hopefully fixes#1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
## Summary
**What is the goal of this PR?**
- Adds `scripts/firmware_size_history.py`, a developer tool that builds
firmware at selected git commits and reports flash usage with deltas
between them.
- Supports two input modes: `--range START END` to walk every commit in
a range, or `--commits REF [REF ...]` to compare specific refs (which
can span branches).
- Defaults to a human-readable aligned table; pass `--csv` for
machine-readable output to stdout or `--csv FILE` to write to a file.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, fully written by
AI**_
## Summary
**What is the goal of this PR?**
Rewrite of font routines to use std binary search algorithms instead of
custom repeated implementations: `lookupKernClass`,
`EpdFont::getLigature`, and `EpdFont::getGlyph`.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
Small follow up to #909, removing an unused member variable and some
temporary debug logging.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?**
Increase accuracy of button hints and text description in the file
browser when viewing empty directory.
* **What changes are included?**
Adjusted button label hint rendering logic in file browser to hide the
"Open", "Up", and "Down" hints when the they are not available due to an
empty directory.
I also changed the NO_BOOKS_FOUND string to NO_FILES_FOUND and updated
translations. File browser shows more than just books so seeing "No
Books Found" really doesn't make sense.
## Additional Context
Very Simple change, here is what that looks like on my device.
<img width="1318" height="879" alt="Untitled (7)"
src="https://github.com/user-attachments/assets/6416c8c8-795d-41a5-9b9f-28d2c26666a0"
/>
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Eliminate the 3-file / 4-location
overhead for adding a new setting. Previously, every new setting
required manually editing JsonSettingsIO.cpp in two places (save +
load), duplicating knowledge already present in SettingsList.h. After
this PR, JsonSettingsIO.cpp never needs to be touched again for standard
settings.
* **What changes are included?**
* `SettingInfo` (in `SettingsActivity.h`) gains one new field: `bool
obfuscated` (base64 save/load for passwords), with a fluent builder
method `.withObfuscated()`. The previously proposed
`defaultValue`/`withDefault()` approach was dropped in favour of reading
the struct field's own initializer value as the fallback (see below).
* `SettingsList.h` entries are annotated with `.withObfuscated()` on the
OPDS password entry. The list is now returned as a `static const`
singleton (`const std::vector<SettingInfo>&`), so it is constructed
exactly once. A missing `key`/`category` on the
`statusBarProgressBarThickness` entry was also fixed — it was previously
skipped by the generic save loop, so changes were silently lost on
restart.
* `JsonSettingsIO::saveSettings` and `loadSettings` replace their ~90
lines of manual per-field code with a single generic loop over
`getSettingsList()`. The loop uses `info.key`,
`info.valuePtr`/`info.stringOffset`+`info.stringMaxLen` (for char-array
string fields), `info.enumValues.size()` (for enum clamping), and
`info.obfuscated`.
* **Default values**: instead of a duplicated `defaultValue` field in
`SettingInfo`, `loadSettings` reads `s.*(info.valuePtr)` *before*
overwriting it. Because `CrossPointSettings` is default-constructed
before `loadSettings` is called, this captures each field's struct
initializer value as the JSON-absent fallback. The single source of
truth for defaults is `CrossPointSettings.h`.
* One post-loop special case remains explicitly: the four `frontButton*`
remap fields (managed by the RemapFrontButtons sub-activity, not in
SettingsList) and `validateFrontButtonMapping()`.
* One pre-loop migration guard handles legacy settings files that
predate the status bar refactor: if `statusBarChapterPageCount` is
absent from the JSON, `applyLegacyStatusBarSettings()` is called first
so the generic loop picks up the migrated values as defaults and applies
its normal clamping.
* OPDS password backward-compat migration (plain `opdsPassword` →
obfuscated `opdsPassword_obf`) is preserved inside the generic
obfuscated-string path.
## Additional Context
Say we want to add a new `bookmarkStyle` enum setting with options
`DOT`, `LINE`, `NONE` and a default of `DOT`:
1. `src/CrossPointSettings.h` — add enum and member:
```cpp
enum BOOKMARK_STYLE { BOOKMARK_DOT = 0, BOOKMARK_LINE = 1, BOOKMARK_NONE = 2 };
uint8_t bookmarkStyle = BOOKMARK_DOT;
```
2. `lib/I18n/translations/english.yaml` — add display strings:
```yaml
STR_BOOKMARK_STYLE: "Bookmark Style"
STR_BOOKMARK_DOT: "Dot"
STR_BOOKMARK_LINE: "Line"
```
(Other language files will fall back to English if not translated. Run
`gen_i18n.py` to regenerate `I18nKeys.h`.)
3. `src/SettingsList.h` — add one entry in the appropriate category:
```cpp
SettingInfo::Enum(StrId::STR_BOOKMARK_STYLE, &CrossPointSettings::bookmarkStyle,
{StrId::STR_BOOKMARK_DOT, StrId::STR_BOOKMARK_LINE, StrId::STR_NONE_OPT},
"bookmarkStyle", StrId::STR_CAT_READER),
```
That's it — no default annotation needed anywhere, because
`bookmarkStyle = BOOKMARK_DOT` in the struct already provides the
fallback. The setting will automatically persist to JSON on save, load
with clamping on boot, appear in the device settings UI under the Reader
category, and be exposed via the web API — all with no further changes.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY>**_
## Summary
Replaces the picojpeg library with bitbank2/JPEGDEC for JPEG decoding in
the EPUB image pipeline. JPEGDEC provides built-in coarse scaling (1/2,
1/4, 1/8), 8-bit grayscale output, and streaming block-based decoding
via callbacks.
Includes a pre-build patch script for two JPEGDEC changes affecting
progressive JPEG support and EIGHT_BIT_GRAYSCALE mode.
Closes#912
## Additional Context
# Example progressive jpeg
<img
src="https://github.com/user-attachments/assets/e63bb4f8-f862-4aa0-a01f-d1ef43a4b27a"
width="400" height="800" />
Good performance increase from JPEGDEC over picojpeg cc @bitbank2 thanks
## Baseline JPEG Decode Performance: picojpeg vs JPEGDEC (float in
callback) vs JPEGDEC (fixed-point in callback)
Tested with `test_jpeg_images.epub` on device (ESP32-C3), first decode
(no cache).
| Image | Source | Output | picojpeg | JPEGDEC float | JPEGDEC
fixed-point | vs picojpeg | vs float |
|-------|--------|--------|----------|---------------|---------------------|-------------|----------|
| jpeg_format.jpg | 350x250 | 350x250 | 313 ms | 256 ms | **104 ms** |
**3.0x** | **2.5x** |
| grayscale_test.jpg | 400x600 | 400x600 | 768 ms | 661 ms | **246 ms**
| **3.1x** | **2.7x** |
| gradient_test.jpg | 400x500 | 400x500 | 707 ms | 597 ms | **247 ms** |
**2.9x** | **2.4x** |
| centering_test.jpg | 350x400 | 350x400 | 502 ms | 412 ms | **169 ms**
| **3.0x** | **2.4x** |
| scaling_test.jpg | 1200x1500 | 464x580 | 5487 ms | 1114 ms | **668
ms** | **8.2x** | **1.7x** |
| wide_scaling_test.jpg | 1807x736 | 464x188 | 4237 ms | 642 ms | **497
ms** | **8.5x** | **1.3x** |
| cache_test_1.jpg | 400x300 | 400x300 | 422 ms | 348 ms | **141 ms** |
**3.0x** | **2.5x** |
| cache_test_2.jpg | 400x300 | 400x300 | 424 ms | 349 ms | **142 ms** |
**3.0x** | **2.5x** |
### Summary
- **1:1 scale (fixed-point vs float)**: ~2.5x faster — eliminating
software float on the FPU-less ESP32-C3 is the dominant win
- **1:1 scale (fixed-point vs picojpeg)**: ~3.0x faster overall
- **Downscaled images (vs picojpeg)**: 8-9x faster — JPEGDEC's coarse
scaling + fixed-point draw callback
- **Downscaled images (fixed-point vs float)**: 1.3-1.7x — less dramatic
since JPEG library decode dominates over the draw callback for fewer
output pixels
- The fixed-point optimization alone (vs float JPEGDEC) saved **~60% of
render time** on 1:1 images, confirming that software float emulation
was the primary bottleneck in the draw callback
- See thread for discussions on quality of progressive images,
https://github.com/crosspoint-reader/crosspoint-reader/pull/1136#issuecomment-3952952315
- and the conclusion
https://github.com/crosspoint-reader/crosspoint-reader/pull/1136#issuecomment-3959379386
- Proposal to improve quality added at
https://github.com/crosspoint-reader/crosspoint-reader/discussions/1179
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
Properly implement `requestUpdateAndWait()` using freeRTOS direct task
notification.
FWIW, I think most of the current use cases of `requestUpdateAndWait()`
are redundant, better to be replaced by `requestUpdate(true)`. But just
keeping them in case we can find a proper use case for it in the future.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **YES**, it's trivial, so
I asked an AI to write the code
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
* **What is the goal of this PR?** During my development I am frequently
jumping from branch to branch flashing test versions on my device. It
becomes sometimes quite difficult to figure out which version of the
software I am currently looking at.
* **What changes are included?**
- Dev builds now display the current git branch in the version string
shown on the Settings screen (e.g. 1.1.0-dev+feat-my-feature), making it
easier to identify which firmware is running on the device when
switching between branches frequently.
- Release, RC, and slim builds are unaffected — they continue to set
their version string statically in platformio.ini.
<img width="480" height="800" alt="after"
src="https://github.com/user-attachments/assets/d2ab3d69-ab6b-47a1-8eb7-1b40b1d3b106"
/>
## Additional Context
A new PlatformIO pre-build script (scripts/git_branch.py) runs
automatically before every dev build. It reads the base version from the
[crosspoint] section of platformio.ini, queries git rev-parse
--abbrev-ref HEAD for the current branch, and injects the combined
string as the CROSSPOINT_VERSION preprocessor define. In a detached HEAD
state it falls back to the short commit SHA. If git is unavailable it
warns and falls back to unknown.
The script can also be run directly with python scripts/git_branch.py
for validation without triggering a full build.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
## Summary
* **What is the goal of this PR?** Add missing strings and tweaks for
polish language
* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Allow users to better manage their epub library by offloading unwanted
or finished books and other files. Resolves#893
* **What changes are included?**
Added Delete Book shortcut in the fil browser. Delete function
implements the new ConfirmationActivity to show file name and solicit
user interaction before either returning to the file browser on a press
of the back button, or proceeding to delete. Delete function then
deletes the file and returns user to the file browser menu at the
current directory. Video of it working on my machine attached here:
https://github.com/user-attachments/assets/329b0198-9e97-45ad-82aa-c39894351667
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Certainly potential risks associated with file deletion. Please let me
know if there are any concerns that need to be better addressed. I think
this is a very good feature to have to go along with the new screenshots
so you don't get stuck with a bunch of extra files on your device. Also
I did add this to the user guide.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Егор Мартынов <martynovegorOF@yandex.ru>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
* Refactored `HttpDownloader::downloadToFile` to use `FileWriteStream`
and `HTTPClient::writeToStream`, removing manual chunked downloading
logic, which was error-prone.
* Fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/632
## Additional Context
* Tested downloading files from OPDS with a size up to 10 mb.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
Fix https://github.com/crosspoint-reader/crosspoint-reader/issues/1137
Introducing `HalFile`, a thin wrapper around `FsFile` that uses a global
mutex to protect file operations.
To test this PR, place the code below somewhere in the code base (I
placed it in `onGoToRecentBooks`)
```cpp
static auto testTask = [](void* param) {
for (int i = 0; i < 10; i++) {
String json = Storage.readFile("/.crosspoint/settings.json");
LOG_DBG("TEST_TASK", "Read settings.json, bytes read: %u", json.length());
}
vTaskDelete(nullptr);
};
xTaskCreate(testTask, "test0", 8192, nullptr, 1, nullptr);
xTaskCreate(testTask, "test1", 8192, nullptr, 1, nullptr);
xTaskCreate(testTask, "test2", 8192, nullptr, 1, nullptr);
xTaskCreate(testTask, "test3", 8192, nullptr, 1, nullptr);
delay(1000);
```
It will reliably lead to crash on `master`, but will function correctly
with this PR.
A macro renaming trick is used to avoid changing too many downstream
code files.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **PARTIALLY**, only to
help with tedious copy-paste tasks
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
**What is the goal of this PR?**
Added overview and migration guide for ActivityManager changes in #1016.
Thanks for the suggestion, @drbourbon!
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, fully written by
Claude 4.6**_
## Summary
* **What is the goal of this PR?**
Fixed a small prefix translation (`STR_TO_PREFIX`)
* **What changes are included?**
Changed the translation from `naar ` to `met `
## Additional Context
* The English translation file doesn't make clear what the context of
`STR_TO_PREFIX` is, so the Dutch translation wasn't correct. This PR
fixes that.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
---------
Co-authored-by: Bas van der Ploeg <bas@MBP-M2-Max-3.localdomain>
## Summary
**What is the goal of this PR?**
Small tweaks to #1016:
- Only Activity and ActivityManager can access activityResultHandler and
activityResult
- `[[maybe_unused]]` in RenderLock constructor
- Only ActivityManager and RenderLock can access renderingMutex
- Missing renderUpdate after failed wifi selection
- Standardize on activities calling finish instead of
activityManager.popActivity
- Hold RenderLock while mutating state in EpubReaderActivity result
handlers
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- Implements auto page turn feature for epub reader in the reader
submenu
* **What changes are included?**
- added auto page turn feature in epub reader in the submenu
- currently there are 5 settings, `OFF, 1, 3, 6, 12` pages per minute
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
- Replacement PR for #723
- when auto turn is enabled, space reserved for chapter title will be
used to indicate auto page turn being active
- Back and Confirm button is used to disable it
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**Partially (mainly code
reviews)**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Fixes a clarity issue regarding the translation string `STR_SET`. The
issue lies in the fact that the english word can have different
meanings.
The only time the string is used is in the language selectio screen,
where it has the meaning of _selected_. (As in _The language has been
**set** to French_).
Another meaning can be _configured_. (As in _The KOReader username has
been __set__). This is the meaning many of the translations have taken.
The reason that the string is right above `STR_NOT_SET` (which is meant
as _not configured_).
With this PR I propose to explicitly use the term "_Selected_". There
are two good reasons for this:
+ it removes the confusion and the misleading translations
+ it is consistent with the button label `Select`, communicating the
link between the two (the row will be marked `Selected` if you press the
buttpn `Select`. Much clearer than now)
* **What changes are included?**
Removed the unused strings and added translations for the new string
`STR_SELECTED` for the languages I know.
tagging the translators for feedback:
fr: @Spigaw @CaptainFrito
de: @DavidOrtmann
cs: @brbla
pt: @yagofarias
it: @andreaturchet @fargolinux
ru: @madebykir @mrtnvgr
es: @yeyeto2788 @Skrzakk @pablohc
sv: @dawiik
ca: @angeldenom
uj: @mirus-ua
be: @dexif
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
the Issue was introduced in #1020. Previously, if a language was
selected it was marked with `[ON]` (`STR_ON_MARKER`). I considered
reverting it back to that, but the solution I described above seemed
superior.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
---------
Co-authored-by: Егор Мартынов <martynovegorOF@yandex.ru>
Co-authored-by: Mirus <mirusim@gmail.com>
## Summary
* **What is the goal of this PR?**
Improve KOReader Sync documentation so new users can self-host and
configure sync quickly, with clear references in both README and
USER_GUIDE.
* **What changes are included?**
- Add a KOReader Sync feature mention in `README.md` and link to a
quick-setup section in the guide.
- Add `3.6.5 KOReader Sync Quick Setup` to `USER_GUIDE.md` with:
- Docker Compose-first setup instructions (plus Podman compose
alternative)
- healthcheck verification
- one-time user registration example using `curl`
- device-side setup steps (`Settings -> System -> KOReader Sync` and
`Authenticate`)
- in-reader usage via `Sync Progress` in the reader menu
- Update reading mode navigation wording from "Chapter Menu" to "Reader
Menu" so it reflects current UI behavior.
Closes#1032.
## Additional Context
* This is documentation-only and does not change firmware behavior.
* The quick-setup flow is intentionally short and focused on getting
sync working quickly for typical home-network setups.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
## Summary
* **Added Dutch translation**
* **(Added dutch.yaml translation file)**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY>**_
---------
Co-authored-by: Bas van der Ploeg <bas@MBP-M2-Max-3.localdomain>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
Added translator name for Portuguese (Portugal)
---
### AI Usage
Did you use AI tools to help write this code? _**< NO >**_
## Summary
Ref comment:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1010#pullrequestreview-3828854640
This PR introduces `ActivityManager`, which mirrors the same concept of
Activity in Android, where an activity represents a single screen of the
UI. The manager is responsible for launching activities, and ensuring
that only one activity is active at a time.
Main differences from Android's ActivityManager:
- No concept of Bundle or Intent extras
- No onPause/onResume, since we don't have a concept of background
activities
- onActivityResult is implemented via a callback instead of a separate
method, for simplicity
## Key changes
- Single `renderTask` shared across all activities
- No more sub-activity, we manage them using a stack; Results can be
passed via `startActivityForResult` and `setResult`
- Activity can call `finish()` to destroy themself, but the actual
deletion will be handled by `ActivityManager` to avoid `delete this`
pattern
As a bonus: the manager will automatically call `requestUpdate()` when
returning from another activity
## Example usage
**BEFORE**:
```cpp
// caller
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
// subactivity
onComplete(true); // will eventually call exitActivity(), which deletes the caller instance (dangerous behavior)
```
**AFTER**: (mirrors the `startActivityForResult` and `setResult` from
android)
```cpp
// caller
startActivityForResult(new NetworkModeSelectionActivity(renderer, mappedInput),
[this](const ActivityResult& result) { onNetworkModeSelected(result.selectedNetworkMode); });
// subactivity
ActivityResult result;
result.isCancelled = false;
result.selectedNetworkMode = mode;
setResult(result);
finish(); // signals to ActivityManager to go back to last activity AFTER this function returns
```
TODO:
- [x] Reconsider if the `Intent` is really necessary or it should be
removed (note: it's inspired by
[Intent](https://developer.android.com/guide/components/intents-common)
from Android API) ==> I decided to keep this pattern fr clarity
- [x] Verify if behavior is still correct (i.e. back from sub-activity)
- [x] Refactor the `ActivityWithSubactivity` to just simple `Activity`
--> We are using a stack for keeping track of sub-activity now
- [x] Use single task for rendering --> avoid allocating 8KB stack per
activity
- [x] Implement the idea of [Activity
result](https://developer.android.com/training/basics/intents/result)
--> Allow sub-activity like Wifi to report back the status (connected,
failed, etc)
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **PARTIALLY**, some
repetitive migrations are done by Claude, but I'm the one how ultimately
approve it
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
* **What is the goal of this PR?**
Fix an infinite render loop bug in CalibreConnectActivity that caused
the e-ink display to refresh continuously every ~421ms after a file
transfer completed.
* **What changes are included?**
- Added lastProcessedCompleteAt member variable to track which server
completion timestamp has already been processed
- Modified the completion status update logic to only accept new values
from the server, preventing re-processing of old timestamps
- Added clarifying comments explaining the fix
## Problem Description
After receiving a file via Calibre Wireless, the activity displays
"Received: [filename]" for 6 seconds, then clears the message. However,
the web server's wsLastCompleteAt timestamp persists indefinitely and is
never cleared.
This created a race condition:
After 6 seconds, lastCompleteAt is set to 0 (timeout)
In the next loop iteration, status.lastCompleteAt (still has the old
timestamp) ≠ lastCompleteAt (0)
The code restores lastCompleteAt from the server value
Immediately, the 6-second timeout condition is met again
This creates an infinite cycle causing unnecessary e-ink refreshes
## Solution
The fix introduces lastProcessedCompleteAt to track which server
timestamp value has already been processed:
Only accept a new status.lastCompleteAt if it differs from
lastProcessedCompleteAt
Update lastProcessedCompleteAt when processing a new value
Do NOT reset lastProcessedCompleteAt when the 6-second timeout clears
lastCompleteAt
This prevents re-processing the same old server value after the timeout.
## Testing
Tested on device with multiple file transfer scenarios:
✅ File received message appears correctly after transfer
✅ Message clears after 6 seconds as expected
✅ No infinite render loop after timeout
✅ Multiple consecutive transfers work correctly
✅ Exiting and re-entering Calibre Wireless works as expected
## Performance Impact
Before: Infinite refreshes every ~421ms after timeout (high battery
drain, display wear)
After: 2-3 refreshes after timeout, then stops (normal behavior)
## Additional Context
This is a targeted fix that only affects the Calibre Wireless file
transfer screen. The root cause is the architectural difference between
the persistent web server state (wsLastCompleteAt) and the per-activity
display state (lastCompleteAt).
An alternative fix would be to clear wsLastCompleteAt in the web server
after some timeout, but that would affect all consumers of the web
server status. The chosen solution keeps the fix localized to
CalibreConnectActivity.
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?** some tweaks to Polish translation
* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
Port two upstream PRs:
- PR #1207: Replace manual chunked download loop with
HTTPClient::writeToStream via a FileWriteStream adapter, improving
reliability for OPDS file downloads including chunked transfers.
- PR #1209: Add support for multiple OPDS servers with a new
OpdsServerStore (JSON persistence with MAC-based password obfuscation),
OpdsServerListActivity and OpdsSettingsActivity UIs, per-server
credentials passed to HttpDownloader, web UI management endpoints,
and migration from legacy single-server settings.
Made-with: Cursor
New "Auto Sync on Boot" toggle in Clock Settings. When enabled, a
background FreeRTOS task scans for saved WiFi networks at boot,
connects, syncs time via NTP, then tears down WiFi — all without
blocking boot or requiring user interaction. If no saved network is
found after two scan attempts (with a 3-second retry gap), it bails
silently.
Conflict guards (BootNtpSync::cancel()) added to all WiFi-using
activities so the background task cleans up before any user-initiated
WiFi flow. Also fixes clock not appearing in the header until a button
press by detecting the invalid→valid time transition after NTP sync.
Made-with: Cursor
## Summary
* As many of us have experienced, the use of AI coding agents (Claude,
Cursor, Copilot) in our daily workflows is no longer a hypothetical—it
is a reality. While these tools can significantly accelerate
development, they also pose a unique risk to a project like CrossPoint
Reader, where our hardware constraints (ESP32-C3 with ~380KB RAM) are
extremely tight.
* AI models often "hallucinate" APIs or suggest high-level C++ patterns
(like std::string or heavy heap usage) that are detrimental to our
memory-constrained environment.
* To address this, I am proposing the introduction of an AI Agent
Guidance File (.skills/SKILL.md recognized by many AI systems). This
file acts as a "Constitutional Document" for AI agents, forcing them to
adhere to our specific engineering rigors before they generate a single
line of code.
## Additional Context
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_ Asked
several Ai systems about their needs, added my own 5 cents of nuisances
I faced
## Summary
**What is the goal of this PR?** Replace the linear scan of
`lookupHtmlEntity` with a simple binary search to improve lookup
performance.
**What changes are included?**
`lib/Epub/Epub/Entities/htmlEntities.cpp`:
- Sorted the `ENTITY_LOOKUP` array.
- Added a compile-time assertion to guarantee the array remains sorted.
- Rewrote `lookupHtmlEntity` to use a binary search.
## Additional Context
Benchmarked on my x64 laptop (probably will be different on RISC-V)
```
=== Benchmark (53 entities x 10000 iterations) ===
Version Total time Avg per lookup
----------------------------------------------
linear 236.97 ms total 447.11 ns/lookup
binary search 22.09 ms total 41.68 ns/lookup
=== Summary ===
Binary search is 10.73x faster than linear scan.
```
This is a simplified alternative to #1180, focused on keeping the
implementation clean, and maintainable.
### AI Usage
Did you use AI tools to help write this code? _**< NO >**_
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
* **What is the goal of this PR?**
Update translation after
https://github.com/crosspoint-reader/crosspoint-reader/pull/733
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
Translation added russian.yaml
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
NO
## Summary
**What is the goal of this PR?** Implement support for footnotes in epub
files.
It is based on #553, but simplified — removed the parts which
complicated the code and burden the CPU/RAM. This version supports basic
footnotes and lets the user jump from location to location inside the
epub.
**What changes are included?**
- `FootnoteEntry` struct — A small POD struct (number[24], href[64])
shared between parser, page storage, and UI.
- Parser: `<a href>` detection (`ChapterHtmlSlimParser`) — During a
single parsing pass, internal epub links are detected and collected as
footnotes. The link text is underlined to hint navigability.
Bracket/whitespace normalization is applied to the display label (e.g.
[1] → 1).
- Footnote-to-page assignment (`ChapterHtmlSlimParser`, `Page`) —
Footnotes are attached to the exact page where their anchor word
appears, tracked via a cumulative word counter during layout, surviving
paragraph splits and the 750-word mid-paragraph safety flush.
- Page serialization (`Page`, `Section`) — Footnotes are
serialized/deserialized per page (max 16 per page). Section cache
version bumped to 14 to force a clean rebuild.
- Href → spine resolution (`Epub`) — `resolveHrefToSpineIndex()` maps an
href (e.g. `chapter2.xhtml#note1`) to its spine index by filename
matching.
- Footnotes menu + activity (`EpubReaderMenuActivity`,
`EpubReaderFootnotesActivity`) — A new "Footnotes" entry in the reader
menu lists all footnote links found on the current page. The user
scrolls and selects to navigate.
- Navigate & restore (`EpubReaderActivity`) — `navigateToHref()` saves
the current spine index and page number, then jumps to the target. The
Back button restores the saved position when the user is done reading
the footnote.
**Additional Context**
**What was removed vs #553:** virtual spine items
(`addVirtualSpineItem`, `isVirtualSpineItem`), two-pass parsing,
`<aside>` content extraction to temp HTML files, `<p class="note">`
paragraph note extraction, `replaceHtmlEntities` (master already has
`lookupHtmlEntity`), `footnotePages` / `buildFilteredChapterList`,
`noterefCallback` / `Noteref` struct, and the stack size increase from 8
KB to 24 KB (not needed without two-pass parsing and virtual file I/O on
the render task).
**Performance:** Single-pass parsing. No new heap allocations in the hot
path — footnote text is collected into fixed stack buffers (char[24],
char[64]). Active runtime memory is ~2.8 KB worst-case (one page × 16
footnotes × 88 bytes, mirrored in `currentPageFootnotes`). Flash usage
is unchanged at 97.4%; RAM stays at 31%.
**Known limitations:** When clicking a footnote, it jumps to the start
of the HTML file instead of the specific anchor. This could be
problematic for books that don't have separate files for each footnote.
(no element-id-to-page mapping yet - will be another PR soon).
---
### AI Usage
Did you use AI tools to help write this code? _**< PARTIALLY>**_
Claude Opus 4.6 was used to do most of the migration, I checked manually
its work, and fixed some stuff, but I haven't review all the changes
yet, so feedback is welcomed.
---------
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
The strings for `Show` and `Hide` were always showing in English,
regardless of which language was selected.
* **What changes are included?**
Replace the variables in the lambda by direct calls to the `tr` macro
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**<NO >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Romanian translations for newly added strings.
* **What changes are included?**
Only the translations
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
A Danish translation for the GUI
* **What changes are included?**
Everything from
[`i18n.md`](https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/i18n.md)
---
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
No
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
Started translating myself, transitioned to having Claude Code do the
bulk of the translation. Read every translation myself and doubled
checked with a dictionary if I agreed with the translation made.
---------
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Adds Finnish language support
* **What changes are included?**
Created new translation yaml file, ran the translation script to
generate the C++ code
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
---------
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
* **What is the goal of this PR?** Implements Polish language
* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Currently we are displaying the languages in the order they were added
(as in the `Language` enum). However, as new languages are coming in,
this will quickly be confusing to the users.
But we can't just change the ordering of the enum if we want to respect
bakwards compatibility.
So my proposal is to add a mapping of the alphabetical order of the
languages. I've made it so that it's generated by the `gen_i18n.py`
script, which will be used when a new language is added.
* **What changes are included?**
Added the array from the python script and changed
`LanguageSelectActivity` to use the indices from there. Also commited
the generated `I18nKeys.h`
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
I was wondering if there is a better way to sort it. Currently, it's by
unicode value and Czech and Russian are last, which I don't know it it's
the most intuitive.
The current order is:
`Català, Deutsch, English, Español, Français, Português (Brasil),
Română, Svenska, Čeština, Русский`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
## Summary
* **What is the goal of this PR?** KOReader sync on a German-language
book would fail with an out-of-memory error when trying to open the
destination chapter after applying remote progress. The root cause was a
chain of two independent bugs that combined to exhaust the contiguous
heap needed by the EPUB inflate pipeline.
* **What changes are included?**
## Fix 1 — Hyphenation heap defragmentation (LiangHyphenation.cpp)
### What was happening
AugmentedWord, the internal struct used during Liang pattern matching,
held three std::vector<> members (bytes, charByteOffsets,
byteToCharIndex) plus a separate scores vector — a total of 4 heap
allocations per word during page layout. For a German-language section
with hundreds of words, thousands of small malloc/free cycles fragmented
the heap. Total free memory was adequate (~108 KB) but the largest
contiguous block shrank well below the 32 KB needed for the INFLATE ring
buffer used during EPUB decompression. The failure was invisible with
hyphenation disabled, where MaxAlloc stayed at ~77 KB; enabling German
hyphenation silently destroyed the contiguity the allocator needed.
### What changed
The three std::vector<> members of AugmentedWord and the scores vector
are replaced with fixed-size C arrays on the render-task stack:
```
uint8_t bytes[160] // was std::vector<uint8_t>
size_t charByteOffsets[70] // was std::vector<size_t>
int32_t byteToCharIndex[160] // was std::vector<int32_t>
uint8_t scores[70] // was std::vector<uint8_t> (local in liangBreakIndexes)
```
Sizing is based on the longest known German word (~63 codepoints × 2
UTF-8 bytes + 2 sentinel dots = 128 bytes); MAX_WORD_BYTES=160 and
MAX_WORD_CHARS=70 give comfortable headroom. The same analysis holds for
all seven supported languages (en, fr, de, es, it, ru, uk) — every
accepted letter encodes to at most 2 UTF-8 bytes after case-folding.
Words exceeding the limits are silently skipped (no hyphenation
applied), which is correct behaviour. The struct lives on the 8 KB
render-task stack so no permanent DRAM is consumed.
Verification: after the fix, MaxAlloc reads 77,812 bytes with German
hyphenation enabled — identical to the figure previously achievable only
with hyphenation off.
## Fix 2 — WiFi lifecycle in KOReaderSyncActivity
(KOReaderSyncActivity.cpp)
### What was happening
onEnter() called WiFi.mode(WIFI_STA) unconditionally before delegating
to WifiSelectionActivity. WifiSelectionActivity manages WiFi mode
internally (it calls WiFi.mode(WIFI_STA) again at scan start and at
connection attempt). The pre-emptive call from KOReaderSyncActivity
interfered with the sub-activity's own state machine, causing
intermittent connection failures that were difficult to reproduce.
Additionally, WiFi was only shut down in onExit(). If the user chose
"Apply remote progress" the activity exited without turning WiFi off
first, leaving the radio on and its memory allocated while the EPUB was
being decompressed — unnecessarily consuming the contiguous heap
headroom that inflate needed.
### What changed
* WiFi.mode(WIFI_STA) removed from onEnter(). WifiSelectionActivity owns
WiFi mode; KOReaderSyncActivity should not touch it before the
sub-activity runs.
* A wifiOff() helper (SNTP stop + disconnect + WIFI_OFF with settling
delays) is extracted into the anonymous namespace and called at every
web-session exit point:
- "Apply remote" path in loop() — before onSyncComplete()
- performUpload() success path
- performUpload() failure path
- onExit() (safety net for all other exit paths)
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_ and two days of
blood, sweat and heavy swearing...
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Update "UI Font Size" to "Reader Font Size", to match the rest of the
"Reader" settings and clarify that the setting doesn't change the UI
font.
* **What changes are included?**
Changes the `english.yaml` string and USER_GUIDE.md entry.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
NO
## Summary
This PR aims to reduce the complexity of the status bar by splitting the
setting into 5:
- Chapter Page Count
- Book Progress %
- Progress Bar
- Chapter Title
- Battery Indicator
These are located within the new StausBarSettings activity, which also
shows a preview of the bar the user has created
<img width="513" height="806" alt="image"
src="https://github.com/user-attachments/assets/cdf852fb-15d8-4da2-a74f-fd69294d7b05"
/>
<img width="483" height="797" alt="image"
src="https://github.com/user-attachments/assets/66fc0c0d-ee51-4d31-b70d-e2bc043205d1"
/>
When updating from a previous version, the user's past settings are
honoured.
## Additional Context
The PR aims to remove any duplication of status bar code where possible,
and extracts the status bar rendering into a new component - StatusBar
It also adds a new (optional) padding option to the progress bar to
allow the status bar to be shifted upwards - this is only intended for
use in the settings.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code?
No - although did help to decode some C++ errors
---------
Co-authored-by: Arthur Tazhitdinov <lisnake@gmail.com>
## Summary
**What is the goal of this PR?**
Correction to #1157, which (embarrassingly) failed to actually include
the updated font header files. (Maybe we should generate these at build
time?)
Add Latin Extended-B glyphs for Croatian, Romanian, Pinyin, and European
diacritical variants. Fixes#921.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY, confirmed
codepoint ranges with Claude**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**
Conrgegate the changes of #1074 , #1013 , and extended upon #911 by
@lkristensen
New function implemented in GfxRenderer.cpp
```C++
std::vector<std::string> GfxRenderer::wrappedText(const int fontId, const char* text, const int maxWidth,
const int maxLines, const EpdFontFamily::Style style) const
```
Applied logic to all uses in Lyra, Lyra Extended, and base theme
(continue reading card as pointed out by @znelson
## Additional Context



---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
* **What is the goal of this PR?** During low memory situations (which I
encounterde a lot during my recent bugfixing activities) a cover was
considered rendered even if the buffer could not be stored.
* **What changes are included?** Proper assignment of flag logic
## Additional Context
-
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
When update activity finds no update or fails, the "Back" button label
was missing. Fixes#1089.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
Probable fix for #1118. `sanitizeFilename` was only passing through
ASCII characters from filenames. It now maintains valid UTF-8
codepoints, including non-ASCII multibyte sequences. Truncation happens
at a maximum number of bytes, rather than characters, to prevent
filenames with many multibyte sequences from unexpectedly exceeding
FAT32 limits.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, I described #1118
to Claude and it suggested sanitizeFilename as the likely cause**_
## Summary
**What is the goal of this PR?**
Add Latin Extended-B glyphs for Croatian, Romanian, Pinyin, and European
diacritical variants. Fixes#921.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY, confirmed
codepoint ranges with Claude**_
Small edits of the French translation.
## Summary
* **What is the goal of this PR?**
Small fixes of the French translation : fixes on missing/unclear rows,
usage of technical terms better suited for an e-reader GUI, shorter
sentences.
* **What changes are included?**
See above and in the .yaml files; only translations have changed, no
code edit.
## Additional Context
* Nothing else
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Summary
**What is the goal of this PR?**
Following up on #1156: generated language header files should be
ignored.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
I18nKeys.h and I18nStrings.h are generated by gen_i18n.py prior to each
build, so we do not need to maintain a checked-in copy of these files.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
## Summary
* **What is the goal of this PR?** During debugging of #1092 i
desperately needed to monitor the biggest allocatable block of memory on
the heap
* **What changes are included?** Added informaqtion to debug output,
amended monitor utility to pick it up
## Additional Context
---
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Implements the Italian language
translation for CrossPoint Reader.
* **What changes are included?**
* Added [lib/I18n/translations/italian.yaml] with Italian translations
for all strings.
* Generated the necessary C++ files by running the [gen_i18n.py] script.
* Added myself to the [docs/translators.md] file under the Italian
section.
## Additional Context
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
* **What is the goal of this PR?**
Add missing `STR_SCREENSHOT_BUTTON`
## Additional Context
After the screenshot feature was added, a new translation line was
introduced
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO **_
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
* **What is the goal of this PR?**
speed increase gh workflows, optimized for pioarduino Platform
* **What changes are included?**
remove pip and pip cache
install and use `uv`
use pioarduino core instead of Platformio core for optimal performance
with pioarduino Platform
## Additional Context
- signed off by the maintainer of pioarduino
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_NO
## Summary
Bookerly's native TrueType hinting is effectively a no-op at the sizes
used here, causing FreeType to place stems at inconsistent sub-pixel
positions. This results in the 'k' stem (8-bit fringe: 0x38=56) falling
just below the 2-bit quantization threshold while 'l' and 'h' stems
(fringes: 0x4C=76, 0x40=64) land above it --- making 'k' visibly
narrower (2.00px vs 2.33px effective width).
FreeType's auto-hinter snaps all stems to consistent grid positions,
normalizing effective stem width to 2.67px across all glyphs.
Adds --force-autohint flag to fontconvert.py and applies it to Bookerly
only. NotoSans, OpenDyslexic, and Ubuntu fonts are unaffected.
Here is an example of before/after. Take notice of the vertical stems on
characters like `l`, `k`, `n`, `i`, etc. The font is Bookerly 12pt
regular:
**BEFORE**:

**AFTER**:

Claude generated this script to quantitatively determine that this
change makes the vertical stems on a variety of characters more
consistent for Bookerly _only_.
<details>
<summary>Python script</summary>
```python
#!/usr/bin/env python3
"""Compare stem consistency across all font families with and without
auto-hinting.
Run from repo root:
python3 compare_all_fonts.py
"""
import freetype
DPI = 150
CHARS = ["k", "l", "h", "i", "b", "d"]
SIZES = [12, 14, 16, 18]
FONTS = {
"Bookerly":
"lib/EpdFont/builtinFonts/source/Bookerly/Bookerly-Regular.ttf",
"NotoSans":
"lib/EpdFont/builtinFonts/source/NotoSans/NotoSans-Regular.ttf",
"OpenDyslexic":
"lib/EpdFont/builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf",
"Ubuntu": "lib/EpdFont/builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf",
}
MODES = {
"default": freetype.FT_LOAD_RENDER,
"autohint": freetype.FT_LOAD_RENDER | freetype.FT_LOAD_FORCE_AUTOHINT,
}
def q4to2(v):
if v >= 12:
return 3
elif v >= 8:
return 2
elif v >= 4:
return 1
else:
return 0
def get_stem_eff(face, char, flags):
gi = face.get_char_index(ord(char))
if gi == 0:
return None
face.load_glyph(gi, flags)
bm = face.glyph.bitmap
w, h = bm.width, bm.rows
if w == 0 or h == 0:
return None
p2 = []
for y in range(h):
row = []
for x in range(w):
row.append(q4to2(bm.buffer[y * bm.pitch + x] >> 4))
p2.append(row)
# Measure leftmost stem in stable middle rows
mid_start, mid_end = h // 4, h - h // 4
widths = []
for y in range(mid_start, mid_end):
first = next((x for x in range(w) if p2[y][x] > 0), -1)
if first < 0:
continue
last = first
for x in range(first, w):
if p2[y][x] > 0:
last = x
else:
break
eff = sum(p2[y][x] for x in range(first, last + 1)) / 3.0
widths.append(eff)
return round(sum(widths) / len(widths), 2) if widths else None
def main():
for font_name, font_path in FONTS.items():
try:
freetype.Face(font_path)
except Exception:
print(f"\n {font_name}: SKIPPED (file not found)")
continue
print(f"\n{'=' * 80}")
print(f" {font_name}")
print(f"{'=' * 80}")
for size in SIZES:
print(f"\n {size}pt:")
print(f" {'':6s}", end="")
for c in CHARS:
print(f" '{c}' ", end="")
print(" | spread")
for mode_name, flags in MODES.items():
face = freetype.Face(font_path)
face.set_char_size(size << 6, size << 6, DPI, DPI)
vals = []
print(f" {mode_name:6s}", end="")
for c in CHARS:
v = get_stem_eff(face, c, flags)
vals.append(v)
print(f" {v:5.2f}" if v else " N/A", end="")
valid = [v for v in vals if v is not None]
spread = max(valid) - min(valid) if len(valid) >= 2 else 0
marker = " <-- inconsistent" if spread > 0.5 else ""
print(f" | {spread:.2f}{marker}")
if __name__ == "__main__":
main()
```
</details>
Here are the results. The table compares how the font-generation
`autohint` flag affects the range of widths of various characters. Lower
`spread` mean that glyph stroke widths should appear more consistent.
```
Spread = max stem width - min stem width across glyphs (lower = more consistent):
┌──────────────┬──────┬─────────┬──────────┬──────────┐
│ Font │ Size │ Default │ Autohint │ Winner │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ Bookerly │ 12pt │ 1.49 │ 1.12 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 14pt │ 1.39 │ 1.13 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 16pt │ 1.38 │ 1.16 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 18pt │ 1.90 │ 1.58 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ NotoSans │ 12pt │ 1.16 │ 0.94 │ mixed │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 14pt │ 0.83 │ 1.14 │ default │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 16pt │ 1.41 │ 1.51 │ default │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 18pt │ 1.74 │ 1.63 │ mixed │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ OpenDyslexic │ 12pt │ 2.22 │ 1.44 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 14pt │ 2.57 │ 3.29 │ default │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 16pt │ 3.13 │ 2.60 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 18pt │ 3.21 │ 3.23 │ ~tied │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ Ubuntu │ 12pt │ 1.25 │ 1.31 │ default │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 14pt │ 1.41 │ 1.64 │ default │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 16pt │ 2.21 │ 1.71 │ autohint │
├──────────────┼──────┼─────────┼──────────┼──────────┤
│ │ 18pt │ 1.80 │ 1.71 │ autohint │
└──────────────┴──────┴─────────┴──────────┴──────────┘
```
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? I used AI to make sure I'm
not doing something stupid, since I'm not a typography expert. I made
the changes though.
## Summary
Replace the default esp32-XXXXXXXXXXXX hostname with
CrossPoint-Reader-AABBCCDDEEFF (full MAC address) so the device is
easily identifiable on the router's client list.
## Summary
Fix redefinition of `FILE_*` macro.
Note that there will still be 2 warning:
```
.pio/libdeps/default/WebSockets/src/WebSocketsClient.cpp: In member function 'void WebSocketsClient::clientDisconnect(WSclient_t*, const char*)':
.pio/libdeps/default/WebSockets/src/WebSocketsClient.cpp:573:31: warning: 'virtual void NetworkClient::flush()' is deprecated: Use clear() instead. [-Wdeprecated-declarations]
573 | client->tcp->flush();
| ~~~~~~~~~~~~~~~~~~^~
```
--> I assume the upstream library need to fix it
And:
```
src/activities/Activity.cpp:8:1: warning: 'noreturn' function does return
8 | }
| ^
```
Will be fixed in #1016
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
## Summary
* **What is the goal of this PR?**
Fix inconsistent WiFi strings in Czech translation.
* **What changes are included?**
Only a few `Wi-Fi` strings changed to `WiFi` to maintain consistency.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Added Romanian translations for newly addded strings
* **What changes are included?**
Just the translations in the localisation file.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
- This PR updates the USER_GUIDE.md to match recent changes, in
particular 1.1.0, alongside some minor copyediting.
* **What changes are included?**
- Renamed titles for screens to match current UI.
- Sorted Settings section by order in UI, added subheadings for Settings
pages, and added all current settings.
- Updated System Navigation behaviour description to match #726.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
This is an admittedly quick edit I did to get USER_GUIDE.md up to
scratch alongside the release of 1.1.0. I'm a writer, not a programmer,
so there are some things that will probably need improvement.
- ~Recent Books needs to be added, something I could add to this PR if
needed.~
- ~Remap Front Buttons might need to be updated.~
These have been added in later commits, Remap Front Buttons might still
need more detail.
- The UI Theme, Embedded Style, Hyphenation, WiFi Networks, KOReader
Sync, and Clear Reading Cache settings might need better (or more
technically specific) descriptions.
Two questions I have:
- ~Should the new Settings subheadings be added to the Table of
Contents?~ Added in later commits.
- The Manual of Style/formatting for USER_GUIDE.md, especially in the
Settings section, is somewhat inconsistent. Let me know if any of my
edits don't fit with this.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
NO
---------
Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary
Ref: https://github.com/crosspoint-reader/crosspoint-reader/issues/1110
Power lock is automatically acquired on `render()`. However, instead of
using `render()`, sleep activity render everything right inside
`onEnter()`, so no power lock was acquired.
After https://github.com/crosspoint-reader/crosspoint-reader/pull/1016 ,
the power lock will also be acquired on activity transition.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
## Summary
* **What is the goal of this PR?** Address expected hyphenation issue
from
https://github.com/crosspoint-reader/crosspoint-reader/issues/998#issuecomment-3940533510
* Closes#998
* **What changes are included?** Add `„` (U+201E, _Double Low-9
Quotation Mark_), `‚` (U+201A, _Single Low-9 Quotation Mark_) and `‹`
(U+2039, _Single Left-pointing Angle Quotation Mark_) exceptions, other
quote types were handled correctly.
**Before**
<img width="480" height="155" alt="hyph3"
src="https://github.com/user-attachments/assets/e06b4071-2c8c-4814-965d-96fbe302a450"
/>
**After**
<img width="480" height="154" alt="hyph3-fix"
src="https://github.com/user-attachments/assets/4f7f5406-e200-451c-8bee-3f410cc84bbe"
/>
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
Consolidated repeated logic to fall back to REPLACEMENT_GLYPH.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
Small cleanup to make getTheme and getMetrics methods on UITheme const.
They return const refs, so updated call sites to use `const auto&`.
Realistically this won't make much performance difference, but it better
conveys the nature of theme metrics being shared const state.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Add a link to files to download on
web server
## Additional Context
* There was already an api to download files, i just added a link to it
for files.
---
### AI Usage
Did you use AI tools to help write this code? **NO**
Addresses #621
---------
Co-authored-by: Eliz Kilic <elizk@google.com>
## Summary
Ref:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1047#discussion_r2838439305
To reproduce:
1. Open file transfer
2. Join a network
3. Once it's connected, press (hold) back
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
## Summary
* **What is the goal of this PR?**
A Ukrainian translation for the GUI
* **What changes are included?**
Everything according to
https://github.com/crosspoint-reader/crosspoint-reader/blob/master/docs/i18n.md
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Nope
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_ as a
consistency validation
## Summary
* Remove miniz and move completely to uzlib
* Move uzlib interfacing to InflateReader to better modularise inflation
code
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? Yes, Claude helped with
the extraction and refactor
## Summary
* Resolve several build warnings
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
- Upgrade platform from espressif32 6.12.0 (Arduino Core 2.0.17) to
pioarduino 55.03.37 (Arduino Core 3.3.7, ESP-IDF 5.5.2)
- Add WebDAV Class 1 server (RFC 4918) - SD card can be mounted as a
network drive
- I also slightly fixed the SDK and also made a [pull request
](https://github.com/open-x4-epaper/community-sdk/pull/21)
First PR #1030 (was closed because the implementation was based on an
old version of the libraries)
Issue #439
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
* platformio.ini is a repository based config for platformio and cannot
be modified without constant nagging of git to include it into you
commits
* PlatformIO allows you to split your configuration into multiple files
using the extra_configs option in the [platformio] block. This
effectively merges other .ini files into your main one. This will be
silently ignored if such a file does not exist
## Additional Context
* Modifiy platformio.ini and add a .gitignore entry to ignore your local
config
* eg my own ``platformio.local.ini``:
```
[env:custom]
extends = base
build_flags =
${base.build_flags}
-DCROSSPOINT_VERSION=\"${crosspoint.version}-custom\"
; inclusion of additional fonts is disabled in custom builds to save space
-DOMIT_FONTS
```
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _NO_
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
- **SleepActivity.cpp**: Add missing `file.close()` calls in 3 code
paths that open BMP files for sleep screen rendering but never close
them before returning. Affects random custom sleep images, the
`/sleep.bmp` fallback, and book cover sleep screens.
- **CrossPointWebServer.cpp**: Add missing `dir.close()` in the delete
handler when `Storage.open()` returns a valid `FsFile` that is not a
directory.
## Context
SdFat is configured with `DESTRUCTOR_CLOSES_FILE=0`, which means
`FsFile` objects are **not** automatically closed when they go out of
scope. Every opened file must be explicitly closed.
The SleepActivity leaks are particularly impactful because they occur on
every sleep cycle. While ESP32 deep sleep clears RAM on wake, these
leaks can still affect the current session if sleep screen rendering is
triggered multiple times (e.g., cover preview, or if deep sleep fails to
engage).
The web server leak in `handleDelete()` is a minor edge case (directory
path that opens successfully but `isDirectory()` returns false), but
it's still worth fixing for correctness.
## Test plan
- [x] Verify sleep screen still renders correctly (custom BMP, fallback,
cover modes)
- [x] Verify folder deletion still works via the web UI
- [ ] Monitor free heap before/after sleep screen rendering to confirm
no leak
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Jan Bažant <janbazant@Jan--Mac-mini.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
* **What is the goal of this PR?** Enhances the file manager with
multi-select deletion functionality and improved UI formatting.
* **What changes are included?**
* Added multi-select capability for file deletion in the web interface
* Fixed formatting issues in file table for folder rows
* Updated [.gitignore] to exclude additional build artifacts and cache
files
* Refactored CrossPointWebServer.cpp to support batch file deletion
* Enhanced FilesPage.html with improved UI for file selection and
deletion
## Additional Context
* The file deletion endpoint now handles multiple files in a single
request, improving efficiency when removing multiple files
* Changes are focused on the web file manager component only
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
---------
Co-authored-by: Jessica Harrison <jessica.harrison@entelect.co.za>
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
* This PR introduces a migration from binary file storage to JSON-based
storage for application settings, state, and various credential stores.
This improves readability, maintainability, and allows for easier manual
configuration editing.
* Benefits:
- Settings files are now JSON and can be easily read/edited manually
- Easier to inspect application state and settings during development
- JSON structure is more flexible for future changes
* Drawback: around 15k of additional flash usage
* Compatibility: Seamless migration preserves existing user data
## Additional Context
1. New JSON I/O Infrastructure files:
- JsonSettingsIO: Core JSON serialization/deserialization logic using
ArduinoJson library
- ObfuscationUtils: XOR-based password obfuscation for sensitive data
2. Migrated Components (now use JSON storage with automatic binary
migration):
- CrossPointSettings (settings.json): Main application settings
- CrossPointState (state.json): Application state (open book, sleep
mode, etc.)
- WifiCredentialStore (wifi.json): WiFi network credentials (Password
Obfuscation: Sensitive data like WiFi passwords, uses XOR encryption
with fixed keys. Note: This is obfuscation, not cryptographic security -
passwords can be recovered with the key)
- KOReaderCredentialStore (koreader.json): KOReader sync credentials
- RecentBooksStore (recent.json): Recently opened books list
3. Migration Logic
- Forward Compatibility: New installations use JSON format
- Backward Compatibility: Existing binary files are automatically
migrated to JSON on first load
- Backup Safety: Original binary files are renamed with .bak extension
after successful migration
- Fallback Handling: If JSON parsing fails, system falls back to binary
loading
4. Infrastructure Updates
- HalStorage: Added rename() method for backup operations
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _** YES**_
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
### Summary
This PR introduces a lightweight contributor onboarding docs section
under `docs/contributing/` and improves local formatting ergonomics for
first-time contributors.
The goal is to make CrossPoint easier to contribute to for software
developers who are new to embedded systems (like me), while keeping
onboarding modular and aligned with existing project docs.
### What changed
- Added contributor docs hub: `docs/contributing/README.md`
- Added focused onboarding pages:
- `docs/contributing/getting-started.md`
- `docs/contributing/architecture.md`
- `docs/contributing/development-workflow.md`
- `docs/contributing/testing-debugging.md`
- Linked contributor docs from `README.md` for discoverability
- Expanded architecture documentation with Mermaid diagrams
- Improved `bin/clang-format-fix`:
- prefers `clang-format-21` when available
- validates formatter version and fails fast with a clear message if too
old
- handles missing positional arg safely
- Updated docs to explain common `clang-format` setup/version issues and
install paths (including fallback steps when `clang-format-21` is
unavailable in default apt sources)
### Why
- There was no dedicated contributor onboarding path; first-time
contributors had to infer workflow from multiple files.
- New contributors (especially from non-embedded backgrounds) need a
clear mental model of architecture, runtime flow, and debugging process.
- Local formatting setup caused avoidable friction due to clang-format
version mismatch (`.clang-format` expects newer keys used in CI).
- The updates make contribution setup more predictable, reduce
onboarding confusion, and align local checks with CI expectations.
### Additional context
- No firmware behavior/runtime logic was changed; this PR focuses on
contributor experience and tooling clarity.
---
### AI Usage
> Did you use AI tools to help write this code?
Yes, I used AI tools to assist with generating the documentation. I then
manually reviewed, tested, and refined the code to ensure it works
correctly. please feel free to point out any discrepancies or areas for
improvement.
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Implement automatic dark theme on server files.
Instead of a big change proposed in
https://github.com/crosspoint-reader/crosspoint-reader/pull/837, this PR
introduces a simple implementation of light/dark themes.
* **What changes are included?**
- Choose `#6e9a82` as accent color (taken from
)
- Implement a very basic media query for dark themes (`@media
(prefers-color-scheme: dark)`)
- Update style using CSS variables
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
We can think of it as a incremental enhancement, this is the first phase
of a series of PRs (hopefully).
Next steps/Phases:
1. Light/Dark themes (this PR)
2. Load external CSS file to avoid duplication
3. HTML enhancement (for example, use dialog element instead of divs)
4. Use SVG instead of emojis
5. Use Vite + Typescript to improve DX and have better minification
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
---------
Co-authored-by: carlosbonadeo <carlosbonadeo@skyscanner.net>
## Summary
_Revision to @blindbat's #802. Description comes from the original PR._
- Replace `std::list` with `std::vector` for word storage in `TextBlock`
and `ParsedText`
- Use index-based access (`words[i]`) instead of iterator advancement
(`std::advance(it, n)`)
- Remove the separate `continuesVec` copy that was built from
`wordContinues` for O(1) access — now unnecessary since
`std::vector<bool>` already provides O(1) indexing
## Why
`std::list` allocates each node individually on the heap with 16 bytes
of prev/next pointer overhead per node. For text layout with many small
words, this means:
- Scattered heap allocations instead of contiguous memory
- Poor cache locality during iteration (each node can be anywhere in
memory)
- Per-node malloc/free overhead during construction and destruction
`std::vector` stores elements contiguously, giving better cache
performance during the tight rendering and layout loops. The
`extractLine` function also benefits: list splice was O(1) but required
maintaining three parallel iterators, while vector range construction
with move iterators is simpler and still efficient for the small
line-sized chunks involved.
## Files changed
- `lib/Epub/Epub/blocks/TextBlock.h` / `.cpp`
- `lib/Epub/Epub/ParsedText.h` / `.cpp`
## AI Usage
YES
## Test plan
- [ ] Open an EPUB with mixed formatting (bold, italic, underline) —
verify text renders correctly
- [ ] Open a book with justified text — verify word spacing is correct
- [ ] Open a book with hyphenation enabled — verify words break
correctly at hyphens
- [ ] Navigate through pages rapidly — verify no rendering glitches or
crashes
- [ ] Open a book with long paragraphs — verify text layout matches
pre-change behavior
---------
Co-authored-by: Kuanysh Bekkulov <kbekkulov@gmail.com>
## Summary
* This PR fixes decomposed diacritic handling end-to-end:
- Hyphenation: normalize common Latin base+combining sequences to
precomposed codepoints before Liang pattern matching, so decomposed
words hyphenate correctly
- Rendering: correct combining-mark placement logic so non-spacing marks
are attached to the preceding base glyph in normal and rotated text
rendering paths, with corresponding text-bounds consistency updates.
- Hyphenation around non breaking space variants have been fixed (and
extended)
- Hyphenation of terms that already included of hyphens were fixed to
include Liang pattern application (eg "US-Satellitensystem" was
*exclusively* broken at the existing hyphen)
## Additional Context
* Before
<img width="800" height="480" alt="2"
src="https://github.com/user-attachments/assets/b9c515c4-ab75-45cc-8b52-f4d86bce519d"
/>
* After
<img width="480" height="800" alt="fix1"
src="https://github.com/user-attachments/assets/4999f6a8-f51c-4c0a-b144-f153f77ddb57"
/>
<img width="800" height="480" alt="fix2"
src="https://github.com/user-attachments/assets/7355126b-80c7-441f-b390-4e0897ee3fb6"
/>
* Note 1: the hyphenation fix is not a 100% bullet proof implementation.
It adds composition of *common* base+combining sequences (e.g. O +
U+0308 -> Ö) during codepoint collection. A complete solution would
require implementing proper Unicode normalization (at least NFC,
possibly NFKC in specific cases) before hyphenation and rendering,
instead of hand-mapping a few combining marks. That was beyond the scope
of this fix.
* Note 2: the render fix should be universal and not limited to the
constraints outlined above: it properly x-centers the compund glyph over
the previous one, and it uses at least 1pt of visual distance in y.
Before:
<img width="478" height="167" alt="Image"
src="https://github.com/user-attachments/assets/f8db60d5-35b1-4477-96d0-5003b4e4a2a1"
/>
After:
<img width="479" height="180" alt="Image"
src="https://github.com/user-attachments/assets/1b48ef97-3a77-475a-8522-23f4aca8e904"
/>
* This should resolve the issues described in #998
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
## Summary
* This PR fixes decomposed diacritic handling end-to-end:
- Hyphenation: normalize common Latin base+combining sequences to
precomposed codepoints before Liang pattern matching, so decomposed
words hyphenate correctly
- Rendering: correct combining-mark placement logic so non-spacing marks
are attached to the preceding base glyph in normal and rotated text
rendering paths, with corresponding text-bounds consistency updates.
- Hyphenation around non breaking space variants have been fixed (and
extended)
- Hyphenation of terms that already included of hyphens were fixed to
include Liang pattern application (eg "US-Satellitensystem" was
*exclusively* broken at the existing hyphen)
## Additional Context
* Before
<img width="800" height="480" alt="2"
src="https://github.com/user-attachments/assets/b9c515c4-ab75-45cc-8b52-f4d86bce519d"
/>
* After
<img width="480" height="800" alt="fix1"
src="https://github.com/user-attachments/assets/4999f6a8-f51c-4c0a-b144-f153f77ddb57"
/>
<img width="800" height="480" alt="fix2"
src="https://github.com/user-attachments/assets/7355126b-80c7-441f-b390-4e0897ee3fb6"
/>
* Note 1: the hyphenation fix is not a 100% bullet proof implementation.
It adds composition of *common* base+combining sequences (e.g. O +
U+0308 -> Ö) during codepoint collection. A complete solution would
require implementing proper Unicode normalization (at least NFC,
possibly NFKC in specific cases) before hyphenation and rendering,
instead of hand-mapping a few combining marks. That was beyond the scope
of this fix.
* Note 2: the render fix should be universal and not limited to the
constraints outlined above: it properly x-centers the compund glyph over
the previous one, and it uses at least 1pt of visual distance in y.
Before:
<img width="478" height="167" alt="Image"
src="https://github.com/user-attachments/assets/f8db60d5-35b1-4477-96d0-5003b4e4a2a1"
/>
After:
<img width="479" height="180" alt="Image"
src="https://github.com/user-attachments/assets/1b48ef97-3a77-475a-8522-23f4aca8e904"
/>
* This should resolve the issues described in #998
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* The goal is to fix the title of books in the Home Screen.
Before

After:

* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? YES, Cursor
## Summary
* **What is the goal of this PR?**
* improve Spanish translations
* **What changes are included?**
- Fix typos and accents (Librería, conexión, etc.)
- Translate untranslated strings (BOOTING, SLEEPING, etc.)
- Improve consistency and conciseness
- Fix question mark placement (¿...?)
- Standardize terminology (Punto de Acceso, Suspensión, etc.)
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* The goal is to fix the title of books in the Home Screen.
Before

After:

* **What changes are included?**
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? YES, Cursor
New BmpViewerActivity opens, parses, and renders BMP files with centered
aspect-ratio-preserving display and localized back navigation. Library
file filter extended to include .bmp. ReaderActivity routes BMP paths to
the new viewer. LyraTheme button hint backgrounds switched to rounded
rect fills to prevent overflow artifacts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Rewrite README.md to distinguish the mod fork from upstream CrossPoint
Reader. Add mod feature documentation (bookmarks, dictionary, clock, book
management, table rendering, reader menu overhaul, etc.), updated feature
checklist, upstream compatibility notes, and mod-specific build instructions.
Bump version from 1.1.1-rc to 1.1.2 to align with upstream approaching
release.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add '/' as explicit hyphen delimiter and relax the alphabetic-surround
requirement for '/' and '-' in buildExplicitBreakInfos so URL path
segments can be line-wrapped. Includes repeated-separator guard to
prevent breaks between consecutive identical separators (e.g. "http://").
Co-authored-by: Cursor <cursoragent@cursor.com>
- Fix device freeze at end-of-book by deferring EndOfBookMenuActivity
creation from render() to loop() (avoids RenderLock deadlock) in
EpubReaderActivity and XtcReaderActivity
- Add initialSkipRelease to BookManageMenuActivity to prevent stale
Confirm release from triggering actions when opened via long-press
- Add initialSkipRelease to MyLibraryActivity for long-press Browse
Files -> archive navigation
- Thread skip-release through HomeActivity callback and main.cpp
- Fix HomeActivity stale cover buffer after archive/delete by fully
resetting render state (freeCoverBuffer, firstRenderDone, etc.)
- Swap short/long-press actions in .archive context: short-press opens
manage menu, long-press unarchives and opens the book
- Add deferred open pattern (pendingOpenPath) to wait for Confirm
release before navigating to reader after unarchive
- Add BookManager::cleanupEmptyArchiveDirs() to remove empty parent
directories after unarchive/delete inside .archive
- Add optional unarchivedPath output parameter to BookManager::unarchiveBook
- Restyle EndOfBookMenuActivity to standard list layout with proper
header, margins, and button hints matching other screens
- Change EndOfBookMenuActivity back button hint to "« Back"
- Add Table of Contents option to EndOfBookMenuActivity
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
* **What is the goal of this PR?**
* improve Spanish translations
* **What changes are included?**
- Fix typos and accents (Librería, conexión, etc.)
- Translate untranslated strings (BOOTING, SLEEPING, etc.)
- Improve consistency and conciseness
- Fix question mark placement (¿...?)
- Standardize terminology (Punto de Acceso, Suspensión, etc.)
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Implements new feature for viewing .bmp files directly from the "Browse
Files" menu.
* **What changes are included?**
You can now view .bmp files when browsing. You can click the select
button to open the file, and then click back to close it and continue
browsing in the same location. Once open a file will display on the
screen with no additional options to interact outside of exiting with
the back button.
The attached video shows this feature in action:
https://github.com/user-attachments/assets/9659b6da-abf7-4458-b158-e11c248c8bef
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
The changes implemented in #884 are also present here as this feature is
actually what led to me noticing this issue. I figured I would add that
PR as a separate request in case that one could be more easily merged
given this feature is significantly more complicated and will likely be
subject to more intense review.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **YES**
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Add support for Catalan language user interface.
* **What changes are included?**
A new i18n file catalan.yml.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? NO
RenderLock is a pure RAII wrapper with no unlock() method. The lock
releases naturally when render() returns. Build now succeeds cleanly.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add txt_* prefix to the directory check so TXT book caches are also
removed. After clearing all cache directories, call RECENT_BOOKS.clear()
to remove stale entries that would show missing covers on the home screen.
Co-authored-by: Cursor <cursoragent@cursor.com>
Interactive menu shown when reaching the end of a book with options:
Archive Book, Delete Book, Back to Beginning, Close Book, Close Menu.
Wired into EpubReaderActivity, XtcReaderActivity, and TxtReaderActivity
(TXT shows menu when user tries to advance past the last page).
Co-authored-by: Cursor <cursoragent@cursor.com>
EpubReaderMenuActivity now shows "Manage Book" instead of "Delete
Book Cache". Selecting it opens BookManageMenuActivity as a sub-activity
with Archive, Delete, Delete Cache, and Reindex options. New menu
actions (ARCHIVE_BOOK, DELETE_BOOK, REINDEX_BOOK, REINDEX_BOOK_FULL)
are forwarded to EpubReaderActivity and handled via BookManager.
Co-authored-by: Cursor <cursoragent@cursor.com>
Long-press Confirm on a recent book opens the BookManageMenuActivity.
Long-press Confirm on Browse Files navigates directly to /.archive/.
Wires onMyLibraryOpenWithPath callback through main.cpp to HomeActivity.
Co-authored-by: Cursor <cursoragent@cursor.com>
Long-pressing Confirm on a book file in MyLibraryActivity or
RecentBooksActivity opens the BookManageMenuActivity popup with
Archive/Delete/Delete Cache/Reindex options. Actions are executed
via BookManager and the file list is refreshed afterward.
Co-authored-by: Cursor <cursoragent@cursor.com>
Change HomeActivity, MyLibraryActivity, and RecentBooksActivity base
class from Activity to ActivityWithSubactivity. Adds subActivity
guard at top of each loop(). No new behavior, just enabling sub-activity
hosting for the upcoming BookManageMenuActivity integration.
Co-authored-by: Cursor <cursoragent@cursor.com>
Contextual popup menu for book management with Archive/Unarchive,
Delete, Delete Cache Only, and Reindex options. Supports long-press
on Reindex to trigger full reindex including cover/thumbnail regen.
Co-authored-by: Cursor <cursoragent@cursor.com>
BookManager provides static functions for archive/unarchive/delete/
deleteCache/reindex operations on books, centralizing cache path
computation and file operations. Archive preserves directory structure
under /.archive/ and renames cache dirs to match new path hashes.
RecentBooksStore: :clear() added for bulk cache clearing use case.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add STR_MANAGE_BOOK, STR_ARCHIVE_BOOK, STR_UNARCHIVE_BOOK,
STR_DELETE_BOOK, STR_DELETE_CACHE_ONLY, STR_REINDEX_BOOK,
STR_BROWSE_ARCHIVE, status messages, STR_BACK_TO_BEGINNING,
and STR_CLOSE_MENU for the manage books and end-of-book menus.
Co-authored-by: Cursor <cursoragent@cursor.com>
Forward SDCardManager::rename() through the HAL layer for
file/directory move operations needed by book archiving.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a "Sync Clock" action in Settings > Clock that connects to WiFi
(auto-connecting to saved networks or prompting for selection) and
performs a blocking NTP time sync. Shows the synced time on success
with an auto-dismiss countdown, or an error on failure.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reduces ParsedText::layoutAndExtractLines CPU time 5–9% via two
independent optimizations from jpirnay's PR #1027:
- 128-entry direct-mapped word-width cache (4 KB BSS, FNV-1a hash)
absorbs redundant getTextAdvanceX calls across paragraphs
- Early exit in hyphenateWordAtIndex when prefix exceeds available
width (ascending byte-offset order guarantees monotonic widths)
- Reusable prefix string buffer eliminates per-candidate substr allocs
- Reserve hint for lineBreakIndices in computeLineBreaks
List-specific upstream changes (splice, iterator style) not applicable
as mod already uses std::vector (PR #1038). Benchmark infrastructure
excluded (removed by author in final commit).
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace per-pixel drawPixel calls with byte-level framebuffer writes
for fillRect, axis-aligned drawLine, and fillRectDither. Adds
fillPhysicalHSpanByte/fillPhysicalHSpan helpers that write directly
to physical rows with memset and partial-byte masking.
Also applies coderabbit nitpick: fillPolygon scanline fill now uses
fillPhysicalHSpan for Landscape orientations.
Upstream: https://github.com/crosspoint-reader/crosspoint-reader/pull/1055
Co-authored-by: Cursor <cursoragent@cursor.com>
When auto-sleep triggers after inactivity, the CPU remains at 10 MHz
(low power mode) during sleep screen rendering, causing it to draw
much slower than expected.
Co-authored-by: Cursor <cursoragent@cursor.com>
When the selected row's filename overflows the available text width
(with extension), the row expands to 2 lines with smart text wrapping.
The file extension moves to the second row (right-aligned). Non-selected
rows retain single-line truncation.
Key behaviors:
- 3-tier text wrapping: preferred delimiters (" - ", " -- ", en/em-dash),
word boundaries, then character-level fallback
- Row-height line spacing for natural visual rhythm
- Icons aligned with line 1 (LyraTheme)
- Pagination uses effectivePageItems with anti-leak clamping to prevent
page boundary shifts while ensuring all items remain accessible
- Boundary item duplication: items bumped from a page due to expansion
appear at the top of the next page, guarded against cascading
Co-authored-by: Cursor <cursoragent@cursor.com>
- #1038 (partial): Add .erase() for consumed words in layoutAndExtractLines
to fix redundant early flush bug; fix wordContinues flag in hyphenateWordAtIndex
- #1037: Add combining mark handling for hyphenation (NFC-like precomposition)
and rendering (base glyph tracking in EpdFont, GfxRenderer including CCW)
- #1045: Shorten STR_FORGET_BUTTON labels across all 9 translation files
- #1019: Display file extensions in File Browser via getFileExtension helper
- Pull romanian.yaml from upstream/master (merged PR #987)
Co-authored-by: Cursor <cursoragent@cursor.com>
Port three new upstream commits and align the existing #1002 port:
- PR #1014: Strip unused CSS rules by filtering unsupported selector
types (+, >, [, :, #, ~, *, descendants) in processRuleBlockWithStyle.
Fix normalized() trailing whitespace to also strip newlines.
- PR #1018: Add deleteCache() to CssParser, move CSS_CACHE_VERSION to
static class member, remove stale cache on version mismatch, invalidate
section caches (Storage.removeDir) when CSS is rebuilt. Refactor
parseCssFiles() to early-return when cache exists.
- PR #990: Adapt classic theme continue-reading card width to cover
aspect ratio (clamped to 90% screen width), increase homeTopPadding
20->40, fix centering with rect.x offset for boxX/continueBoxX.
- #1002 alignment: Add tryInterpretLength() to skip non-numeric CSS
values (auto, inherit), add "both width and height set" image sizing
branch in ChapterHtmlSlimParser.
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
To add upport for a romanian language user interface.
* **What changes are included?**
A new i18n file `romanian.yml`
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
Just fixed a typo `Xtink` -> `Xteink`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* Destroy CSS Cache file when invalid
## Additional Context
* Fixes issue where it would attempt to rebuild every book open
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
* Destroy CSS Cache file when invalid
## Additional Context
* Fixes issue where it would attempt to rebuild every book open
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
* **What is the goal of this PR?**
* **What changes are included?**
- Adapt card width to cover image aspect ratio in Classic theme
- Increase homeTopPadding from 20px to 40px to avoid overlap with
battery icon
- Card width now calculated from BMP dimensions instead of fixed 240px
- Maximum card width limited to 90% of screen width
- Falls back to original behavior (half screen width) when no cover
available
## Additional Context
* Solve conflicts in PR #683
Before:
<img width="1052" height="1014" alt="image"
src="https://github.com/user-attachments/assets/6c857913-d697-4e9e-9695-443c0a4c0804"
/>
PR:

---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* In a sample book I loaded, it had 900+ CSS rules, and took up 180kB of
memory loading the cache in
* Looking at the rules, a lot of them were completely useless as we only
ever apply look for 3 kinds of CSS rules:
* `tag`
* `tag.class1`
* `.class1`
* Stripping out CSS rules with descendant, nested, attribute matching,
sibling matching, pseudo element selection (as we never actually read
these from the cache) reduced the rule count down to 200
## Additional Context
* I've left in `.class1.class2` rules for now, even though we
technically can never match on them as they're likely to be addressed
soonest out of the all the CSS expansion
* Because we don't ever delete the CSS cache, users will need to delete
the book cache through the menu in order to get this new logic
* A new PR should be done up to address this - tracked here
https://github.com/crosspoint-reader/crosspoint-reader/issues/1015
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
When no width is set for an image, the image currently automatically
sets to the width of the page. However, with this fix, the parser will
use the height and aspect ratio of the image to properly set a height
for it. See below example:
Before:

After:

* **What changes are included?✱
Changes to the CSS parser
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? YES, Cursor
## Summary
Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/1011
Use double FAST_REFRESH for image pages to prevent grayscale washout,
HALF_REFRESH sets e-ink particles too firmly for the grayscale LUT to
adjust, causing washed-out images (especially large, light-gray ones).
Replace HALF_REFRESH with @pablohc's double FAST_REFRESH technique:
blank only the image bounding box area, then re-render with images. This
clears ghosting while keeping particles loosely set for grayscale.
## Additional Context
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Improve legibility of Cover Icons on the home page and elsewhere. Fixes
#898
Re implements the changes made in #907 that were overwritten by the new
lyra themes
* **What changes are included?**
Cover outline is now shown even when cover is found to prevent issues
with low contrast covers blending into the background. Photo is attached
below:
<img width="1137" height="758" alt="Untitled (4)"
src="https://github.com/user-attachments/assets/21ae6c94-4b43-4a0c-bec7-a6e4c642ffad"
/>
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Re implements the changes made in #907 that were overwritten by the new
lyra themes
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**Yes**_
## Summary
* ``renderChar`` checked ``is2Bit`` on every pixel inside the inner
loop, even though the value is constant for the lifetime of a single
glyph
* Moved the branch above both loops so each path (2-bit antialiased /
1-bit monochrome) runs without a per-pixel conditional
* Eliminates redundant work in the two inner loops that render font
glyphs to the frame buffer, targeting ``renderChar`` and
``drawTextRotated90CW`` in ``GfxRenderer.cpp``
## Additional Context
* Measured on device using a dedicated framebuffer benchmark (no display
refresh). 100 repetitions of "The quick brown fox jumps".
| Test | Before | After | Change |
|-----------------|-----------------|-----------------|---------|
| drawText UI12 | 1,337 µs/call | 1,024 µs/call | −23%|
| drawText Bookerly14 | 2.174 µs / call | 1,847 µs/call | −15% |
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_ Claude did
the analysis and wrote the benchmarks
## Summary
* **What is the goal of this PR?** Small fix for bug I found.
## Additional Context
1. `RecentBooksActivity::loop()` calls
`onSelectBook(recentBooks[selectorIndex].path)` - passing a
**reference** to the path
2. `onSelectBook` is `onGoToReader` which first calls `exitActivity()`
3. `exitActivity()` triggers `RecentBooksActivity::onExit()` which call
`recentBooks.clear()`
4. The string reference `initialEpubPath` is now a **dangling
reference** - the underlying string has been destroyed
5. When the reference is then used in `new ReaderActivity(...)`, it
reads garbage memory
6. The same issue occurs in `HomeActivity` at line 200 with the same
pattern
The fix is to make a copy of the string in `onGoToReader` before calling
`exitActivity()`, so the path data persists even after the activity
clears its data structures.
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_ Claude found
the bug, after I shared with it a serial log.
## Summary
* **What is the goal of this PR?**
* **What changes are included?**
- Adapt card width to cover image aspect ratio in Classic theme
- Increase homeTopPadding from 20px to 40px to avoid overlap with
battery icon
- Card width now calculated from BMP dimensions instead of fixed 240px
- Maximum card width limited to 90% of screen width
- Falls back to original behavior (half screen width) when no cover
available
## Additional Context
* Solve conflicts in PR #683
Before:
<img width="1052" height="1014" alt="image"
src="https://github.com/user-attachments/assets/6c857913-d697-4e9e-9695-443c0a4c0804"
/>
PR:

---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* In a sample book I loaded, it had 900+ CSS rules, and took up 180kB of
memory loading the cache in
* Looking at the rules, a lot of them were completely useless as we only
ever apply look for 3 kinds of CSS rules:
* `tag`
* `tag.class1`
* `.class1`
* Stripping out CSS rules with descendant, nested, attribute matching,
sibling matching, pseudo element selection (as we never actually read
these from the cache) reduced the rule count down to 200
## Additional Context
* I've left in `.class1.class2` rules for now, even though we
technically can never match on them as they're likely to be addressed
soonest out of the all the CSS expansion
* Because we don't ever delete the CSS cache, users will need to delete
the book cache through the menu in order to get this new logic
* A new PR should be done up to address this - tracked here
https://github.com/crosspoint-reader/crosspoint-reader/issues/1015
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
When no width is set for an image, the image currently automatically
sets to the width of the page. However, with this fix, the parser will
use the height and aspect ratio of the image to properly set a height
for it. See below example:
Before:

After:

* **What changes are included?✱
Changes to the CSS parser
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? YES, Cursor
Cherry-pick upstream commit 07d715e which refactors renderChar and
drawTextRotated90CW into a template-based renderCharImpl, hoisting
the is2Bit branch outside inner pixel loops for 15-23% speedup.
Additionally extends the template with Rotated90CCW to fix two bugs
in the mod's drawTextRotated90CCW: operator precedence in bmpVal
calculation and missing compressed font support via getGlyphBitmap.
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Epub image support was added in #556. The goal of this PR is to document
that in the readme.
* **What changes are included?**
Only the checkmark in the readme.
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/1011
Use double FAST_REFRESH for image pages to prevent grayscale washout,
HALF_REFRESH sets e-ink particles too firmly for the grayscale LUT to
adjust, causing washed-out images (especially large, light-gray ones).
Replace HALF_REFRESH with @pablohc's double FAST_REFRESH technique:
blank only the image bounding box area, then re-render with images. This
clears ghosting while keeping particles loosely set for grayscale.
## Additional Context
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< PARTIALLY >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Improve legibility of Cover Icons on the home page and elsewhere. Fixes
#898
Re implements the changes made in #907 that were overwritten by the new
lyra themes
* **What changes are included?**
Cover outline is now shown even when cover is found to prevent issues
with low contrast covers blending into the background. Photo is attached
below:
<img width="1137" height="758" alt="Untitled (4)"
src="https://github.com/user-attachments/assets/21ae6c94-4b43-4a0c-bec7-a6e4c642ffad"
/>
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Re implements the changes made in #907 that were overwritten by the new
lyra themes
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**Yes**_
## Summary
* ``renderChar`` checked ``is2Bit`` on every pixel inside the inner
loop, even though the value is constant for the lifetime of a single
glyph
* Moved the branch above both loops so each path (2-bit antialiased /
1-bit monochrome) runs without a per-pixel conditional
* Eliminates redundant work in the two inner loops that render font
glyphs to the frame buffer, targeting ``renderChar`` and
``drawTextRotated90CW`` in ``GfxRenderer.cpp``
## Additional Context
* Measured on device using a dedicated framebuffer benchmark (no display
refresh). 100 repetitions of "The quick brown fox jumps".
| Test | Before | After | Change |
|-----------------|-----------------|-----------------|---------|
| drawText UI12 | 1,337 µs/call | 1,024 µs/call | −23%|
| drawText Bookerly14 | 2.174 µs / call | 1,847 µs/call | −15% |
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_ Claude did
the analysis and wrote the benchmarks
## Summary
* **What is the goal of this PR?** Small fix for bug I found.
## Additional Context
1. `RecentBooksActivity::loop()` calls
`onSelectBook(recentBooks[selectorIndex].path)` - passing a
**reference** to the path
2. `onSelectBook` is `onGoToReader` which first calls `exitActivity()`
3. `exitActivity()` triggers `RecentBooksActivity::onExit()` which call
`recentBooks.clear()`
4. The string reference `initialEpubPath` is now a **dangling
reference** - the underlying string has been destroyed
5. When the reference is then used in `new ReaderActivity(...)`, it
reads garbage memory
6. The same issue occurs in `HomeActivity` at line 200 with the same
pattern
The fix is to make a copy of the string in `onGoToReader` before calling
`exitActivity()`, so the path data persists even after the activity
clears its data structures.
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_ Claude found
the bug, after I shared with it a serial log.
## Summary
The introduction of `HalGPIO` moved the `BatteryMonitor battery` object
into the member function `HalGPIO::getBatteryPercentage()`.
Then, with the introduction of `HalPowerManager`, this function was
moved to `HalPowerManager::getBatteryPercentage()`.
However, the original `BatteryMonitor battery` object is still utilized
by themes for displaying the battery percentage.
This PR replaces these deprecated uses of `BatteryMonitor battery` with
the new `HalPowerManager::getBatteryPercentage()` function.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO **_
Reorder refresh branches so image+AA pages always use the double
FAST_REFRESH technique instead of occasionally falling through to
HALF_REFRESH when the refresh counter expires. Image pages no longer
count toward the full refresh cadence. Remove experimental Method B
toggle (USE_IMAGE_DOUBLE_FAST_REFRESH / displayWindow).
Co-authored-by: Cursor <cursoragent@cursor.com>
Parse CSS height/width into CssStyle for images and use aspect-ratio-
preserving logic when CSS dimensions are set. Falls back to viewport-fit
scaling when no CSS dimensions are present. Includes divide-by-zero
guards and viewport clamping with aspect ratio rescaling.
- Add imageHeight field to CssStyle/CssPropertyFlags
- Parse CSS height declarations into imageHeight
- Add imageHeight + width to cache serialization (bump cache v2->v3)
- Replace viewport-fit-only image scaling with CSS-aware sizing
Co-authored-by: Cursor <cursoragent@cursor.com>
Cherry-pick two bug fixes from upstream PR #992:
- fix(GfxRenderer): Null-safety in getSpaceWidth/getTextAdvanceX to
prevent Load access fault when bold/italic font variants lack certain
glyphs (upstream 3e2c518)
- fix(PNGdec): Increase PNG_MAX_BUFFERED_PIXELS to 16416 for 2048px
wide images and add pre-decode buffer overflow guard (upstream b8e743e)
Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary
* Increased `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 in
`platformio.ini` to support up to 2048px wide RGBA images
* adds a check to abort decoding and log an error if the required PNG
scanline buffer exceeds the configured `PNG_MAX_BUFFERED_PIXELS`,
preventing possible buffer overruns.
* fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/993
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* Increased `PNG_MAX_BUFFERED_PIXELS` from 6402 to 16416 in
`platformio.ini` to support up to 2048px wide RGBA images
* adds a check to abort decoding and log an error if the required PNG
scanline buffer exceeds the configured `PNG_MAX_BUFFERED_PIXELS`,
preventing possible buffer overruns.
* fixes
https://github.com/crosspoint-reader/crosspoint-reader/issues/993
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?**
I flashed the last revision before commit f1740dbe, and chapter indexing
worked without any crashes.
After applying f1740dbe, the same chapter consistently triggered a
device reboot during indexing.
The affected chapter contains inline equation images surrounded by
styled (bold/italic) text that includes special math/symbol characters.
## Additional Context
Prior to f1740dbe, both `getTextAdvanceX()` and `getSpaceWidth()` always
measured text using `EpdFontFamily::REGULAR`, regardless of the actual
style.
Commit f1740dbe improved correctness by passing the active style so
spacing is calculated using the actual bold/italic font variant.
However, bold and italic variants have narrower Unicode coverage than
the regular font. When a character exists in the regular font but not in
the selected styled variant, `pdFont::getGlyph()` returns `nullptr`.
The updated measurement functions did not check for this and immediately
dereferenced the pointer:
`width += font.getGlyph(cp, style)->advanceX; // nullptr->advanceX`
Because `advanceX` is located at byte offset 2 within `EpdGlyph`,
dereferencing a null pointer caused the CPU to attempt a load from
address `0x00000002`, resulting in a RISC-V:
Load access fault
MCAUSE = 5
MTVAL = 2
## Fix
Added null-safety checks to both `getTextAdvanceX()` and
`getSpaceWidth()`, following the same pattern used in the rendering
path:
If the glyph is missing in the selected style → fall back to the
replacement glyph.
If the replacement glyph is also unavailable → treat the character as
zero-width.
This preserves the improved style-correct spacing while preventing
crashes.
No behavioral changes occur for characters that are supported by the
selected font variant.
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_
I encounter this bug while testing 1.1.0 RC.
I pasted the serial log to Claude, which identify the bug and fixed it.
I can confirm now the chapter in question is indexed and loaded
correctly.
## Summary
* **What is the goal of this PR?**
I flashed the last revision before commit f1740dbe, and chapter indexing
worked without any crashes.
After applying f1740dbe, the same chapter consistently triggered a
device reboot during indexing.
The affected chapter contains inline equation images surrounded by
styled (bold/italic) text that includes special math/symbol characters.
## Additional Context
Prior to f1740dbe, both `getTextAdvanceX()` and `getSpaceWidth()` always
measured text using `EpdFontFamily::REGULAR`, regardless of the actual
style.
Commit f1740dbe improved correctness by passing the active style so
spacing is calculated using the actual bold/italic font variant.
However, bold and italic variants have narrower Unicode coverage than
the regular font. When a character exists in the regular font but not in
the selected styled variant, `pdFont::getGlyph()` returns `nullptr`.
The updated measurement functions did not check for this and immediately
dereferenced the pointer:
`width += font.getGlyph(cp, style)->advanceX; // nullptr->advanceX`
Because `advanceX` is located at byte offset 2 within `EpdGlyph`,
dereferencing a null pointer caused the CPU to attempt a load from
address `0x00000002`, resulting in a RISC-V:
Load access fault
MCAUSE = 5
MTVAL = 2
## Fix
Added null-safety checks to both `getTextAdvanceX()` and
`getSpaceWidth()`, following the same pattern used in the rendering
path:
If the glyph is missing in the selected style → fall back to the
replacement glyph.
If the replacement glyph is also unavailable → treat the character as
zero-width.
This preserves the improved style-correct spacing while preventing
crashes.
No behavioral changes occur for characters that are supported by the
selected font variant.
---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_
I encounter this bug while testing 1.1.0 RC.
I pasted the serial log to Claude, which identify the bug and fixed it.
I can confirm now the chapter in question is indexed and loaded
correctly.
Replace HALF_REFRESH with double FAST_REFRESH technique for the BW
pass when dithered letterbox fill is active. This avoids the e-ink
crosstalk and image corruption that occurred when HALF_REFRESH drove
large areas of dithered gray pixels simultaneously.
Revert the hash-based block dithering workaround (bayerCrossesBwBoundary,
hashBlockDither) back to standard Bayer dithering for all gray ranges,
since the root cause was HALF_REFRESH rather than the dithering pattern
itself.
Letterbox fill is now included in all three render passes (BW, LSB, MSB)
so the greyscale LUT treats letterbox pixels identically to cover pixels,
maintaining color-matched edges.
Co-authored-by: Cursor <cursoragent@cursor.com>
I've been reading "Children of Time" over the last days and that book,
annyoingly, has some tabular content.
This content is relevant for the story so I needed some really basic way
to at least be able to read those tables.
This commit simply renders the contents of table cells as separate
paragraphs with a small header describing its position in the table. For
me, it's better than nothing.
## Summary
* **What is the goal of this PR?**
Implements really basic table support
* **What changes are included?**
* Minimal changes to ChapterHtmlSlimParser
* A demo book in test/epubs
## Additional Context
Here's some screenshots of the demo-book I provide with this PR.


---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**PARTIALLY**_
_Little bit of guidance on what to touch, parts of the impl, rest
manually._
## Summary
**What is the goal of this PR?**
* Implement feature request
[#954](https://github.com/crosspoint-reader/crosspoint-reader/issues/954)
* Ensure cover images are scaled up to match the dimensions of the
screen, as well as scaled down
**What changes are included?**
* Naïve implementation for scaling up the source image
## Additional Context
If you find the extra comments to be excessive I can pare them back.
Edit: Fixed title
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Adresses Feature Request #896
* **What changes are included?**
Changed key dimensions, initial positions and margins.
## Additional Context
The keyboard now looks like this:

---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* What is the goal of this PR?
- Allow users to create custom sleep screen images with standard tools
(ImageMagick, GIMP, etc.) that render cleanly on the e-ink display
without dithering artifacts. Previously, avoiding dithering required
non-standard 2-bit BMPs that no standard image editor can produce. ( see
issue #931 )
* What changes are included?
- Add 4-bit BMP format support to Bitmap.cpp (standard format, widely
supported by image tools)
- Auto-detect "native palette" images: if a BMP has ≤4 palette entries
and all luminances map within ±21 of the display's native gray levels
(0, 85, 170, 255), skip dithering entirely and direct-map pixels
- Clarify pixel processing strategy with three distinct paths:
error-diffusion dithering, simple quantization, or direct mapping
- Add scripts/generate_test_bmps.py for generating test images across
all supported BMP formats
## Additional Context
* The e-ink display has 4 native gray levels. When a BMP already uses
exactly those levels, dithering adds noise to what should be clean
output. The native palette detection uses a ±21 tolerance (~10%) to
handle slight rounding from color space conversions in image tools.
Users can now create a 4-color grayscale BMP with (imagemagic example):
```
convert input.png -colorspace Gray -colors 4 -depth
```
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _** YES**_
## Summary
- `UITheme::currentTheme` was a raw owning pointer with no destructor,
causing a heap leak every time `setTheme()` was called (e.g. on
theme change via settings reload)
## Additional Context
- Replaced `const BaseTheme*` with `std::unique_ptr<BaseTheme>` so the
previous theme object is automatically deleted on reassignment
- Added `<memory>` include to `UITheme.h`; allocations updated to
`std::make_unique<>` in `UITheme.cpp`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_ (identified by
claude though)
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
1. Go to the first page in a .epub file.
2. Hit `Up` button
3. Get teleported to the last page :)
`TxtRenderActivity` seems to have this if check, but EPUB one does not.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
`hasPrintableChars` does a pass over text before rendering. It looks up
glyphs in the font and measures dimensions, returning early if the text
results in zero size.
This additional pass doesn't offer any benefit over moving straight to
rendering the text, because the rendering loop already gracefully
handles missing glyphs. This change saves an extra pass over all
rendered text.
Note that both `hasPrintableChars` and `renderChar` replace missing
glyphs with `glyph = getGlyph(REPLACEMENT_GLYPH)`, so there's no
difference for characters which are not present in the font.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* Fixes:
https://github.com/crosspoint-reader/crosspoint-reader/issues/947
**What changes are included?**
* Check to see if there's free heap memory before processing CSS (should
we be doing this type of check or is it better to just crash if we
exhaust the memory?)
* Skip CSS files larger than 128kb
## Additional Context
* I found that a copy of `Release it` contained a 250kb+ CSS file, from
the homepage of the publisher. It has nothing to do with the epub, so we
should just skip it
* Major question: Are there better ways to detect CSS that doesn't
belong in a book, or is this size-based approach valid?
* Another question: Are there any epubs we know of that legitimately
include >128kb CSS files?
Code changes themselves created with an agent, all investigation and
write-up done by human. If you (the maintainers) would prefer a
different fix for this issue, let me know.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* GfxRender did handle horizontal and vertical lines but had a TODO for
arbitrary lines.
* Added integer based Bresenham line drawing
## Additional Context
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* Original implementation had inconsistent positioning logic:
- When XPath parsing succeeded: incorrectly set pageNumber = 0 (always
beginning of chapter)
- When XPath parsing failed: used percentage for positioning (worked
correctly)
- Result: Positions restored to wrong locations depending on XPath
parsing success
- Mentioned in Issue #581
* Solution
- Unified ProgressMapper::toCrossPoint() to use percentage-based
positioning exclusively for both spine identification and intra-chapter
page calculation, eliminating unreliable XPath parsing entirely.
## Additional Context
* ProgressMapper.cpp: Simplified toCrossPoint() to always use percentage
for positioning, removed parseDocFragmentIndex() function
* ProgressMapper.h: Updated comments and removed unused function
declaration
* Tests confirmed appropriate positioning
* __Notabene: the syncing to another device will (most probably) end up
at the current chapter of crosspoints reading position. There is not
much we can do about it, as KOReader needs to have the correct XPath
information - we can only provide an apporximate position (plus
percentage) - the percentage information is not used in KOReaders
current implementation__
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? YES
## Summary
* **What is the goal of this PR?**
Add proper hyphenation support for the Ukrainian language.
* **What changes are included?**
- Added Ukrainian hyphenation rules/dictionary
## Additional Context
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
This change fixes an issue I noticed while reading where occasionally,
especially in italics, some words would have too much space between
them. The problem was that word width calculations were including any
negative X overhang, and combined with a space before the word, that can
lead to an inconsistently large space.
## Additional Context
Screenshots of some problematic text:
| In CrossPoint 1.0 | With this change |
| -- | -- |
| <img
src="https://github.com/user-attachments/assets/87bf0e4b-341f-4ba9-b3ea-38c13bd26363"
width="400" /> | <img
src="https://github.com/user-attachments/assets/bf11ba20-c297-4ce1-aa07-43477ef86fc2"
width="400" /> |
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* During chapter parsing, every <img> tag triggered ZIP decompression
and an SD card write regardless of whether the image format was
supported. The mandatory delay(50) after each SD write compounded the
cost. A chapter with 6 GIF images (a common decorative element in older
EPUBs) wasted ~750 ms before any text rendering began.
* **What changes are included?**
Added an ``ImageDecoderFactory::isFormatSupported()`` check before any
file I/O in the img-handler. Only JPEG and PNG proceed to extraction;
all other formats (GIF, SVG, WebP, etc.) fall through immediately to
alt-text rendering with no SD card access.
## Additional Context
Measured impact on a representative chapter with 6 GIF decorations:
| | Before | After|
|-- | -- | --|
|Total parse time | ~882 ms | ~207 ms|
|Image handling | ~750 ms | ~76 ms|
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
Compress reader font bitmaps to reduce flash usage by 30.7%.
**What changes are included?**
- New `EpdFontGroup` struct and extended `EpdFontData` with
`groups`/`groupCount` fields
- `--compress` flag in `fontconvert.py`: groups glyphs (ASCII base group
+ groups of 8) and compresses each with raw DEFLATE
- `FontDecompressor` class with 4-slot LRU cache for on-demand
decompression during rendering
- `GfxRenderer` transparently routes bitmap access through
`getGlyphBitmap()` (compressed or direct flash)
- Uses `uzlib` for decompression with minimal heap overhead.
- 48 reader fonts (Bookerly, NotoSans 12-18pt, OpenDyslexic) regenerated
with compression; 5 UI fonts unchanged
- Round-trip verification script (`verify_compression.py`) runs as part
of font generation
## Additional Context
## Flash & RAM
| | baseline | font-compression | Difference |
|--|--------|-----------------|------------|
| Flash (ELF) | 6,302,476 B (96.2%) | 4,365,022 B (66.6%) | -1,937,454 B
(-30.7%) |
| firmware.bin | 6,468,192 B | 4,531,008 B | -1,937,184 B (-29.9%) |
| RAM | 101,700 B (31.0%) | 103,076 B (31.5%) | +1,376 B (+0.5%) |
## Script-Based Grouping (Cold Cache)
Comparison of uncompressed baseline vs script-based group compression
(4-slot LRU cache, cleared each page). Glyphs are grouped by Unicode
block (ASCII, Latin-1, Latin Extended-A, Combining Marks, Cyrillic,
General Punctuation, etc.) instead of sequential groups of 8.
### Render Time
| | Baseline | Compressed (cold cache) | Difference |
|---|---|---|---|
| **Median** | 414.9 ms | 431.6 ms | +16.7 ms (+4.0%) |
| **Pages** | 37 | 37 | |
### Memory Usage
| | Baseline | Compressed (cold cache) | Difference |
|---|---|---|---|
| **Heap free (median)** | 187.0 KB | 176.3 KB | -10.7 KB |
| **Heap free (min)** | 186.0 KB | 166.5 KB | -19.5 KB |
| **Largest block (median)** | 148.0 KB | 128.0 KB | -20.0 KB |
| **Largest block (min)** | 148.0 KB | 120.0 KB | -28.0 KB |
### Cache Effectiveness
| | Misses/page | Hit rate |
|---|---|---|
| **Compressed (cold cache)** | 2.1 | 99.85% |
------
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Implementation was done by Claude Code (Opus 4.6) based on a plan
developed collaboratively. All generated font headers were verified with
an automated round-trip decompression test. The firmware was compiled
successfully but has not yet been tested on-device.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
I want to preface this PR by stating that the proposed changes are
subjective to people's opinions. The following is just my suggestion,
but I'm of course open to changes.
The popups in the currently implemented version of the Lyra theme feel a
bit out of place. This PR suggests an updated version which looks a bit
more polished and in line with the rest of the theme.
I've also taken the liberty to remove the ellipsis behind the text of
the popups, as they made the popup feel a bit off balance (example
below).
With the applied changes, popups will look like this.

The vertical position is (more or less) aligned to be in line with the
sleep button. I'm aware the popup is used for other purposes aside from
the sleep message, but this still felt like a good place. It's also a
place where your eyes naturally 'rest'.
The popup has a small 2px white outline, neatly separating it from
whatever is behind it.
### Alternatives considered and rationale behind proposal
Initially I started out worked off the Figma design for the Lyra theme,
which [moves the
popups](https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2011-19296&t=Ppj6B2MrFRfUo9YX-1)
to the bottom of the screen. To me, this results in popups that are much
too easy to miss:

After this, I tried moving the popup back up (to the position of the
sleep button), but to me it still kinda disappeared into the text of the
book:

Inverting the colors of the popup made things stand out the perfect
amount in my opinion. The white outline separates the popup from what is
behind it.

This looked much better to me. The only thing that felt a bit off to me,
was the balance due to the ellipsis at the end of the popup text. Also,
"Entering Sleep..." felt a bit.. engineer-y. I felt something a bit more
'conversational' makes at all feel a bit more human-centric. But I'm no
copywriter, and English is not even my native language. So feel free to
chip in!
After tweaking that, I ended up with the final result:
_(Same picture as the first one shown in this PR)_

## Additional Context
* Figma design:
https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2011-19296&t=Ppj6B2MrFRfUo9YX-1
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
**What is the goal of this PR?**
In some places, button labels are omitted intentionally because the
button has no purpose in the activity. I noticed a few obvious cases,
like Home > File Transfer and Settings > System > Language, where the up
and down button labels were missing. This change fixes those and all
similar instances I could find.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
Continue my experiment from
https://github.com/crosspoint-reader/crosspoint-reader/pull/801
This PR add the ability to lower the CPU frequency on extended idle
period (currently set to 3 seconds). By default, the esp32c3 CPU is set
to 160MHz, and now on idle, we can reduce it to just 10MHz.
Note that while this functionality is already provided by [esp power
management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html),
the current Arduino build lacks of this, and enabling it is just too
complicated (not worth the effort compared to this PR)
Update: more info in
https://github.com/crosspoint-reader/crosspoint-reader/pull/852#issuecomment-3904562827
## Testing
Pre-condition for each test case: the battery is charged to 100%, and is
left plugged in after fully charged for an extra 1 hour.
The table below shows how much battery is **used** for a given duration:
| case / duration | 6 hrs | 12 hrs |
| --- | --- | --- |
| `delay(10)` | 26% | 48% |
| `delay(50)`, PR
https://github.com/crosspoint-reader/crosspoint-reader/pull/801 | 20% |
Not tested |
| `delay(50)` + low CPU freq (This PR) | Not tested | 25% |
| `delay(10)` + low CPU freq (1) | Not tested | Not tested |
(1) I decided not to test this case because it may not make sense. The
problem is that CPU frequency vs power consumption do not follow a
linear relationship, see
[this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes)
as an example. So, tight loop (10ms) + lower CPU freq significantly
impact battery life, because the active CPU time is now much higher
compared to the wall time.
**So in conclusion, this PR improves ~150% to ~200% battery use time per
charge.**
The projected battery life is now: ~36-48 hrs of reading time (normal
reading, no wifi)
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
## Summary
**What is the goal of this PR?**
Several methods in GfxRenderer were doing a `count()` followed by `at()`
on the fonts map, effectively doing the same map lookup unnecessarily.
This can be avoided by doing a single `find()` and reusing the iterator.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Fix a dangling pointer issue caused
by using `.c_str()` on a temporary `std::string`.
`basepath.substr()` creates a temporary `std::string`, and calling
`.c_str()` on it returns a pointer to its internal buffer (not a copy).
Since the temporary string is destroyed at the end of the full
expression, `folderName` ends up holding a dangling pointer, leading to
undefined behavior.
To solve this, we stores the result in a persistent `std::string`
object, ensuring the underlying buffer remains valid for the duration of
its use.
A similar pattern caused the behavior reported in
https://github.com/crosspoint-reader/crosspoint-reader/pull/728#issuecomment-3902529697
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
* **What is the goal of this PR?** Update translators.md to include all
the contributors from #728
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< NO >**_
## Summary
**What is the goal of this PR?**
Skip constructing a `std::string` just to get the underlying `c_str()`
buffer, when a string literal gives the same end result.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
This PR includes vocabulary and grammar fixes for Russian translation,
originally made as review comments
[here](https://github.com/crosspoint-reader/crosspoint-reader/pull/728).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
Issues solved: #729 and #739
## Summary
* **What is the goal of this PR?**
Currently, the battery icon and charge percentage were aligned to the
left even for the UI, where they were positioned on the right side of
the screen. This meant that when changing values of different numbers of
digits, the battery would shift, creating a block of icons and text that
was illegible.
* **What changes are included?**
- Add drawBatteryUi() method for right-aligned battery display in UI
headers
- Keep drawBattery() for left-aligned display in reader mode
- Extract drawBatteryIcon() helper to reduce code duplication
- Battery icon now stays fixed at right edge regardless of percentage
digits
- Text adjusts to left of icon in UI mode, to right of icon in reader
mode
## Additional Context
* Add any other information that might be helpful for the reviewer
* This fix applies to both themes (Base and Lyra).
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**< YES >**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Add a translators document for us to track which individuals have
volunteered to contribute in which languages.
* **What changes are included?**
Add a new document that includes who the translators are and what
languages they have volunteered for.
## Additional Context
This is primarily to keep a handle on the volunteers coming into the
repo. This will serve as a master list of all volunteer translators.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
---------
Signed-off-by: Andrew Brandt <brandt.andrew89@gmail.com>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Improve legibility of Cover Icons on the home page and elsewhere. Fixes
#898
* **What changes are included?**
Cover outline is now shown even when cover is found to prevent issues
with low contrast covers blending into the background. Photo is attached
below:
<img width="404" height="510" alt="Group 1 (4)"
src="https://github.com/user-attachments/assets/9d794b51-554b-486d-8520-6ef920548b9a"
/>
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Not much else to say here. I did simplify the logic in lyratheme.cpp
based on there no longer being a requirement for any non-cover specific
rendering differences.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Updating webserver.md documentation to align with 1.0.0 features
* **What changes are included?**
Added documentation for the following new features (including replacing
screenshots)
- file renaming
- file moving
- support for uploading any file type
- batch uploads
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Nothing comes to mind
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Empty Button Icons (I.E. Back button in the home menu) were still
rendering the full sized white rectangles going passed the boarders of
the little button nub. This was not visible on the home screen due to
the white background, but it does cause issues if we ever want to have
bmp files displayed while buttons are visible or implement a dark mode.
* **What changes are included?**
Made it so that when a button hint text is empty string or null the
displayed mini button nub does not have a white rectangle extending
passed the bounds of the mini button nub
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).
Having that extended rectangle was likely never noticed due to the only
space where that feature is used being the main menu where the
background is completely white. I am working on some new features that
would have an image displayed while there are button hints and noticed
this issue while implementing that.
One other note is that this only affects the Lyra Theme
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
## Summary
**What is the goal of this PR?**
This PR introduces Internationalization (i18n) support, enabling users
to switch the UI language dynamically.
**What changes are included?**
- Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage
language state and string retrieval.
- Data Structures:
- `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported
language.
- `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access.
- `lib/I18n/translations.csv`: single source of truth.
- Documentation: Added `docs/i18n.md` detailing the workflow for
developers and translators.
- New Settings activity:
`src/activities/settings/LanguageSelectActivity.h/cpp`
## Additional Context
This implementation (building on concepts from #505) prioritizes
performance and memory efficiency.
The core approach is to store all localized strings for each language in
dedicated arrays and access them via enums. This provides O(1) access
with zero runtime overhead, and avoids the heap allocations, hashing,
and collision handling required by `std::map` or `std::unordered_map`.
The main trade-off is that enums and string arrays must remain perfectly
synchronized—any mismatch would result in incorrect strings being
displayed in the UI.
To eliminate this risk, I added a Python script that automatically
generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which
will serve as the single source of truth for all translations. The full
design and workflow are documented in `docs/i18n.md`.
### Next Steps
- [x] Python script `generate_i18n.py` to auto-generate C++ files from
CSV
- [x] Populate translations.csv with initial translations.
Currently available translations: English, Español, Français, Deutsch,
Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!
**Status:** EDIT: ready to be merged.
As a proof of concept, the SPANISH strings currently mirror the English
ones, but are fully uppercased.
---
### AI Usage
Did you use AI tools to help write this code? _**< PARTIALLY >**_
I used AI for the black work of replacing strings with I18n references
across the project, and for generating the documentation. EDIT: also
some help with merging changes from master.
---------
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: yeyeto2788 <juanernestobiondi@gmail.com>
## Summary
Follow-up to
https://github.com/crosspoint-reader/crosspoint-reader/pull/774
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Release Notes
* **Refactor**
* Modernized internal synchronization mechanisms across multiple
components to improve code reliability and maintainability. All
functionality remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This partially fixes#769 but is dependent upon PR #827 being merged
along side this @daveallie. I removed my PNG conversion code after
finding out that PR was already created. Without this PR though that
book in #769 will still fail to load because of how it's stored in the
file
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
- EPUB books with PNG cover images now display covers on the home screen
instead of blank rectangles
- Adds `PngToBmpConverter` library mirroring the existing
`JpegToBmpConverter` pattern
- Uses miniz (already in the project) for streaming zlib decompression
of PNG IDAT data
- Supports all PNG color types (Grayscale, RGB, RGBA, Palette,
Gray+Alpha)
- Optimized for ESP32-C3: batch grayscale conversion, 2KB read buffer,
same area-averaging scaling and Atkinson dithering as the JPEG path
## Changes
- **New:** `lib/PngToBmpConverter/PngToBmpConverter.h` — Public API
matching JpegToBmpConverter's interface
- **New:** `lib/PngToBmpConverter/PngToBmpConverter.cpp` — Streaming PNG
decoder + BMP converter
- **Modified:** `lib/Epub/Epub.cpp` — Added `.png` handling in
`generateCoverBmp()` and `generateThumbBmp()`
## Test plan
- [x] Tested with EPUB files using PNG covers — covers appear correctly
on home screen
- [ ] Verify with various PNG color types (most stock EPUBs use 8-bit
RGB)
- [ ] Confirm no regressions with JPEG cover EPUBs
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
**New Features**
- Added PNG format support for EPUB cover and thumbnail images. PNG
files are automatically processed and cached alongside existing
supported formats. This enhancement enables users to leverage PNG cover
artwork when generating EPUB files, improving workflow flexibility and
compatibility with common image sources.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Nik Outchcunis <outchy@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Dave Allie <dave@daveallie.com>
## Summary
* I am getting miniz warning during compilation: "Using fopen, ftello,
fseeko, stat() etc. path for file I/O - this path may not support large
files."
* Disable the io module from miniz as it is not used and get rid of the
warning
## Additional Context
* the ZipFile.cpp implementation only uses tinfl_decompressor,
tinfl_init(), and tinfl_decompress() (low-level API) and does all ZIP
file parsing manually using SD card file I/O
* it never uses miniz's high-level file functions like
mz_zip_reader_init_file()
* so we can disable Miniz io-stack be setting MINIZ_NO_STDIO to 1
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? partially, let claude
inspect the codebase
## Summary
* If an EPUB has:
```
<dc:creator>J.R.R. Tolkien</dc:creator>
<dc:creator>Christopher Tolkien</dc:creator>
```
the current result for epub.author would provide : "J.R.R.
TolkienChristopher Tolkien" (no separator!)
* The fix will seperate multiple authors: "J.R.R. Tolkien, Christopher
Tolkien"
## Additional Context
* Simple fix in ContentOpfParser - I am not seeing any dependence on the
wrong concatenated result.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? NO
## Summary
* Include dictionary as in-scope
## Additional Context
* Discussion in
https://github.com/crosspoint-reader/crosspoint-reader/discussions/878
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? No
## Summary
**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Minor development tooling fix for nonstandard environments (NixOS,
FreeBSD, Guix, etc.)
**What changes are included?**
- environment relative shebang in `clang-format-fix`
- clang-format check in `clang-format-fix`
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**NO**_
## Summary
* The constant SETTINGS_CONST was hardcoded and needed to be updated
whenever an additional setting was added
* This is no longer necessary as the settings size will be determined
automatically on settings persistence
## Additional Context
* New settings need to be added (as previously) in saveToFile - that's
it
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? YES
---------
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
## Summary
Currently, each activity has to manage their own `displayTaskLoop` which
adds redundant boilerplate code. The loop is a wait loop which is also
not the best practice, as the `updateRequested` boolean is not protected
by a mutex.
In this PR:
- Move `displayTaskLoop` to the super `Activity` class
- Replace `updateRequested` with freeRTOS's [direct to task
notification](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/03-Direct-to-task-notifications/01-Task-notifications)
- For `ActivityWithSubactivity`, whenever a sub-activity is present, the
parent's `render()` automatically goes inactive
With this change, activities now only need to expose `render()`
function, and anywhere in the code base can call `requestUpdate()` to
request a new rendering pass.
## Additional Context
In theory, this change may also make the battery life a bit better,
since one wait loop is removed. Although the equipment in my home lab
wasn't been able to verify it (the electric current is too noisy and
small). Would appreciate if anyone has any insights on this subject.
Update: I managed to hack [a small piece of
code](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage)
that allow tracking CPU idle time.
The CPU load does decrease a bit (1.47% down to 1.39%), which make
sense, because the display task is now sleeping most of the time unless
notified. This should translate to a slightly increase in battery life
in the long run.
```
PR:
[40012] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[40012] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[50017] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[50017] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[60022] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[60022] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
master:
[20012] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[20012] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[30017] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[30017] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[40022] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[40022] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
```
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? **NO**
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Refactor**
* Streamlined rendering architecture by consolidating update mechanisms
across all activities, improving efficiency and consistency.
* Modernized synchronization patterns for display updates to ensure
reliable, conflict-free rendering.
* **Bug Fixes**
* Enhanced rendering stability through improved locking mechanisms and
explicit update requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: znelson <znelson@users.noreply.github.com>
## Summary
This PR applies a micro optimization on `SerializedHyphenationPatterns`,
which allow reading `rootOffset` directly without having to parse then
cache it.
It should not affect storage space since no new bytes are added.
This also gets rid of the linear cache search whenever
`liangBreakIndexes` is called. In theory, the performance should be
improved a bit, although it may be too small to be noticeable in
practice.
## Testing
master branch:
```
english: 99.1023%
french: 100%
german: 97.7289%
russian: 97.2167%
spanish: 99.0236%
```
This PR:
```
english: 99.1023%
french: 100%
german: 97.7289%
russian: 97.2167%
spanish: 99.0236%
```
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? PARTIALLY - mostly IDE
tab-autocompletions
## Summary
- Add embedded image support to EPUB rendering with JPEG and PNG
decoders
- Implement pixel caching system to cache decoded/dithered images to SD
card for faster re-rendering
- Add 4-level grayscale support for display
## Changes
### New Image Rendering System
- Add `ImageBlock` class to represent an image with its cached path and
display dimensions
- Add `PageImage` class as a new `PageElement` type for images on pages
- Add `ImageToFramebufferDecoder` interface for format-specific image
decoders
- Add `JpegToFramebufferConverter` - JPEG decoder with Bayer dithering
and scaling
- Add `PngToFramebufferConverter` - PNG decoder with Bayer dithering and
scaling
- Add `ImageDecoderFactory` to select appropriate decoder based on file
extension
- Add `getRenderMode()` to GfxRenderer for grayscale render mode queries
### Dithering and Grayscale
- Implement 4x4 Bayer ordered dithering for 4-level grayscale output
- Stateless algorithm works correctly with MCU block decoding
- Handles scaling without artifacts
- Add grayscale render mode support (BW, GRAYSCALE_LSB, GRAYSCALE_MSB)
- Image decoders and cache renderer respect current render mode
- Enables proper 4-level e-ink grayscale when anti-aliasing is enabled
### Pixel Caching
- Cache decoded/dithered images to `.pxc` files on SD card
- Cache format: 2-bit packed pixels (4 pixels per byte) with
width/height header
- On subsequent renders, load directly from cache instead of re-decoding
- Cache renderer supports grayscale render modes for multi-pass
rendering
- Significantly improves page navigation speed for image-heavy EPUBs
### HTML Parser Integration
- Update `ChapterHtmlSlimParser` to process `<img>` tags and extract
images from EPUB
- Resolve relative image paths within EPUB ZIP structure
- Extract images to cache directory before decoding
- Create `PageImage` elements with proper scaling to fit viewport
- Fall back to alt text display if image processing fails
### Build Configuration
- Add `PNG_MAX_BUFFERED_PIXELS=6402` to support up to 800px wide images
### Test Script
- Generate test EPUBs with annotated JPEG and PNG images
- Test cases cover: grayscale (4 levels), centering, scaling, cache
performance
## Test plan
- [x] Open EPUB with JPEG images - verify images display with proper
grayscale
- [x] Open EPUB with PNG images - verify images display correctly and no
crash
- [x] Navigate away from image page and back - verify faster load from
cache
- [x] Verify grayscale tones render correctly (not just black/white
dithering)
- [x] Verify large images are scaled down to fit screen
- [x] Verify images are centered horizontally
- [x] Verify page serialization/deserialization works with images
- [x] Verify images rendered in landscape mode
## Test Results
[png](https://photos.app.goo.gl/5zFUb8xA8db3dPd19)
[jpeg](https://photos.app.goo.gl/SwtwaL2DSQwKybhw7)








---
### AI Usage
Did you use AI tools to help write this code? _**< YES >**_
---------
Co-authored-by: Matthías Páll Gissurarson <mpg@mpg.is>
Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-16 19:56:59 +11:00
464 changed files with 375972 additions and 174979 deletions
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.
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:
| 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 |
- **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.
**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
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.
Project: Open-source e-reader firmware for Xteink X4 (ESP32-C3)
Mission: Provide a lightweight, high-performance reading experience focused on EPUB rendering on constrained hardware.
## AI Agent Identity and Cognitive Rules
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
---
## Development Environment Awareness
**CRITICAL**: Detect the host platform at session start to choose appropriate tools and commands.
### Platform Detection
```bash
# Detect platform (run once per session)
uname -s
# Returns: MINGW64_NT-* (Windows Git Bash), Linux, Darwin (macOS)
```
**Detection Required**: Run `uname -s` at session start to determine platform
### Platform-Specific Behaviors
- **Windows (Git Bash)**: Unix commands, `C:\` paths in Windows but `/` in bash, limited glob (use `find`+`xargs`)
- **Linux/WSL**: Full bash, Unix paths, native glob support
* **NO PSRAM**: ESP32-C3 has no PSRAM capability (unlike ESP32-S3)
* **Single Buffer Mode**: Only ONE 48KB framebuffer (not double-buffered)
* Flash: 16MB (Instruction storage and static data)
* Display: 800x480 E-Ink (Slow refresh, monochrome, 1-2s full update)
* Framebuffer: 48,000 bytes (800 × 480 ÷ 8)
* Storage: SD Card (Used for books and aggressive caching)
### The Resource Protocol
1. Stack Safety: Limit local function variables to < 256 bytes. The ESP32-C3 default stack is small; use std::unique_ptr or static pools for larger buffers.
2. Heap Fragmentation: Avoid repeated new/delete in loops. Allocate buffers once during onEnter() and reuse them.
3. Flash Persistence: Large constant data (UI strings, lookup tables) MUST be marked static const to stay in Flash (Instruction Bus), freeing DRAM.
4. String Policy: Prohibit std::string and Arduino String in hot paths. Use std::string_view for read-only access and snprintf with fixed char[] buffers for construction.
5. UI Strings: All user-facing text must use the `tr()` macro (e.g., `tr(STR_LOADING)`) for i18n support. Never hardcode UI strings directly. For the avoidance of doubt, logging messages (LOG_DBG/LOG_ERR) can be hardcoded, but user-facing text must use `tr()`.
6.`constexpr` First: Compile-time constants and lookup tables must be `constexpr`, not just `static const`. This moves computation to compile time, enables dead-branch elimination, and guarantees flash placement. Use `static constexpr` for class-level constants.
7.`std::vector` Pre-allocation: Always call `.reserve(N)` before any `push_back()` loop. Each growth event allocates a new block (2×), copies all elements, then frees the old one — three heap operations that fragment DRAM. When the final size is unknown, estimate conservatively.
8. SPIFFS Write Throttling: Never write a settings file on every user interaction. Guard all writes with a value-change check (`if (newVal == _current) return;`). Progress saves during reading must be debounced — write on activity exit or every N page turns, not on every turn. SPIFFS sectors have a finite erase cycle limit.
---
## Project Architecture
### Build System: PlatformIO
**PlatformIO is BOTH a VS Code extension AND a CLI tool**:
1.**VS Code Extension** (Recommended):
* Extension ID: `platformio.platformio-ide` (see `.vscode/extensions.json`)
* File Names: Match Class names (e.g., EpubReaderActivity.cpp)
### Header Guards
* Use #pragma once for all header files.
### Memory Safety and RAII
* Smart Pointers: Prefer std::unique_ptr. Avoid std::shared_ptr (unnecessary atomic overhead for a single-core RISC-V).
* RAII: Use destructors for cleanup, but call file.close() or vTaskDelete() explicitly for deterministic resource release.
### ESP32-C3 Platform Pitfalls
#### `std::string_view` and Null Termination
`string_view` is *not* null-terminated. Passing `.data()` to any C-style API (`drawText`, `snprintf`, `strcmp`, SdFat file paths) is undefined behaviour when the view is a substring or a view of a non-null-terminated buffer.
**Rule**: `string_view` is safe only when passing to C++ APIs that accept `string_view`. For any C API boundary, convert explicitly:
```cpp
// WRONG - undefined behaviour if view is a substring:
All code runs from flash via the instruction cache. During SPI flash operations (OTA write, SPIFFS commit, NVS update) the cache is briefly suspended. Any code that can execute during this window — ISRs in particular — must reside in IRAM or it will crash silently.
```cpp
// ISR handler: must be in IRAM
voidIRAM_ATTRgpioISR(){...}
// Data accessed from IRAM_ATTR code: must be in DRAM, never a flash const
staticDRAM_ATTRuint32_tisrEventFlags=0;
```
**Rules**:
- All ISR handlers: `IRAM_ATTR`
- Data read by `IRAM_ATTR` code: `DRAM_ATTR` (a flash-resident `static const` will fault)
- Normal task code does **not** need `IRAM_ATTR`
#### ISR vs Task Shared State
`xSemaphoreTake()` (mutex) **cannot** be called from ISR context — it will crash. Use the correct primitive for each communication direction:
ESP32-C3 faults on unaligned multi-byte loads. Never cast a `uint8_t*` buffer to a wider pointer type and dereference it directly. Use `memcpy` for any unaligned read:
This applies to all cache deserialization code and any raw buffer-to-struct casting. `__attribute__((packed))` structs have the same hazard when accessed via member reference.
#### Template and `std::function` Bloat
Each template instantiation generates a separate binary copy. `std::function<void()>` adds ~2–4 KB per unique signature and heap-allocates its closure. Avoid both in library code and any path called from the render loop:
```cpp
// Avoid — heap-allocating, large binary footprint:
std::function<void()>callback;
// Prefer — zero overhead:
void(*callback)()=nullptr;
// For member function + context (common activity callback pattern):
structCallback{void*ctx;void(*fn)(void*);};
```
When a template is necessary, limit instantiations: use explicit template instantiation in a `.cpp` file to prevent the compiler from generating duplicates across translation units.
Implements progress tracking for book downloads using
UITheme progress bar component with heap-safe updates.
Tested in all 4 orientations with 5MB+ files.
```
### When to Commit
**DO commit when**:
- User explicitly requests: "commit these changes"
- Feature is complete and tested on device
- Bug fix is verified working
- Refactoring preserves all functionality
- All tests pass (`pio run` succeeds)
**DO NOT commit when**:
- Changes are untested on actual hardware
- Build fails or has warnings
- Experimenting or debugging in progress
- User hasn't explicitly requested commit
- Files excluded by `.gitignore` would be included — always run `git status` and cross-check against `.gitignore` before staging (e.g., `*.generated.h`, `.pio/`, `compile_commands.json`, `platformio.local.ini`)
**Rule**: **If uncertain, ASK before committing.**
---
## Generated Files and Build Artifacts
### Files Generated by Build Scripts
**NEVER manually edit these files** - they are regenerated automatically:
1. **HTML Headers** (generated by `scripts/build_html.py`):
- `src/network/html/*.generated.h`
- **Source**: HTML templates in `data/html/` directory
- **Triggered**: During PlatformIO `pre:` build step
- **To modify**: Edit source HTML in `data/html/`, not generated headers
2. **I18n Headers** (generated by `scripts/gen_i18n.py`):
1. **ALWAYS increment version** BEFORE changing binary structure
2. Version mismatch → Cache auto-invalidated and regenerated
3. Document format changes in `docs/file-formats.md`
**Example** (incrementing section format version):
```cpp
// lib/Epub/Epub/Section.cpp
static constexpr uint8_t SECTION_FILE_VERSION = 13; // Was 12, now 13
// Add new field to structure
struct PageLine {
// ... existing fields ...
uint16_t newField; // New field added
};
```
---
Philosophy: We are building a dedicated e-reader, not a Swiss Army knife. If a feature adds RAM pressure without significantly improving the reading experience, it is Out of Scope.
For more details about the scope of the project, see the [SCOPE.md](SCOPE.md) document.
@@ -154,6 +156,8 @@ For more details on the internal file structures, see the [file formats document
Contributions are very welcome!
If you are new to the codebase, start with the [contributing docs](./docs/contributing/README.md).
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
@@ -39,6 +39,14 @@ usability over "swiss-army-knife" functionality.
***Complex Annotation:** No typed out notes. These features are better suited for devices with better input
capabilities and more powerful chips.
### In-scope — Technically Unsupported
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
## 3. Idea Evaluation
While I appreciate the desire to add new and exciting features to CrossPoint Reader, CrossPoint Reader is designed to be
Button layout can be customized in **[Settings](#35-settings)**.
Button layout can be customized in the **[Controls Settings](#363-controls)**.
### Taking a Screenshot
When the Power Button and Volume Down button are pressed at the same time, it will take a screenshot and save it in the folder `screenshots/`.
Alternatively, while reading a book, press the **Confirm** button to open the reader menu and select **Take screenshot**.
---
@@ -45,9 +56,9 @@ Button layout can be customized in **[Settings](#35-settings)**.
### Power On / Off
To turn the device on or off, **press and hold the Power button for approximately half a second**.
In **[Settings](#35-settings)** you can configure the power button to turn the device off with a short press instead of a long one.
In the **[Controls Settings](#363-controls)** you can configure the power button to turn the device off with a short press instead of a long one.
To reboot the device (for example if it's frozen, or after a firmware update), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
To reboot the device (for example after a firmware update or if it's frozen), press and release the Reset button, and then quickly press and hold the Power button for a few seconds.
### First Launch
@@ -62,29 +73,34 @@ Upon turning the device on for the first time, you will be placed on the **[Home
### 3.1 Home Screen
The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, **[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen.
The Home screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, the**[Browse Files](#33-browse-files-screen)** screen, the **[Recent Books](#34-recent-books-screen)** screen, the **[File Transfer](#35-file-transfer-screen)** screen, or **[Settings](#36-settings)**.
### 3.2 Book Selection
The Book Selection acts as a folder and file browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
### 3.3 Reading Mode
### 3.2 Reading Mode
See [Reading Mode](#4-reading-mode) below for more information.
### 3.4 File Upload Screen
### 3.3 Browse Files Screen
The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
The Browse Files screen acts as a file and folder browser.
* **Navigate List:** Use **Left** (or **Volume Up**), or **Right** (or **Volume Down**) to move the selection cursor up and down through folders and books. You can also long-press these buttons to scroll a full page up or down.
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
* **Delete Files:** Hold and release **Confirm** to delete the selected file. You will be given an option to either confirm or cancel deletion. Folder deletion is not supported.
### 3.4 Recent Books Screen
The Recent Books screen lists the most recently opened books in a chronological view, displaying title and author.
### 3.5 File Transfer Screen
The File Transfer screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server.
See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files.
> [!TIP]
> Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details.
### 3.4.1 Calibre Wireless Transfers
### 3.5.1 Calibre Wireless Transfers
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
@@ -96,23 +112,26 @@ CrossPoint supports sending books from Calibre using the CrossPoint Reader devic
3. Make sure your computer is on the same WiFi network.
4. In Calibre, click "Send to device" to transfer books.
### 3.5 Settings
### 3.6 Settings
The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust:
#### 3.6.1 Display
- **Sleep Screen**: Which sleep screen to display when the device sleeps:
- "Dark" (default) - The default dark Crosspoint logo sleep screen
- "Light" - The same default sleep screen, on a white background
- "Custom" - Custom images from the SD card; see [Sleep Screen](#36-sleep-screen) below for more information
- "Custom" - Custom images from the SD card; see [Sleep Screen](#37-sleep-screen) below for more information
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
- "None" - A blank screen
- "Cover + Custom" - The book cover image, fallbacks to "Custom" behavior
- "Cover + Custom" - The book cover image, falls back to "Custom" behavior
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
- "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected)
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected
- "Crop" - Scale the image down and crop as necessary to try to fill the screen (Note: this is experimental and may not work as expected)
- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected:
- "None" (default) - The cover image will be converted to a grayscale image and displayed as it is
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
- "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
- **Status Bar**: Configure the status bar displayed while reading:
- "None" - No status bar
- "No Progress" - Show status bar without reading progress
@@ -120,56 +139,199 @@ The Settings screen allows you to configure the device's behavior. There are a f
- "Full w/ Book Bar" - Show status bar with book progress (as bar)
- "Book Bar Only" - Show book progress (as bar)
- "Full w/ Chapter Bar" - Show status bar with chapter progress (as bar)
- **Hide Battery %**: Configure where to suppress the battery pecentage display in the status bar; the battery icon will still be shown:
- "Never" - Always show battery percentage (default)
- **Hide Battery %**: Configure where to suppress the battery percentage display in the status bar; the battery icon will still be shown:
- "Never" (default) - Always show battery percentage
- "In Reader" - Show battery percentage everywhere except in reading mode
- "Always" - Always hide battery percentage
- **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book. If disabled, paragraphs will not have vertical space between them, but will have first-line indentation.
- **Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
- **Short Power Button Click**: Controls the effect of a short click of the power button:
- "Ignore" - Require a long press to turn off the device
- "Sleep" - A short presspowers the device off
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting; options are every 1, 5, 10, 15, or 30 pages.
- **UI Theme**: Set which UI theme to use:
- "Classic" - The original Crosspoint theme
- "Lyra" - The new theme for Crosspoint featuring rounded elements and menu icons
- "Lyra Extended" - Lyra, but displays 3 books instead of 1 on the **[Home Screen](#31-home-screen)**
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight:
- "OFF" (default) - Disable the fix
- "ON" - Enable the fix
#### 3.6.2 Reader
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium" (default), "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal" (default), or "Wide".
- **Reader Screen Margin**: Controls the screen margins in Reading Mode between 5 and 40 pixels in 5-pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Embedded Style**: Whether to use the EPUB file's embedded HTML and CSS stylisation and formatting; options are "ON" or "OFF".
- **Hyphenation**: Whether to hyphenate text in Reading Mode; options are "ON" or "OFF".
- **Reading Orientation**: Set the screen orientation for reading EPUB files:
- "Portrait" (default) - Standard portrait orientation
- **Front Button Layout**: Configure the order of the bottom edge buttons:
-Back, Confirm, Left, Right (default)
-Left, Right, Back, Confirm
-Left, Back, Confirm, Right
- Back, Confirm, Right, Left
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- **Extra Paragraph Spacing**: Set how to handle paragraph breaks:
-"ON" - Vertical space will be added between paragraphs in Reading Mode
-"OFF" - Paragraphs will not have vertical space added, but will have first-line indentation
-**Text Anti-Aliasing**: Whether to show smooth grey edges (anti-aliasing) on text in reading mode. Note this slows down page turns slightly.
#### 3.6.3 Controls
- **Remap Front Buttons**: A menu for customising the function of each bottom edge button.
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from "Prev/Next" (default) to "Next/Prev". This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skips to the next/previous chapter:
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
-Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
-**Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
- "Open Dyslexic" - Font designed for readers with dyslexia
- **Reader Font Size**: Adjust the text size for reading; options are "Small", "Medium", "Large", or "X Large".
- **Reader Line Spacing**: Adjust the spacing between lines; options are "Tight", "Normal", or "Wide".
- **Reader Screen Margin**: Controls the screen margins in reader mode between 5 and 40 pixels in 5 pixel increments.
- **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right".
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep.
- **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting.
- **Sunlight Fading Fix**: Configure whether to enable a software-fix for the issue where white X4 models may fade when used in direct sunlight
- "OFF" (default) - Disable the fix
- "ON" - Enable the fix
-**Short Power Button Click**: Controls the effect of a short click of the power button:
-"Ignore" (default) - Require a long press to turn off the device
- "Sleep" - A short press puts the device into sleep mode
- "Page Turn" - A short press in reading mode turns to the next page; a long press turns the device off
#### 3.6.4 System
- **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep; options are 1, 5, 10 (default), 15 or 30 minutes.
- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication.
- **Check for updates**: Check for firmware updates over WiFi.
- **Clear Reading Cache**: Clear the internal SD card cache.
- **Check for updates**: Check for Crosspoint firmware updates over WiFi.
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
### 3.6 Sleep Screen
#### 3.6.5 KOReader Sync Quick Setup
You can customize the sleep screen by placing custom images in specific locations on the SD card:
CrossPoint can sync reading progress with KOReader-compatible sync servers.
It also interoperates with KOReader apps/devices when they use the same server and credentials.
- **Single Image:** Place a file named `sleep.bmp` in the root directory.
- **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps.
##### Option A: Free Public Server (`sync.koreader.rocks`)
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
2. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
- Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
##### Option B: Self-Hosted Server (Docker Compose)
1. Start a sync server:
```bash
mkdir -p kosync-quickstart
cd kosync-quickstart
cat > compose.yaml <<'YAML'
services:
kosync:
image: koreader/kosync:latest
ports:
- "7200:7200"
- "17200:17200"
volumes:
- ./data/redis:/var/lib/redis
environment:
- ENABLE_USER_REGISTRATION=true
restart: unless-stopped
YAML
# Docker
docker compose up -d
# Podman (alternative)
podman compose up -d
```
> [!NOTE]
> You'll need to set the **Sleep Screen** setting to **Custom** in order to use these images.
> `ENABLE_USER_REGISTRATION=true` is convenient for first setup. After creating your users, set it to `false` (or remove it) to avoid unexpected registrations.
If this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, the account already exists.
4. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `http://<server-ip>:17200`.
- Run **Authenticate**.
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**.
- Choose **Apply Remote** to jump to remote progress.
- Choose **Upload Local** to push current progress.
### 3.7 Sleep Screen
The **Sleep Screen** setting controls what is displayed when the device goes to sleep:
| Mode | Behavior |
|------|----------|
| **Dark** (default) | The CrossPoint logo on a dark background. |
| **Light** | The CrossPoint logo on a white background. |
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
| **Cover + Custom** | The cover of the currently open book. Falls back to **Custom** behavior if no book is open. |
| **None** | A blank screen. |
#### Cover settings
When using **Cover** or **Cover + Custom**, two additional settings apply:
- **Sleep Screen Cover Mode**: **Fit** (scale to fit, white borders) or **Crop** (scale and crop to fill the screen).
- **Sleep Screen Cover Filter**: **None** (grayscale), **Contrast** (black & white), or **Inverted** (inverted black & white).
#### Custom images
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
> [!TIP]
> For best results:
@@ -188,7 +350,7 @@ Once you have opened a book, the button layout changes to facilitate reading.
The role of the volume (side) buttons can be swapped in **[Settings](#35-settings)**.
The role of the volume (side) buttons can be swapped in the **[Controls Settings](#363-controls)**.
If the **Short Power Button Click** setting is set to "Page Turn", you can also turn to the next page by briefly pressing the Power button.
@@ -196,13 +358,13 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
This feature can be disabled in the **[Controls Settings](#363-controls)** to help avoid changing chapters by mistake.
### System Navigation
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**.
* **Return to Home:** Press the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.
* **Return to Browse Files:** Press and hold the **Back** button to close the book and return to the **[Browse Files](#33-browse-files-screen)** screen.
* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)** screen.
# 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).
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:
# 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.
-`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.
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.
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".
### 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
- 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
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.
**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.
- 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)
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
-`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
# 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
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
- 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
- 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
# 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.
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.
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.
- 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`
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.
# 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.
**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()`
- **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)
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".
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`.
# 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.
# 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/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`
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.
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`
- 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
- **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
# 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
# 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
- 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
- **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
# 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)
- 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
# 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()`.
- 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.
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.
# 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.
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.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.
- 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)
- **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
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
- 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.
- 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`
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
| #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
- **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
# 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.
- **`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.
- **`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.
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`.
- 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.
- **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`).
- 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.
- 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)
# 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`.
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.
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 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).
- **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).
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
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.
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.
- **`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.
-`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&`
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/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.
- **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.
# 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:
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)
**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)
# 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`
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).
-`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.
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.
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`
- **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.
- **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.
# 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.
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 `--`.
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)
**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.
- **`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.
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:
- 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)
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
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
-`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/browser/OpdsBookBrowserActivity.h` / `.cpp` — Constructor accepts `OpdsServer`, uses server-specific credentials and URL, shows server name in header
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).
-`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)
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.
- **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`
# Fresh Replay: Sync mod/master with upstream/master (continued)
## Task
Continue the "Fresh Replay" synchronization of mod/master with upstream/master (HEAD: 170cc25). This session picked up from Phase 2c (GfxRenderer/theme modifications) and completed through Phase 4 (verification).
Re-add the `PUSH_ONLY` sync mode to `KOReaderSyncActivity` that was lost during the upstream resync/ActivityManager migration (originally from PR #1090). This mode allows the reader to silently push local progress to the KOReader sync server and then enter deep sleep — no interactive UI.
- Added `PUSH_AND_SLEEP` case in `onReaderMenuConfirm`: launches `KOReaderSyncActivity` in `PUSH_ONLY` mode, then calls `activityManager.requestSleep()` on completion (regardless of success/failure)
Comprehensive audit of `mod/master-resync` vs `mod/backup-pre-sync-2026-03-07` identified 4 mod features lost during the upstream resync. All 4 have been re-implemented.
## Changes
### 1. EndOfBookMenuActivity wired into EpubReaderActivity (HIGH)
Fix compilation errors in the `mod` PlatformIO build environment after the upstream resync. The `default` environment was also verified.
## Changes Made
### Include path fixes (11 files)
-`src/activities/{reader,settings,util}/*.cpp`: Changed bare `#include "ActivityResult.h"` to `#include "activities/ActivityResult.h"` (10 files)
-`src/activities/reader/DictionarySuggestionsActivity.cpp`: Changed `#include "RenderLock.h"` to `#include "activities/RenderLock.h"`
### API compatibility fixes
-`src/util/Dictionary.h`: Replaced invalid `class FsFile;` forward declaration with `#include <HalStorage.h>` (`FsFile` is now a `using` alias)
-`lib/Epub/Epub/blocks/TextBlock.h`: Added `getWordXpos()` public accessor
-`lib/GfxRenderer/GfxRenderer.{h,cpp}`: Re-added `drawTextRotated90CCW()` with `Rotated90CCW` enum value and coordinate mapping, adapted to new fixed-point rendering
Implemented the full plan to restore 8 mod-specific features lost during the upstream sync, organized into 5 phased commits.
## Changes Made
### Phase 0 — Commit Pending Changes (`4627ec9`)
- Staged and committed 22 modified files from previous sessions (ADC fix, logging guard, include path fixes, etc.) as a clean baseline.
### Phase 1 — UI Rendering Fixes (`f44657a`)
- **Clock display**: Added clock rendering to `BaseTheme::drawHeader()` and `LyraTheme::drawHeader()` supporting OFF/AM-PM/24H formats and Small/Medium/Large sizes.
- **Placeholder covers**: Fixed `splitWords()` in `PlaceholderCoverGenerator.cpp` to treat `\n`, `\r`, `\t` as whitespace (not just spaces). Removed `drawBorder()` call since the UI already draws frames around book cards.
### Phase 2 — Reader Menu Restructure (`0493f30`)
- **Manage Book submenu**: Replaced 4 individual top-level menu items (Archive, Delete, Reindex, Delete Cache) with single "Manage Book" entry that launches `BookManageMenuActivity`.
- **Dictionary submenu**: Created new `DictionaryMenuActivity` and replaced 3 scattered dictionary items with single "Dictionary" entry.
- **Long-press TOC**: Added 700ms long-press detection on Confirm button to open Table of Contents directly, bypassing the menu.
- Added `STR_DICTIONARY` i18n key and regenerated I18nKeys.h/I18nStrings.h.
### Phase 3 — Settings and Indexing (`22c1892`)
- **Letterbox Fill position**: Moved from bottom of settings list to immediately after Sleep Screen Cover Filter.
- **Indexing Display setting**: Added to Display section with Popup/Status Bar Text/Status Bar Icon modes.
- **Silent indexing**: Restored proactive next-chapter indexing logic and status bar indicator (text or hourglass icon) in `EpubReaderActivity`.
### Phase 4 — Book Preparation (`a5ca15d`)
- **Prerender on first open**: Restored the cover/thumbnail prerender block in `EpubReaderActivity::onEnter()` with "Preparing book..." popup and progress bar.
- Added `isValidThumbnailBmp()`, `generateInvalidFormatCoverBmp()`, and `generateInvalidFormatThumbBmp()` methods to `Epub` class.
- Added `#include <EpdFontData.h>` for `fp4::toPixel()`
- Fixed `renderGlyph()`: `glyph->advanceX` is 12.4 fixed-point, not pixels — was advancing cursor ~16x too far, causing only the first character of each word to be visible
- Fixed `getCharAdvance()`: same fixed-point conversion needed for space width calculation in word wrapping
### src/activities/reader/EpubReaderActivity.cpp
- Removed `silentIndexNextChapterIfNeeded()` call from `loop()` (line 385) — this blocked the UI before render, preventing the indicator from ever showing. The backup branch only called it from `render()`, after the page was drawn.
- Fixed indexing icon Y position: changed `textY - kIndexingIconSize + 2` to `textY + 2` to align within the status bar alongside battery/text
- Passed `consumeFirstRelease` flag when creating chapter selection activity from long-press path
- **Root cause**: `ignoreNextConfirmRelease` was set to `true` on long-press but never cleared after being transferred to the child `EpubReaderChapterSelectionActivity`. The parent's flag remained `true`, causing the next Confirm press to be silently consumed.
- **Fix**: Added `ignoreNextConfirmRelease = false;` immediately after capturing the value into `consumeRelease`.
### Bug 2: Footnotes inaccessible from reader menu
- **Root cause**: `buildMenuItems()` accepted a `hasFootnotes` parameter but never used it to add the `MenuAction::FOOTNOTES` entry.
- **Fix**: Added conditional `FOOTNOTES` menu item after DICTIONARY when `hasFootnotes` is true.
### Bug 3: Phantom screen re-render at idle
- **File**: `src/main.cpp`
- **Root cause**: The clock update logic in `loop()` called `activityManager.requestUpdate()` every minute when the displayed minute changed. Reader activities don't show the clock, but still received full page re-renders, causing visible e-ink flashes while idle.
- **Fix**: Guarded the clock block with `!activityManager.isReaderActivity()`.
## Follow-up
- Hardware testing needed for all three fixes
- Bug 3 fix means the clock won't update while reading; it resumes updating when returning to non-reader activities
# Restore Preferred Orientation Settings and Long-Press Sub-Menu
**Date**: 2026-03-08
**Branch**: mod/master
**Commit**: 0d8a3fd
## Task
Restore two orientation preference features lost during the upstream PR resync:
1. Settings UI entries for preferred portrait/landscape modes
2. Long-press sub-menu on the reader menu's orientation toggle
## Changes
### Settings UI and Persistence
- **`src/SettingsList.h`**: Added `DynamicEnum` entries for `preferredPortrait` (Portrait/Inverted) and `preferredLandscape` (Landscape CW/Landscape CCW) in the Reader settings category. Uses getter/setter lambdas to map between sequential indices and non-sequential orientation enum values.
- **`src/JsonSettingsIO.cpp`**: Added manual JSON save/load for both fields with validation (rejects invalid orientation values, falls back to defaults).
### Long-Press Orientation Sub-Menu
- **`src/activities/reader/EpubReaderMenuActivity.h`**: Added `orientationSubMenuOpen`, `orientationSubMenuIndex`, and `ignoreNextConfirmRelease` state flags.
-`loop()`: Long-press (700ms) on Confirm when the orientation item is selected opens the sub-menu. Sub-menu handles its own Up/Down/Confirm/Back input. Added `ignoreNextConfirmRelease` guard to prevent the long-press release from immediately selecting.
-`render()`: When sub-menu is open, renders a centered list of all 4 orientations with the current one marked with `*`. Uses the same gutter/hint layout as the main menu.
## Follow-up
- Hardware testing needed for both features
- Translations for `STR_PREFERRED_PORTRAIT` and `STR_PREFERRED_LANDSCAPE` only exist in English; other languages fall back automatically
**Task:** Port two upstream PRs and verify alignment of a previously-ported PR that was recently merged.
## Changes Made
### PR #1311 -- Fix inter-word spacing rounding error (UNMERGED, ported as mod feature)
Replaced `getSpaceKernAdjust()` with `getSpaceAdvance()` which combines space glyph advance and flanking kern values into a single fixed-point sum before pixel snapping, fixing +/-1 px rounding drift in inter-word spacing.
# Parse and Display All Available EPUB Metadata Fields + Cleanup Refactor
## Task
1. Extend the OPF parser, metadata cache, Epub accessors, i18n, and BookInfo screen to parse and display all standard Dublin Core metadata fields plus Calibre rating.
2. Refactor for code quality: consolidate duplicate static blank strings, add `getMetadata()` accessor, and simplify `buildLayout` to accept `BookMetadata` struct.
# BookInfoActivity: Performance Fix and Cover Image
## Task
Fix BookInfoActivity sluggishness (slow open, unresponsive scrolling) and add book cover display.
## Root Cause (from device debug log)
1.**No Y-culling in render()**: All text lines drawn even when off-screen. Content extending ~1162px on 800px screen caused hundreds of thousands of `LOG_ERR("Outside range")` calls per frame, each doing serial I/O. Render times: 7-13 seconds per frame.
2.**Description text contained literal newlines**: `stripHtml()` and `trim()` in ContentOpfParser don't replace interior `\n` characters. These got passed to `drawText()`, triggering "No glyph for codepoint 10" errors.
3.**`wrappedText()` recomputed every frame**: Original render called it for every field on every scroll -- now pre-computed once.
4.**No cover image**: Activity never loaded or displayed any cover.
## Changes Made
### Committed first: PR #1342 port (commit 4cf395a)
- Staged and committed all prior working state before making further changes
### BookInfoActivity refactor (2 files)
**`src/activities/home/BookInfoActivity.h`**:
- Replaced individual metadata string members with `InfoField` struct + `std::vector<InfoField> fields`
- Added `coverBmpPath`, `coverDisplayHeight`, `coverDisplayWidth` members
- Added `buildLayout()` method for pre-computation
**`src/activities/home/BookInfoActivity.cpp`**:
- **Y-culling**: `render()` skips draw calls for items entirely above or below the visible screen area (`y + height > 0 && y < pageH`); breaks out of field loop when `y >= pageH`
- **Newline normalization**: Added `normalizeWhitespace()` helper that collapses `\n`, `\r`, `\t` sequences into single spaces; applied to description text before word-wrapping
- **Cover height clamping**: `drawBitmap1Bit` maxHeight capped to `std::min(coverDisplayHeight, pageH - y)` to prevent drawing beyond screen
- **Pre-computed layout**: All `wrappedText()` calls moved to `onEnter()` via `buildLayout()`; `render()` only iterates pre-computed lines
- Cover thumbnail generated via `epub.generateThumbBmp()` / `xtc.generateThumbBmp()`; fallback to `PlaceholderCoverGenerator`
- Cover rendered centered at top using `renderer.drawBitmap1Bit()`
**Task**: Implement fixes 1-5 from the BookInfo Buttons and Load plan
## Changes Made
### BookInfoActivity.cpp — Fixes 1-3
1.**Button mapping** (`loop()`): Removed `Confirm` as exit trigger. Added `Left` and `Right` front buttons as scroll-down/scroll-up handlers alongside existing side-button `Down`/`Up` and `PageForward`/`PageBack`.
2.**Button hints** (`render()`): Updated `mapLabels` to show `STR_DIR_DOWN` on Left (btn3) and `STR_DIR_UP` on Right (btn4), with separate variables for each direction instead of a single combined hint.
3.**Fallback load** (`onEnter()`): Changed `epub.load(false, true)` pattern to try `epub.load(true, true)` on failure, ensuring cache is built for books that were never opened. XTC branch already builds unconditionally via `xtc.load()`.
### BookManageMenuActivity — Fix 4
- Added `BOOK_INFO` to `Action` enum (first entry)
- Added `STR_BOOK_INFO` menu item as first entry in `buildMenuItems()`
- Handled `BOOK_INFO` in all 4 result sites:
-`HomeActivity::openManageMenu()` — launches `BookInfoActivity` via `startActivityForResult`
-`RecentBooksActivity::openManageMenu()` — same pattern
-`FileBrowserActivity::handleManageResult()` — same pattern
-`EpubReaderActivity` inline switch — same pattern
- Added `BOOK_INFO` no-op case to `executeManageAction()` in Home/Recent to prevent compiler warnings
### HomeActivity cover regeneration — Fix 5
- After `generateThumbBmp()` fails or produces invalid BMP, now calls `generateCoverBmp(false)` to extract the full cover from the EPUB before retrying thumbnail generation
- Added `Epub::isValidThumbnailBmp()` validation after each `generateThumbBmp()` call
- Applied same pattern to XTC branch using `xtc.generateCoverBmp()` + validation
- This aligns the HomeActivity pipeline with the EpubReaderActivity's multi-tier fallback approach
**Task**: Show a progress popup when BookInfo needs to parse an unopened book
## Changes Made
### BookInfoActivity.cpp
Added a "Loading..." progress popup with progress bar to `onEnter()` when the book cache doesn't exist and a full parse is required:
- **EPUB branch**: `epub.load(false, true)` is tried first (fast, cache-only). If it fails, `GUI.drawPopup(tr(STR_LOADING))` is shown at 10%, `epub.load(true, true)` runs (slow full parse), progress updates to 50%, thumbnail generation runs, then progress hits 100%.
- **XTC branch**: Checks `Storage.exists(xtc.getCachePath())` before `xtc.load()`. If cache directory is missing, shows the same popup pattern around load + thumbnail generation.
- For books with existing cache, no popup is shown — the fast path completes silently.
**Task**: Fix BookInfo header not properly overlaying scrolled content (cover image and text fields bleeding into the header bar area).
## Problem
The BookInfo screen's header (clock, battery, "Book Info" title) was being drawn *before* content, so the book cover thumbnail and text fields could render on top of or overlap with the header when scrolling. Additionally, the initial attempt to clip the cover by reducing `maxHeight` in `drawBitmap1Bit` caused the cover to *shrink* rather than scroll behind the header, because `maxHeight` controls bitmap scaling, not clipping.
## Changes Made
### `src/activities/home/BookInfoActivity.cpp`
- **Moved header rendering after content**: `GUI.drawHeader()` is now called *after* all content (cover + fields) is drawn, not before. A `renderer.fillRect(0, 0, pageW, contentTop, false)` wipe of the header zone precedes the header draw, ensuring any content that scrolled into the header region is cleared before the header paints on top.
- **Removed incorrect cover clipping**: The cover bitmap is now drawn at its natural scrolled `y` position with full `coverDisplayWidth` and `coverDisplayHeight` (no clamping of `maxHeight`). The header fill+draw handles visual clipping. This gives the correct "scroll behind the header" behavior instead of the cover appearing to shrink.
## Follow-up
None -- visual behavior confirmed acceptable by user.
**Task**: Implement a side-by-side layout for BookInfo in landscape orientation -- cover fixed on the left, scrollable metadata fields on the right. Portrait layout unchanged.
## Changes Made
### `src/activities/home/BookInfoActivity.h`
- Added `bool isLandscape` and `int coverPanelWidth` member variables.
### `src/activities/home/BookInfoActivity.cpp`
- **`onEnter()`**: Detects orientation via `renderer.getOrientation()`. In landscape, the cover thumbnail height fills the full content area (between header and button hints) instead of 2/5 of screen height.
- **`buildLayout()`**: Reads cover bitmap dimensions first. In landscape with a cover, computes `coverPanelWidth` (cover width + padding, capped at 40% of screen). Text fields are wrapped to the narrower right-panel width. Cover height is excluded from `contentHeight` (it sits beside, not above, the fields).
- **`render()`**: In landscape, draws the cover at a fixed position in the left panel (unaffected by scroll). Fields render in the right panel at `x = coverPanelWidth`. In portrait, existing behavior is preserved (cover above fields, both scroll together). The header fill + draw-on-top pattern continues to prevent content bleeding into the header zone.
## Behavior Summary
- **Portrait** (480x800): Cover on top, fields below, everything scrolls vertically (no change).
- **Landscape** (800x480): Cover pinned on left (centered vertically, fills content height), metadata fields scroll independently on the right.
- If no cover exists in landscape, the full screen width is used for fields (same as portrait but in landscape dimensions).
## Follow-up
None -- ready for hardware testing in all 4 orientations.
**Task**: Adjust BookInfo's content area and header placement in landscape orientations to account for button hints appearing on a side instead of the bottom.
## Problem
In landscape orientations the physical front buttons end up on a side of the screen (CW = left, CCW = right). BookInfo was reserving `buttonHintsHeight` at the bottom in all orientations, wasting vertical space in landscape and not accounting for the side gutter needed to avoid overlapping the side-drawn button hints.
## Changes Made
### `src/activities/home/BookInfoActivity.cpp`
- **`buildLayout()`**: Added `hintGutterWidth` computation (`metrics.sideButtonHintsWidth` in landscape, 0 in portrait). Text wrapping width (`contentW`) now subtracts `hintGutterWidth`. Bottom padding for `contentHeight` uses only `verticalSpacing` in landscape (no `buttonHintsHeight` since there are no bottom hints).
- **`render()`**: Added `isLandscapeCw`, `hintGutterWidth`, and `contentX` computations following the established codebase pattern. In CW, `contentX = hintGutterWidth` shifts all content right (hints on left). In CCW, `contentX = 0` and content width is reduced (hints on right). Cover X position offset by `contentX`. Field X offset by `contentX`. `contentBottom` no longer subtracts `buttonHintsHeight` in landscape. Header `fillRect` and `drawHeader` Rect both adjusted by `contentX` and `hintGutterWidth`.
## Pattern Followed
The established pattern from `LookedUpWordsActivity`, `DictionarySuggestionsActivity`, `EpubReaderMenuActivity`, and others:
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.