## Summary
This PR enhances the EPUB reader's bookmark system with two
complementary improvements: a per-page bookmark indicator icon and
toggle behavior on the existing long-press action.
---
### What Changed
**Bookmark Toggle (was: add-only)**
The long-press Confirm action now toggles bookmarks rather than always
adding. `addBookmark()` checks whether a bookmark with the same xpath
already exists in the in-memory cache:
- If found → removes it and shows "Bookmark removed."
- If not found → adds it and shows "Bookmark added."
A new `STR_BOOKMARK_REMOVED` translation string was added to support the
removal message.
**Bookmark Icon Indicator**
A `BookmarkIcon` is now drawn at the top-right corner of the page
whenever the current page has a bookmark. `updateBookmarkFlag()` is
called at render time to determine whether the current page is
bookmarked.
**In-Memory Bookmark Cache**
Bookmarks are now loaded into `cachedBookmarks` on `onEnter()` rather
than being re-read from disk on every toggle. All subsequent add/remove
operations work against this cache and flush to disk, avoiding redundant
file reads on each bookmark action.
**Faster bookmarks list**
Previously, calculating "page X/Y" for each entry required decompressing
the entire spine item. We now persist `si`/`pc`/`pp` (spine index, page
count, and page progress) in the bookmark JSON when saving, and restore
them when loading. This avoids the expensive `toCrossPoint()` loop in
`onEnter()`, significantly reducing the cost of initializing the
bookmarks list.
---
### Files Changed
- `EpubReaderActivity.cpp` — `addBookmark()` toggle logic,
`updateBookmarkFlag()` (new), icon rendering in `renderContents()`,
cache initialization in `onEnter()`
- `EpubReaderActivity.h` — new fields: `currentPageBookmarked`,
`bookmarkRemoved`, `cachedBookmarks`; new method declaration
`updateBookmarkFlag()`
---
### 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: Julia Nguyen <julia@uxj.io>
## Summary
* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
Fixes footnote links landing in start section instead of the specific
foot note target.
* **What changes are included?**
ChapterHtmlSlimParser.cpp: call flushPendingAnchor() before overwriting
pendingAnchorId. First id got lost due to consecutive non-block elements
carry ids, the first id was lost and the reader had no page to jump to,
so it defaulted to page 0.
## Additional Context
Tested with the epubs attached to to #2336. Need to clear .crosspoint/
cache after flashing so the anchor map gets rebuilt with the fix.
* 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 assisted with writing documentation.
## Summary
* **What is the goal of this PR?**
* Make manual clock sync work even when the device is not already
connected to Wi-Fi, so the user can start the sync flow directly from
settings instead of being blocked by connection state.
* **What changes are included?**
* `ClockSyncActivity` now launches the normal Wi-Fi selection flow
before syncing when the device is offline.
* After Wi-Fi selection succeeds, clock sync resumes automatically and
performs the existing forced NTP sync.
* If clock sync had to bring Wi-Fi up, the activity disconnects and uses
the existing silent restart cleanup path to avoid leaving the device in
a fragmented post-Wi-Fi heap state.
* The completion UI now only advertises Back, matching the updated input
handling.
## Additional Context
* Existing behavior is unchanged when Wi-Fi is already connected: the
activity syncs immediately.
* Cancelling Wi-Fi selection exits the clock sync flow.
---
### 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
* Adds a live font preview pane to the font selection screen so users
can see how a font looks before committing to it.
* Changes
* A preview pane occupying the top 30% of the font selection screen,
rendering sample pangram text in the previewed font
* A two-step confirm flow: first press (enter button) previews the font,
second press selects it
* Back restores the original font settings, so browsing has no side
effects
* Layout dimensions cached in `onEnter()` to avoid redundant
recalculation between `loop()` and `render()`
## Additional Context
* Preview sample text is hardcoded English; didn't want to use AI for
translation as I would not be able to verify the output in most
languages...
* The preview pane reduces visible list height; this is compensated by
passing the reserved height into `getNumberOfItemsPerPage`
---
### AI Usage
Did you use AI tools to help write this code? **PARTIALLY**
AI use for assisting in coding and in writing the PR description.
## Summary
* **What is the goal of this PR?**
Fixes#2275. A book could get stuck reopening on an old page, with
progress no
longer saving and neither "Delete Book Cache" nor "Clear Reading Cache"
able to
fix it. Root cause: `progress.bin` was written truncate-in-place, so an
interrupted write (power loss, or a crash mid-SPI during sleep) left it
with a
broken FAT cluster chain that the firmware could neither rewrite nor
delete —
recovery required `fsck`/manual deletion on a host PC.
Confirmed in the SDK: `SDCardManager::openFileForWrite` opens with
`O_RDWR | O_CREAT | O_TRUNC`, so the canonical file is zeroed before the
few
progress bytes are rewritten — exactly the window that corrupts the FAT
chain.
* **What changes are included?**
* New shared helper `ProgressFile::writeAtomic()`
(`src/activities/reader/ProgressFile.h`): writes progress to
`progress.bin.tmp`, flushes and closes it, then `remove`s the old
`progress.bin` and `rename`s the temp into place. An interrupted write
now
only ever damages the throwaway temp; the canonical file is never torn.
* All three readers route their progress saves through the helper:
EPUB (`EpubReaderUtils.h`), `TxtReaderActivity`, `XtcReaderActivity` —
they
all shared the identical vulnerable pattern.
* Minor: `EpubReaderUtils::saveProgress` now takes `const Epub&` (clears
a
cppcheck `constParameterReference` finding).
## Additional Context
* **Crash-safe, not metadata-atomic.** On FAT the replace is `remove` +
`rename`
(two directory ops; SdFat's `rename` won't overwrite, hence
remove-first). A
crash between them leaves *neither* file, which reads as "no saved
progress" on
next launch — a harmless reset to an old page, never a
corrupt/unclearable file.
The guarantee is that `progress.bin` is never half-written.
* **Prevents, does not repair.** This stops new corruption on healthy
cards. It
cannot fix an already-corrupted `progress.bin` (removing it may itself
fail at
the FAT level) — those still need `fsck`/manual deletion, as in the
issue's
workaround.
* **Known follow-up (out of scope here):** a crash *while writing the
temp* can
leave an orphan `progress.bin.tmp`. It's harmless and self-healing (the
next
save overwrites it, and it never blocks reading progress), but a
boot-time
orphan-`.tmp` cleanup would be a tidy follow-up.
* **Focus areas for review:** the close-before-rename ordering in
`ProgressFile.h` and the remove-before-rename rationale.
## Verification
* `./bin/clang-format-fix` — clean
* `pio check --fail-on-defect low --fail-on-defect medium
--fail-on-defect high` — no defects
* `pio run` — SUCCESS (RAM 30.9%, Flash 78.8%; footprint essentially
unchanged)
* Tested on a **Xteink X4** device: open book, turn pages, sleep/exit,
reopen —
progress now restores to the navigated page across all three readers
(EPUB / TXT / XTC).
---
### AI Usage
Did you use AI tools to help write this code? _**YES**_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Summary
- Hold the menu (Confirm) button for ≥1s while reading an EPUB to launch
the existing `KOReaderSyncActivity` directly — replaces the three-step
path (open reader menu → scroll to Sync → confirm) with a single
gesture.
- Reuses `ReaderUtils::GO_HOME_MS` (same 1s threshold used by long-press
Back) and the existing `KOREADER_STORE.hasCredentials()` guard.
- Adds a Controls picker **"Long-press Menu"** (`longPressMenuFunction`,
default **Bookmark**) that **cycles through the available functions**
bound to the long-press gesture: `KOSync → Disabled → Bookmark`. The
field name and `LONG_PRESS_MENU_FUNCTION` enum are intentionally general
so future actions (dictionary lookup, table of contents, etc.) can be
appended without another schema migration. The setting is **not** a
binary toggle.
- Existing menu Sync entry still works — both call sites share one
extracted helper (`launchKOReaderSync`); no logic duplication.
- Short-press Confirm release is gated on duration so the reader menu
does not also open after a long press that *acts*, mirroring the
existing long-press Back pattern.
- **No-credentials fall-through:** `launchKOReaderSync()` now returns
whether it acted. When the function is set to KOSync but no KOReader
credentials are stored, the long-press is a no-op that **falls through
to open the reader menu** — so the menu stays reachable instead of the
hold silently swallowing the gesture. The release is only suppressed
when sync actually launched or surfaced a save error.
## Test plan
- [x] `pio run` succeeds clean for the `default` ESP32-C3 environment.
- [x] On-device, value **KOSync**, valid KOReader credentials:
long-press Confirm ≥1s → sync screen launches; get + update progress
return HTTP 200; release does **not** also open the reader menu; returns
to the same page.
- [x] On-device, value **KOSync**, **no** credentials: long-press
Confirm falls through and **opens the reader menu** (regression fix);
short-press also opens the menu.
- [x] On-device: menu → Sync still launches the same screen (shared
helper) and syncs (200/200).
- [x] On-device, value **Bookmark**: long-press drops a bookmark and
does **not** also open the menu.
- [x] On-device, value **Disabled**: long-press Confirm opens the menu
on release; no sync, no bookmark.
- [x] On-device: long-press Back still goes to the file browser
(unchanged path).
- [x] Heap: epub is released before the TLS handshake (frees ~16 KB);
min free heap stayed ~84 KB during sync, well above the safe floor. No
panics/OOM across the session.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This PR adds the [Libre Baskerville
](https://github.com/impallari/Libre-Baskerville) font family to the
fonts available for download in CrossPoint.
## Additional Context
Baskerville is a classic font family, widely available in some form or
the other in most ereaders and widely used in publishing and in the
academy.
Libre Baskerville is a libre (SIL Open Font License v1) implementation
of it, which makes it possible for CrossPoint to ship it without issues.

---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
Non-functional cleanup: `ChapterHtmlSlimParser::` prefix is redundant
when calling `applyDirectionToEntry` within `ChapterHtmlSlimParser`
methods.
---
### AI Usage
Did you use AI tools to help write this code? _**NO**_
## Summary
* **What is the goal of this PR?** Prevent heap memory exhaustion caused
by thousands of machine-generated anchor IDs injected by epub converters
like Kobo KePub.
* Fix#2292.
* **What changes are included?**
* **Element-type filter (Layer 1):** Introduced the
`isNonNavigableInlineElement()` function in `ChapterHtmlSlimParser.cpp`
to automatically skip recording IDs on `<span>` elements, as they are
purely inline wrappers used for tracking and lack navigable meaning.
* **Hard cap (Layer 2):** Added the `MAX_ANCHORS_PER_CHAPTER = 1024`
constant to act as a fallback safety net against unbounded heap growth
from unknown future ID-injection patterns on non-span elements.
* **TOC Safety net:** Ensured that IDs matching known Table of Contents
(TOC) entries explicitly bypass both the `<span>` filter and the
1024-anchor cap, guaranteeing that chapter page-break and core
navigation logic are never compromised.
---
### 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?** Update project documentation to match
the current master implementation for cache formats, i18n, file
transfer/web server workflows, SD-card fonts, and root user-facing docs.
* **What changes are included?** Refreshes `book.bin`/`section.bin` docs
for v6/v25, updates File Transfer/Calibre/WebDAV/API docs, documents 24
UI languages and JSON language persistence, updates root
README/USER_GUIDE cache and network details, and syncs the tracked
CLAUDE skill doc cache-version notes.
## Additional Context
* Docs-only change. Verified with `git diff --check origin/master..HEAD`
and stale-reference greps for old cache versions, removed i18n APIs, old
WiFi screen wording, and raw `Serial.printf` examples. No firmware build
was run.
---
### 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
`std::map` heap-allocates each node. Avoid using it for wifi SSID
dedupe, and instead just dedupe in-place with the existing `networks`
vector. Removed dead `ipAddress` member in `WifiNetworkInfo` struct.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
## Summary
* **What is the goal of this PR?**
* Fix EPUBs where internal file references are written in URL-style
escaped form, like spaces appearing as `%20`, so the reader can find the
right files instead of treating those references as missing.
* **What changes are included?**
* Added a small shared helper that converts those escaped EPUB-internal
paths back into their normal filenames before we try to look them up.
* Applied that cleanup step across the EPUB parsing flow wherever we
resolve internal references, including cover images, manifest items, TOC
links, spine entries, and inline HTML images.
* Kept the change narrowly focused on EPUB-internal asset resolution
rather than changing broader URL or networking behavior.
## Additional Context
* The user-facing bug here is that some books package their internal
filenames in an escaped form, so a file like `Chapter 1.xhtml` may be
referenced more like `Chapter%201.xhtml`. The reader was treating that
escaped text as the literal filename, which means otherwise-valid books
could lose images, covers, or chapter targets because the lookup no
longer matched the real file inside the EPUB.
* Risk is intentionally low. The helper only rewrites valid `%XX` escape
sequences and leaves malformed input alone, so it should improve
compatibility with escaped filenames without broadening the parser’s
behavior in unrelated cases.
## Local Testing Performed
* This was tested on my device with the user-provided optimized epub
that was not rendering images within the text prior to this fix (cover
image and chapter headers were rendering fine):
[orv_main_baseline.epub.zip](https://github.com/user-attachments/files/28529545/orv_main_baseline.epub.zip)
* This was also tested by the user with a local build and the affected
epub and confirmed to be working
## Steps for Testing
* Try to open the affected epub (linked above) or any epub that has
similar percent-encoding on a build prior to this fix.
* Apply this fix, clear book cache, and re-open the affected book.
* Images should render properly.
---
### 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
Use `std::string_view` and case-insensitive comparisons to avoid string
allocations during CSS parsing.
**Hot path:** `resolveStyle` (called per HTML start tag during chapter
rendering) now does zero heap allocations. Previously it allocated a
normalized tag string, a vector of class strings, and a composite key
per class. For a chapter with ~2000 tags × 2 classes each, that's ~12
000 small short-lived allocations eliminated per page render — primarily
a heap-fragmentation win on the ESP32-C3's ~380KB RAM.
**Cold path:** CSS load no longer allocates per-rule selector vectors or
per-token strings; `splitOnChar`/`splitWhitespace` are gone, replaced
with callback-based tokenization (`forEachDelimitedToken`).
**Behavioral notes:**
- The selector `unordered_map` now uses an ASCII-case-insensitive
hash/equal. Selectors are stored with their original case rather than
pre-lowercased; the observable lookup result is unchanged.
- `stripTrailingImportant` is now case-insensitive (per CSS spec;
previously matched only lowercase `!important`).
**Cache compatibility:** `CSS_CACHE_VERSION` unchanged. Old caches
(lowercase selectors) load correctly under the new lookup; new caches
will contain verbatim-case selectors — both forms work.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_