- 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: 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?**
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?** 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
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
* 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
* 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 >**_
## 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
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?** (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
**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
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?** 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.)
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
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?**
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
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
**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
- **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?** (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
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
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 **_
## 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
* 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?**
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?** 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 >**_
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?**
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 -->
## 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
- 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>