Commit Graph
246 Commits
Author SHA1 Message Date
eunchurn 5717374e4b feat(update): SD-card firmware update + X3 bootloader compatibility (#1786) 2026-05-04 19:51:20 -04:00
Zach Nelson 6c4ae7c41a refactor: Avoid vector for page turn rates list (#1818)
## Summary

Small cleanup to avoid a dynamically allocated static structure for auto
page-turn rate values.

---

### 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**_
2026-05-04 08:45:44 -05:00
MaciekandЕгор Мартынов 2e2ea6a9e8 feat: Page turn button orientation change (#1069)
Co-authored-by: Егор Мартынов <martynovegorOF@yandex.ru>
2026-05-04 08:56:02 +03:00
Julia b692bad10d fix: support legacy XTC file headers where pageTableOffset=48 (#1816)
## Summary

**What is the goal of this PR?**

Restore compatibility with older XTC files that use the legacy 48-byte
header layout, so they open correctly instead of failing
header/page-table validation.

**What changes are included?**

- Updates the XTC parser to accept legacy files where pageTableOffset
starts at 0x30 instead of the newer full header size.
- Avoids treating legacy header bytes as a valid chapterOffset, so older
files are not misclassified as having chapters.
- Switches XTC file size and seek operations to 64-bit-safe wrapper
calls in HalStorage, which matches the XTC format’s 64-bit offset fields
and keeps page/chapter lookups from narrowing offsets.
- Keeps the existing bounds checks and improves related logging when XTC
page loads fail.

## Additional Context

- This is a narrow compatibility fix for XTC handling. The functional
change is in the XTC parser; the HalStorage changes are supporting
wrapper methods needed by that parser update.
- Risk should be low outside XTC reading, since the HAL changes only
expose existing underlying file APIs rather than changing storage
behavior.

---

### 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**
2026-05-03 20:48:35 -05:00
Justin MitchellandJustin Mitchell ba4a361d64 fix: OTA update on x3 and progress bar on x4 and x3 (#1805)
Co-authored-by: Justin Mitchell <1875695+itsthisjustin@users.noreply.github.com>
2026-05-01 19:15:15 -04:00
Zach Nelson b25389b43e refactor: Move language setting into JSON settings (#1796)
## Summary

Another follow up to #1408. That PR revised the language.bin settings
file to version 2. After merging, it occurred to me that it would make
more sense to have the language inside the overall settings.json file,
easy for users to see and change directly.

This change adds migration from language.bin to settings.json. It only
migrates from the pre-#1408 v1 language.bin format, because v2 has only
been in for a couple of commits and I don't think we need long-term
maintenance code to handle it.

#1408 had a potential long-term maintenance issue for migration. It
assumed that the enum order of languages never changes, only grows. So,
e.g. "Portuguese (Portugal)" could never be added next to "Portuguese
(Brazil)", only appended. This change includes a hardcoded frozen
ordering of the language indices as they existed at 2f969a9, and
migrates to storing a language code string in the settings.json instead
for future reordering flexibility.

Similar to the settings.bin to settings.json migration, which renames
settings.bin to settings.bin.bak, this change renames language.bin to
language.bin.bak after adding the language setting to settings.json.

---

### 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**_
2026-05-01 09:39:28 -05:00
Jon Stieglitz 2f969a93b4 feat: context-aware screenshot filenames with book title (#1589)
## Summary

I love the new screenshot feature but I can tell that soon I'm gonna
have a folder full of screenshots and not know what's what. It would be
great if the folder could self organize.

When a screenshot is taken while reading, the filename would now include
the book title, chapter (EPUB), page number, and progress percentage.

Example: My-Great-Book_ch3_p5_42pct_12345.bmp

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **What changes are included?**

## Additional Context

* Non-reader screens keep the existing screenshot-<millis>.bmp naming.

---

### 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 reviewed all the changes but I don't have any experience with this
project so this is my best guess
2026-04-30 13:56:45 -05:00
5d2e5596b4 feat: X3 gyroscope-based tilt page turning via QMI8658 IMU (#1636)
Co-authored-by: justinian <juicecutlus@gmail.com>
Co-authored-by: Vincent Politzer <vincent@skipwithjoy.com>
2026-04-29 15:29:33 -04:00
spfenwick bc9651b664 fix: use same file name as KOReader for OPDS downloads (#1286)
This allows KOSync to work for files downloaded from OPDS when set to
use filename matching

## Summary

* **What is the goal of this PR?** Brings the names of files downloaded
via OPDS into line with the KOReader OPDS plugin so the files sync when
KOSync is set to filename matching
* **What changes are included?** Change the filename generated by the
OPDS browser from "\<title\> - \<author\>" to "\<author\> - \<title\>"
so it matches KOReader.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

Both KOReader and crosspoint sanitise the filename by removing special
characters. This step seems to already use the same rules.

---

### 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
2026-04-28 19:29:05 -05:00
b8a51522ca feat(theme): add roundedraff theme and fix sleep cover crop grid artifacts (#918)
## Summary
- Add new `roundedraff` theme.
- Fix sleep screen artifact where book cover showed grid lines in
`Cover` mode with `Crop`.

## Additional Context
## Key changes
- Added `src/components/themes/roundedraff/` theme implementation.
- Updated sleep cover rendering logic in:
  - `src/activities/boot_sleep/SleepActivity.cpp`
  - `src/activities/boot_sleep/SleepActivity.h`
- Improved cover generation/regeneration paths in:
  - `lib/Epub/Epub.cpp`
  - `lib/Epub/Epub.h`
  - `lib/Txt/Txt.cpp`
  - `lib/Txt/Txt.h`

## Verification
- Sleep screen set to Book cover
- Sleep screen cover mode set to Crop
- Confirmed grid artifacts no longer appear on cover sleep screen.

### 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: CaptainFrito <yzq6x5zypy@privaterelay.appleid.com>
Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
2026-04-27 19:01:22 +03:00
pablohc 907e14da28 fix: pressing space barely moves input cursor (#1729) (#1733)
## Summary

* **What is the goal of this PR?** 
Fix the visual keyboard cursor not advancing when typing a space. Typing
one space leaves the cursor at position 0; typing two spaces advances it
only by one space width. This affects all input types except URL.
* **What changes are included?** 
Replace `getTextWidth()` with `getTextAdvanceX()` in four locations
within `KeyboardEntryActivity::render()` for cursor positioning and
line-wrapping calculations.

## Additional Context

* **Root cause**: `getTextWidth()` returns the bounding-box width of the
drawn glyphs. The space glyph has `width=0` and `height=0` (it's
invisible), so `getTextWidth(" ") == 0`. The trailing `advanceX` of the
last character is only flushed when the *next* character is processed,
so a string ending in space reports zero width. `getTextAdvanceX()`
correctly includes the final glyph's advance, matching how `drawText()`
actually positions characters.

---

### 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**_
2026-04-22 22:18:26 +03:00
Arthur TazhitdinovandCopilot 1cf2239742 feat: Support for multiple OPDS servers (#1209)
## Summary

* Add support for configuring and using multiple OPDS servers, replacing
the previous single-server limitation. Closes
https://github.com/crosspoint-reader/crosspoint-reader/issues/1178
* New OpdsServerStore singleton (modeled after WifiCredentialStore) that
persists up to 8 OPDS servers to /.crosspoint/opds.json with MAC-based
password obfuscation.
* One-time migration from legacy single-server fields in
CrossPointSettings to the new store on first boot.
* New OpdsServerListActivity for the device UI — works in two modes: a
settings list (add/edit/delete servers) and a picker (select which
server to browse). When only one server is configured, the picker is
skipped automatically.
* Renamed CalibreSettingsActivity → OpdsSettingsActivity for clarity. It
now edits individual OpdsServer entries (name, URL, username, password,
delete).
* OpdsBookBrowserActivity now receives an OpdsServer at construction and
uses its credentials for all fetches/downloads, and shows the server
name in the header.
* HttpDownloader::fetchUrl and downloadToFile accept optional per-call
username/password parameters instead of reading from global settings.
* REST API endpoints on CrossPointWebServer: GET /api/opds, POST
/api/opds, POST /api/opds/delete — passwords are never exposed over the
API (only a hasPassword flag), and omitting the password field on update
preserves the existing one.
* Web UI (SettingsPage.html) with dynamic OPDS server management cards —
add, edit, save, and delete servers from the browser.
<img width="932" height="906" alt="SCR-20260416-stvu"
src="https://github.com/user-attachments/assets/a8f18d84-4204-46a0-bb31-b73d24b3255f"
/>


## Additional Context

* The OpdsServerStore JSON format and obfuscation scheme are identical
to WifiCredentialStore, so the same JsonSettingsIO infrastructure
handles both.
* The web API uses POST /api/opds/delete instead of DELETE /api/opds
because the ESP32 WebServer doesn't support the DELETE method with a
request body.
* Existing single-server configurations are migrated automatically — no
user action required. After migration the legacy CrossPointSettings
fields are cleared so it only runs once.
* The HttpDownloader changes are backward-compatible: the credential
parameters default to empty strings, so existing callers are unaffected.

---

### 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>
2026-04-21 18:35:41 -05:00
Brandon PhilipsandClaude Sonnet 4.6 c0ee096841 fix: relative opds paths and query param with copyparty (#1535)
## Summary

* **What is the goal of this PR?**

This PR fixes bugs when using Copyparty as an OPDS server. 

https://github.com/9001/copyparty?tab=readme-ov-file#opds-feeds

OPDS uses a query parameter `?opds` to differentiate between HTML
requests and OPDS requests for the same path. It also uses relative
paths in the responses instead of full paths. Crosspoint didn't handle
these two cases.

* **What changes are included?**

Fixes to the two issues above.

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
  specific areas to focus on).

Here is some example XML from my Copyparty instance:
https://gist.github.com/philips/9ecec29dfb69ed0591b032f16e799675

### 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
>**_

Partially. I used Claude code to write the fix. I am not a strong C++
programmer. But, I manually compiled and tested.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 15:35:17 -05:00
pablohc 1a145fe085 fix: keyboard feedback #1644 (#1697)
Addresses reviewer feedback from #1644:
- **Localize keyboard hint strings** — 13 hardcoded English strings
replaced with \`tr()\` macro (\`STR_KB_HINT_*\`), making them
translatable across all 22 languages (fallback to English when not yet
translated)
- **Deduplicate \`Lyra3CoversMetrics\`** — now derives from
\`LyraMetrics\` via lambda copy, overriding only \`homeCoverTileHeight\`
and \`homeRecentBooksCount\` (eliminates ~30 duplicated metric fields)
- **Unify keyboard drawing in \`BaseTheme\`** — \`drawTextField\` and
\`drawKeyboardKey\` overrides removed from \`LyraTheme\`; variability
controlled via \`keyboardKeyCornerRadius\` metric (0=Base, 6=Lyra).
Unified text field padding to 6, adopted Lyra's secondary label draw
order (main first, then secondary)
- **Add URL-optimized keyboard layout** — \`urlLayout\` with \`:\` and
\`/\` replacing \`=\` and \`,\` for easier URL input without switching
to SYM mode
---

### 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 **_
2026-04-20 12:47:57 -05:00
Justin Mitchellandjpirnay 302dea1eea fix: Switch to xpath map for paragraph level syncing in KOSync (#1686)
Switch KOReader sync progress mapping from chapter matching to
XPath-based mapping.

- resolves KOReader positions using real XHTML ancestry paths
- supports paragraph-based upload mapping with text offsets
where needed
- passes the current paragraph index into sync so uploads map
back to KOReader more accurately

No HTTP client changes are included. No reader-state or resume-flow
changes are included.

---------

Co-authored-by: jpirnay <jens@pirnay.com>
2026-04-20 12:47:41 -05:00
pablohc 77b2c31635 refactor: redesign on-screen keyboard (#1644)
# Refactor: Redesign On-Screen Keyboard

## Summary

Complete redesign of the on-screen keyboard (used for WiFi password,
KOReader, Calibre URLs) with improved layout, navigation, visual style,
and new input features: **cursor mode** for text navigation, **password
mode** with visibility toggle, and **URL mode** with pre-defined
snippets.

## Screenshots

### Base Theme
|**master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/49125857-12d0-4020-b872-05d0ddbf1d94"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/ad16656b-d66e-43dd-8697-2b85f709d7f8"
/> |

### Lyra Theme
| **master** | **PR #1644** |
|----------|-------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/d9901251-9154-48d2-83b4-376d3223d132"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/84b45949-ed61-4924-af1b-d570c4c61e13"
/> |

### Keyboard States

| ABC Mode | Symbol Mode | URL Mode |
|----------|-------------|----------|
| <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/6397a82e-50b8-4d03-92e0-f707ed7c9054"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/205d34fe-0413-49e9-9db2-30569c297ba0"
/> | <img width="480" height="280" alt="image"
src="https://github.com/user-attachments/assets/801ddeab-e082-4a22-a9df-c66adb1afc16"
/> |

| Cursor Mode | Password Toggle |
|-------------|-----------------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/841773e1-7a3c-45e5-aa89-e5787255608c"
/> | <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/9487a0b3-3f47-41ed-9cd3-b24e56e342a8"
/> |

## Changes

### Layout (10-column uniform grid)
- Reduced from 13/11/10 columns per row to **10 uniform columns** across
all rows
- Keyboard now uses **90% of screen width** (was ~66%)
- Row 0: Numbers `1-9, 0` with secondary symbols (`!@#$%^&*()`)
- Rows 1-3: Standard QWERTY letters
- Bottom row: `shift` | `#@!` | `___` | `←` | `OK`

### New Symbol Mode (#@!)
- New mode toggle key `#@!` / `abc` switches between letter and symbol
layouts
- Symbol layout: 4 rows (numbers, inverted symbols, paired symbols,
loose symbols)
- Covers all 95 printable ASCII characters
- **No secondary hints, no long-press** in symbol mode (simple and
direct)
- SHIFT key remains visible but **disabled** in symbol mode

### URL Mode
- In `InputType::Url`, the Space key becomes a **URL toggle** button
- Activating URL mode replaces the 4 content rows with a **3×3 grid of
URL snippets**:
  - Col 0 (protocols): `https://`, `http://`, `/opds`
  - Col 1 (hosts/ports): `www.`, `192.168.`, `:8080`
  - Col 2 (domains): `.com`, `.org`, `.net`
- Snippets are inserted as full strings at the cursor position
- URL mode **persists** after inserting a snippet (does not
auto-deactivate)
- Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del
- Up/Down navigation maps `bottomCol - 1` / `urlCol + 1`
- SHIFT disabled in URL mode
- SpecMode (`abc`) exits URL mode back to ABC
- SpecSpace (`URL`) toggles URL mode on/off; selection always stays on
the URL button
- Button styled with `KeyboardKeyType::Mode` for consistent outline

### Cursor Mode
- **Enter**: Long-press Up (500ms) while in keyboard mode
- **Exit**: Short-press Down while in cursor mode (resets
`passwordVisible`, clears toggle position)
- **Navigate**: Left/Right move cursor position within text (one
position per press, no continuous repeat)
- **Visual**:
  - Keyboard mode: underline cursor (2px line + serifs)
  - Cursor mode: inverted block cursor (black fill + white character)
- Block width adapts to the actual character width under cursor (minimum
6px for narrow chars like space)
- Block position includes inter-character kerning offset for correct
alignment (calculated via string-difference: `getTextWidth(before+char)
- getTextWidth(before) - getTextWidth(char)`)
  - End-of-text: thin 6px block
- Password hidden: 3-part drawing (Part 1 + block + Part 3) to prevent
block overflow onto `*` characters
- Toggle position: caret ("I") cursor at saved position, `[abc]`/`[***]`
label with inverted selection
- **Inactive key styling**:
  - BaseTheme: 2px outline rectangle
  - LyraTheme: gray filled rounded rectangle (`Color::LightGray`)
- **Password toggle position**: in cursor mode (Password only), Hold
Right (500ms) enters toggle — caret cursor shows saved position,
`[abc]`/`[***]` label becomes selected. Press Confirm to toggle
`passwordVisible`. Press Left to restore cursor to saved position. Right
from toggle is a no-op. Down from toggle exits to keyboard.
- `cursorPos` persists between keyboard and cursor modes

### Password Mode
- `InputType::Password` enum replaces `bool isPassword` parameter
- Text is masked with `*` except for one revealed character:
  - Keyboard mode: reveals character at `cursorPos - 1`
- Cursor mode: no reveal in display text (block cursor draws actual char
directly)
- **Toggle `[abc]`/`[***]`**: accessible via cursor mode — Hold Right
(500ms) enters toggle position, Confirm toggles visibility, Left exits
back to cursor. Caret ("I") shown at saved position while in toggle.
- `passwordVisible` resets to `false` when exiting cursor mode
- **Long-press Del (1.5s)**: clears all text and resets cursor to 0

### InputType Enum
- Replaced `bool isPassword` constructor parameter with `enum class
InputType { Text, Password, Url }`
- Callers updated: `WifiSelectionActivity`, `KOReaderSettingsActivity`,
`CalibreSettingsActivity`

### Contextual Tips
- `"Tips:"` header followed by context-sensitive hints, centered between
text field underline and keyboard as a block
- ABC mode: `"Hold SELECT for UPPERCASE or secondary char"` (shift ON:
`"lowercase"` variant) + `"Hold DEL to clear all text"` (only if text
not empty)
- ABC + `InputType::Url`: same + `"Press URL for snippets"`
- Symbol mode: `"Hold DEL to clear all text"` (only if text not empty)
- URL mode: `"Press ABC to exit URL mode"` + `"Hold DEL to clear all
text"` (only if text not empty)
- Cursor mode: `"Press DOWN to return to keyboard"`

### Hint Phases (cursor mode, Password only)
- **Phase 1**: `"Hold UP to edit entry"` — shown after 2× DEL press,
auto-hides after 4s, positioned below underline
- **Phase 2**: `"Press < or > to move cursor"` + dynamic password toggle
hint — shown when entering cursor mode, positioned below underline,
visible until exit
- When `!passwordVisible`: `"Hold > then press [abc] to show password"`
  - When `passwordVisible`: `"Hold > then press [***] to hide password"`
  - When in toggle position: `"Press < to return to cursor position"`

### Long-Press Alternative Character
- Holding Confirm (>500ms) inserts the **alternative character** instead
of the primary
- Letters: long-press inserts opposite case (e.g., `a`→`A`, `A`→`a`)
- Numbers/symbols (row 0): long-press inserts secondary (e.g., `0`→`)`,
`)`→`0`)
- Only active in ABC mode; disabled in Symbol mode and URL mode
- **`InputType::Url`**: Hold SELECT on ABC rows 1+ (letters) returns
primary character only (same as short press). Row 0 (symbols) still
returns secondary character on Hold SELECT.

### Shift (2 sticky states)
- Reduced from 3 states (shift/SHIFT/LOCK) to **2 sticky states**
(shift/SHIFT)
- Shift stays active after typing until manually toggled off
- Label: `shift` (off) / `SHIFT` (on)

### SpecialKeyType Enum
- `enum class SpecialKeyType { Shift, Mode, Space, Del, Ok }` replaces
plain `enum` (`SpecShift`, `SpecMode`, etc.) for type safety
- All switch cases updated to `SpecialKeyType::*` with
`static_cast<int>()` for array indexing
- `onExit()` reverted to simple `Activity::onExit()` call (half-refresh
removed)
- **Bottom row column mapping**: navigating up/down between content rows
and bottom row uses `col/2` and `col*2` formulas for consistent
positioning (10 cols ↔ 5 cols)
- **URL mode column mapping**: `bottomCol - 1` / `urlCol + 1` (3 cols ↔
5 cols)
- **Wrap-around**: row 0 → up → bottom row and bottom row → down → row 0
both apply correct column mapping

### Visual Improvements (both Base and Lyra themes)
- **Space key**: underscore-style horizontal line (60% of key width, 3px
thick)
- **Delete key**: arrow `←` drawn with lines (3px thick) instead of
"DEL" text
- **Secondary label** (ABC row 0): small hint in top-right corner with
separation from primary number
- **BaseTheme**: selection uses **inverted fill** (black rect + white
text) instead of `[bracket]` markers
- **BaseTheme**: text field brackets drawn as **stretchable lines** that
adapt to multi-line input (1px normal, 3px cursor mode)
- **LyraTheme**: text field uses **fixed-width underline** (16px
margins, 8px each side) instead of stretchable line (2px normal, 3px
cursor mode)
- **Both themes**: special keys (shift, mode, space, del, OK) have
bordered/bordered-rounded rectangles
- **Font size**: keyboard uses `UI_12_FONT_ID` in both themes (was
`UI_10` in Base)
- **Key height**: 40px in all themes for better proportions
- **Layout unification**: text and password toggle are left-aligned in
all themes (`keyboardCenteredText = false` for Lyra/Lyra3Covers)
- **`primaryOffset` removed**: dead code eliminated from BaseTheme and
LyraTheme `drawKeyboardKey`

### New Theme Metrics
- `keyboardVerticalOffset`: per-theme vertical adjustment of keyboard
position
  - Base: `-13`, Lyra: `-7`
- `keyboardBottomKeySpacing`: independent spacing for bottom row keys
  - Base: `5`, Lyra: `5`
- Bottom-aligned keyboard in both themes for consistent vertical
positioning
- Bottom row total width calculated to match content rows width (10-col
based, consistent across modes)
- 4px extra gap between content rows and bottom row when `bkSpacing > 0`
- `keyboardCenteredText`: `false` for all themes (unified left-aligned
text)

### Defensive Improvements
- **State reset on re-entry**: `onEnter()` resets all mutable state
(`symMode`, `urlMode`, `cursorMode`, `togglePos`, `passwordVisible`,
`shiftState`, `selectedRow`, `selectedCol`, `rightHeld`,
`rightLongHandled`, `savedCursorPos`, `rightStartCursorPos`,
`delPressCount`, `hintVisible`, `hintShowTime`) — prevents stale state
when re-entering the keyboard
- **Bounds checking**: `insertChar`/`insertString` clamp `cursorPos` to
`text.length()` before inserting
- **Empty string guard**: `insertString` returns early on empty string
- **`std::string::npos`**: used instead of `SIZE_MAX` for size_t
sentinel (proper C++ idiom)
- **`<algorithm>` header**: included for `std::max`

## Files Modified

| File | Changes |
|------|---------|
| `src/activities/util/KeyboardEntryActivity.h` | `InputType` enum,
`KeyDef` struct, 10-col layouts, cursor/password/URL/toggle state, hints
(`delPressCount`, `hintVisible`, `hintShowTime`), held vars
(`rightHeld`, `rightLongHandled`, `savedCursorPos`,
`rightStartCursorPos`), `mapColContentBottom` helper |
| `src/activities/util/KeyboardEntryActivity.cpp` | Complete rewrite:
layout rendering, symbol/cursor/password/URL modes, toggle position,
long-press, contextual tips, hint phases, block cursor kerning
alignment, defensive bounds checks, state reset |
| `src/components/themes/BaseTheme.h` | `KeyboardKeyType` enum, new
`drawTextField`/`drawKeyboardKey` signatures, `keyboardVerticalOffset`,
`keyboardBottomKeySpacing` metrics |
| `src/components/themes/BaseTheme.cpp` | Redesigned `drawTextField`
(stretchable brackets), `drawKeyboardKey` (inverted selection,
space/delete graphics, secondary label, inactive selection), removed
`primaryOffset` dead code |
| `src/components/themes/lyra/LyraTheme.h` | Override signatures,
`keyboardVerticalOffset`, `keyboardBottomKeySpacing`,
`keyboardKeyHeight` adjustments |
| `src/components/themes/lyra/LyraTheme.cpp` | `drawTextField` (fixed
underline), `drawKeyboardKey` (rounded rects for special keys,
space/delete graphics, secondary label, inactive selection), removed
`primaryOffset` dead code |
| `src/components/themes/lyra/Lyra3CoversTheme.h` |
`keyboardCenteredText = false`, `keyboardVerticalOffset = -7`, inherits
Lyra overrides |
| `src/activities/network/WifiSelectionActivity.cpp` | `bool isPassword`
→ `InputType::Password` |
| `src/activities/settings/KOReaderSettingsActivity.cpp` | `bool
isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` |
| `src/activities/settings/CalibreSettingsActivity.cpp` | `bool
isPassword` → `InputType::Text`/`InputType::Password`/`InputType::Url` |

## Backward Compatibility

- **API change**: Constructor parameter changed from `bool isPassword`
to `InputType inputType` (default `InputType::Text`)
- **All callers updated**: WiFi, KOReader, and Calibre integrations
migrated to new `InputType` enum

## Testing

### Input & Text Handling
- [x] Empty input → press OK (submit empty string)
- [x] Back button → cancel (no text returned)
- [x] Pre-filled initial text (e.g., editing existing WiFi password)
- [x] Password mode: text masked with `*` characters, one character
revealed
- [x] Delete on empty text (no crash)
- [x] Very long text near maxLength limit
- [x] URL with path and port (~60 chars)
- [x] Multi-line text wrapping in input field
- [x] Space insert in middle of text (cursor mode)
- [x] Delete last character repeatedly
- [ ] Type all 95 printable ASCII characters

### Mode Switching
- [x] ABC → #@! preserves typed text and cursor position
- [x] #@! → ABC preserves typed text and cursor position
- [x] Shift state preserved when switching modes
- [x] Switch modes multiple times rapidly

### Shift Behavior
- [x] Shift OFF → type letter → inserts lowercase, shift stays OFF
- [x] Shift ON → type letter → inserts uppercase, shift stays ON
- [x] Shift ON → type number → inserts symbol, shift stays ON
- [x] Shift ON → navigate rows → shift stays ON
- [x] Shift ON → switch to #@! → shift shows "shift" (disabled)
- [x] Shift ON → switch to ABC → shift state preserved
- [x] Shift ON → switch to URL → shift shows "shift" (disabled)
- [x] Shift disabled in URL mode: pressing shift does nothing

### Long-Press
- [x] Long-press letter with shift OFF → inserts uppercase
- [x] Long-press letter with shift ON → inserts lowercase
- [x] Long-press number → inserts secondary symbol
- [x] Long-press symbol (row 0) → inserts opposite (number)
- [x] Long-press key without secondary (e.g., `-`, `=` in rows 2-3) →
inserts primary character on release
- [x] Long-press on special keys (shift, mode, space, del, ok) → no
alternative inserted
- [x] Long-press in #@! mode → no effect (disabled)
- [x] Long-press in URL mode → no effect (disabled)
- [x] Long-press number in row 0 with InputType::Url → inserts secondary
symbol (same as non-URL)
- [x] Short press after cancelled long-press → normal behavior
- [x] Long-press at maxLength → no character inserted
- [x] Long-press Del (1.5s) → clears all text

### Cursor Mode
- [x] Long-press Up → enters cursor mode
- [x] Short-press Down → exits cursor mode (resets passwordVisible)
- [x] Left/Right navigate within text
- [x] Left at position 0 → no movement
- [x] Right at end of text → no movement in Text mode, enters toggle in
Password mode (Hold Right)
- [x] Block cursor visual: correct width for character, thin block at
end
- [x] Underline cursor visual (keyboard mode): correct position with
serifs
- [x] Inactive key styling: outline (Base) or gray fill (Lyra) on
selected key
- [x] Typing with cursor mid-text → inserts at cursor position
- [x] Deleting with cursor mid-text → deletes character before cursor
- [x] Exit cursor mode → type at cursor position (inserts mid-text, not
at end)
- [x] Exit cursor mode from toggle → cursor at saved position (not end
of text)

### Password Mode
- [x] Masked text with one revealed character at `cursorPos - 1`
- [x] Cursor mode: block shows actual character, display text all `*`
- [x] Toggle `[abc]`/`[***]`: Hold Right (500ms) in cursor mode enters
toggle, Confirm toggles visibility, Left exits back to cursor
- [x] Exiting cursor mode resets `passwordVisible` to false
- [x] Long-press Del clears all text

### URL Mode
- [x] URL toggle activates/deactivates URL mode
- [x] URL button stays selected after toggle (both on and off)
- [x] Deactivating URL mode returns to ABC (not SYM)
- [x] 3×3 snippet grid displays correctly
- [x] Column alignment: col 0 over ABC, col 1 over URL, col 2 over Del
- [x] Snippet insertion: inserts full string at cursor position
- [x] URL mode persists after snippet insertion
- [x] Shift disabled in InputType::Url
- [x] SpecMode (`abc`) exits URL mode to ABC
- [x] Up/Down navigation between URL grid and bottom row

### Re-entry State Reset
- [x] Enter keyboard → activate URL mode → exit → re-enter → URL mode
OFF
- [x] Enter keyboard → switch to SYM → exit → re-enter → ABC mode
- [x] Enter keyboard → enter cursor mode → exit → re-enter → keyboard
mode
- [x] Enter keyboard → enter toggle pos → exit → re-enter → togglePos
OFF
- [x] Enter keyboard → activate shift → exit → re-enter → shift OFF
- [x] Enter password keyboard → toggle password visible → exit →
re-enter → password hidden

### Navigation
- [x] Left/right wrap-around within content rows
- [x] Left/right wrap-around within bottom row
- [x] Up from row 0 → bottom row (correct column mapping)
- [x] Down from bottom row → row 0 (correct column mapping)
- [x] Up from bottom row → last content row (correct column)
- [x] Down from last content row → bottom row (correct column)
- [x] Navigate horizontally in bottom row, then up → correct content
column
- [x] Navigate horizontally in bottom row, then down (wrap) → correct
content column

### Visual (both themes)
- [x] Secondary hints only on ABC row 0
- [x] No secondary hints in #@! mode or URL mode
- [x] No secondary hints on letter rows (1-3)
- [x] Space bar: horizontal line centered, not touching edges
- [x] Delete: arrow `←` drawn correctly
- [x] Selected key: inverted colors (black fill, white text)
- [x] All special keys have border rectangles
- [x] Fixed underline in text field (Both themes)
- [x] Mode key label: `#@!` in ABC mode, `abc` in symbol mode, `abc` in
URL mode
- [x] URL key label: `URL` (only in InputType::Url), styled same as
other bottom keys
- [x] Shift label: `shift` when OFF, `SHIFT` when ON, `shift` when
disabled (SYM/URL)
- [x] Both themes: bottom row total width matches content rows width
- [x] URL snippet grid centered over ABC/URL/Del buttons

### Device & Theme Coverage
- [ ] Base Theme on X3
- [x] Base Theme on X4
- [ ] Lyra Theme on X3
- [x] Lyra Theme on X4
- [ ] Lyra Extended Theme on X3
- [x] Lyra Extended Theme on X4

### Toggle Position
- [x] Hold Right > 500ms in cursor mode (Password) → enters toggle,
caret visible at saved position
- [x] Short-press Right in cursor mode (Password) → advances cursor 1
position, does not jump to toggle
- [x] Short-press Left in cursor mode (Password) → moves cursor left 1
position, from toggle returns to saved position
- [x] Confirm in toggle → toggles `passwordVisible`
- [x] Left from toggle → returns to saved position, caret disappears,
block cursor appears
- [x] Right from toggle → no-op
- [x] Down from toggle → exits to keyboard, cursor at saved position
- [x] Hold Right in cursor mode (InputType::Text) → no effect
- [x] Hold Right in cursor mode (InputType::Url) → no effect
- [x] Hold Right < 500ms released in cursor mode (Password) → short
press, advances cursor 1
- [x] No continuous repeat when holding Left or Right in cursor mode

### Caret Visual in Toggle
- [x] In toggle: caret "I" visible at saved cursor position
- [x] Character under cursor visible (no gap) in password not-visible
mode
- [x] Character under cursor visible in password visible mode
- [x] `[abc]`/`[***]` label with inverted selection in toggle

### Contextual Tips
- [x] `"Tips:"` header centered above contextual hints
- [x] Single tip → `"Tips:"` + one line
- [x] Multiple tips → `"Tips:"` + multiple lines, all centered as block
- [x] No tips shown when not applicable (e.g., ABC with empty text and
non-URL)
- [x] `"UPPERCASE"` shown when shift OFF
- [x] `"lowercase"` shown when shift ON
- [x] `"secondary char"` shown for InputType::Url

### Hint Phases
- [x] 2× DEL → Phase 1 appears ("Hold UP to edit entry")
- [x] Phase 1 auto-hides after 4s
- [x] Phase 2 appears when entering cursor mode ("Press < or > to move
cursor")
- [x] Phase 2 shows "Hold > then press [abc] to show password" when
`!passwordVisible`
- [x] Phase 2 shows "Hold > then press [***] to hide password" when
`passwordVisible`
- [x] Phase 2 shows "Press < to return to cursor position" when in
toggle
- [x] Phase 2 disappears when exiting cursor mode

### Long-Press `InputType::Url` Behavior
- [x] Hold SELECT on letter rows (rows 1+) with InputType::Url → same
character as short press
- [x] Hold SELECT on row 0 with InputType::Url → secondary character
works normally

### Number Row Reorder
- [x] Number row order: 1-9, 0 left to right
- [x] `(` and `)` are adjacent (positions 8 and 9) via secondary labels
- [x] Long-press on row 0 returns correct secondary symbols in new order
- [x] SYM row 1: `(` and `)` also adjacent (positions 8 and 9)

### Block Cursor Alignment
- [x] Block cursor correctly positioned for consecutive spaces (kerning
offset applied)
- [x] Block cursor correctly positioned for mixed characters (letters,
numbers, symbols)
- [x] Block width minimum 6px for narrow characters (space) — visible as
block, not thin line
- [x] Password hidden: 3-part drawing prevents block overflow onto `*`
characters
- [x] Password visible: block post-loop draws correctly on continuous
text (no 3-part needed)
- [x] End-of-text block: thin 6px block at correct position

### Integration
- [x] WiFi password entry (connect to network)
- [ ] KOReader username, password, and sync server URL
- [ ] Calibre OPDS URL, username, and password
- [ ] Calibre OPDS URL: empty → opens with "https://" prefilled
- [ ] Calibre OPDS URL: type "http://" or "https://" only → saved as
empty
- [ ] Calibre OPDS URL: type full URL → saved correctly
- [ ] Calibre OPDS URL: existing URL → opens with existing URL (not
"https://" prefill)
2026-04-18 16:12:56 -05:00
Jan IvanovandJan Ivanov 3b12c083bc fix: make footnotes consider orientation for gutters (#1665)
## Summary

* **What is the goal of this PR?** 
Noticed that the footnotes selection screen does not add proper margins
to accommodate screen orientation
 
* **What changes are included?**
Copied some code over from `EpubReaderChapterSelectionActivity` to
calculate the proper margins in `CW` and `Inverted` orientation

| Before | After |
|--------|--------|
| <img width="578" height="435" alt="image"
src="https://github.com/user-attachments/assets/0518a0c6-13d2-48a1-9283-90c83861e4c2"
/> | <img width="578" height="435" alt="image"
src="https://github.com/user-attachments/assets/ac34365c-72d0-4f07-85a6-17e966b28909"
/> |
| <img width="328" height="435" alt="image"
src="https://github.com/user-attachments/assets/0614f19b-1000-4efe-8ef9-b533d2763a53"
/> | <img width="328" height="435" alt="image"
src="https://github.com/user-attachments/assets/ce9add2f-88e8-4032-a59c-efb55f366604"
/> |

---

### 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: Jan Ivanov <jan.ivanov@sirma.com>
2026-04-17 01:37:41 +03:00
zgredexandPatryk Radtke c4f5c8e931 fix: prevent wallpaper clustering with 16-entry recency buffer (#1606)
## Problem

Custom sleep wallpapers feel repetitive — the same image appearing
multiple times in a short session. Only the single most-recently-shown
index was stored (`uint8_t lastSleepImage`), so on collections of 3–5
images, two-pick cycles were common. On larger collections, any image
could reappear within the next few picks.

## Solution

Add a 16-entry circular recency buffer to `CrossPointState` that
excludes recently shown wallpapers from selection.

**Recency buffer** (`recentSleepImages[16]`, `recentSleepPos`,
`recentSleepFill` — 34 bytes DRAM):
- Tracks the last 16 shown wallpaper indices
- On each pick, rerolls up to 20 times if the candidate was recently
shown
- Window auto-shrinks to `numFiles - 1` for small collections
(guarantees a non-repeat is always possible)
- `isRecentSleep()` clamps to `recentSleepFill` to avoid false positives
on unwritten buffer slots
- State persisted to `state.json` so the buffer survives sleep/wake
cycles

**Migration**:
- Binary (`state.bin`): old `lastSleepImage` field seeded into the new
buffer if valid
- JSON (`state.json`): legacy `lastSleepImage` key detected and seeded
into the buffer when upgrading from older firmware

## Index type fix

`randomFileIndex` upgraded from `uint8_t` to `uint16_t` — silently
truncated for collections larger than 255 wallpapers.

## Repeat probability: before vs after

Chance of seeing a recently-shown image on the next pick. After:
`(numFiles - 16) / numFiles` once collection exceeds the window; 0%
while ≤17.

| Collection | Before | After |
|---|---|---|
| 3 | 50% | 0% |
| 5 | 75% | 0% |
| 10 | 89% | 0% |
| 17 | 94% | 0% |
| 18 | 94% | 6% |
| 20 | 95% | 20% |
| 25 | 96% | 36% |
| 30 | 97% | 47% |
| 50 | 98% | 68% |
| 100 | 99% | 84% |

## Memory impact

| Addition | Size |
|---|---|
| `recentSleepImages[16]` | 32 bytes DRAM |
| `recentSleepPos` + `recentSleepFill` | 2 bytes DRAM |
| **Total** | **34 bytes DRAM** |

## Files changed

- `src/CrossPointState.h` — recency buffer fields + `isRecentSleep()` /
`pushRecentSleep()` declarations
- `src/CrossPointState.cpp` — `isRecentSleep()` / `pushRecentSleep()`
implementations + binary migration path
- `src/JsonSettingsIO.cpp` — JSON serialisation of buffer state + JSON
migration
- `src/activities/boot_sleep/SleepActivity.cpp` — retry loop with
recency check

Co-authored-by: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
2026-04-17 00:05:21 +03:00
jpirnay ed54f97909 fix: Fix ghosting on exit of BMPViewer (#1432)
## Summary

* **What is the goal of this PR?** After displaying an image via the
filebrowser ghosting artifacts remained on the screen
* **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? _**< NO >**_
2026-04-16 00:52:29 +02:00
Егор Мартынов cced77783f feat: add orientation-aware popups for reader activities (#1428)
## Summary

Make popups (like "Going to sleep") respect the current screen
orientation when shown from reader activities.

**What changes are included?**

- Apply reader orientation in SleepActivity before showing popup when
`lastSleepFromReader` is true
- Make popup Y-position proportional to screen height (7.5% for
BaseTheme, 16.5% for LyraTheme) instead of hardcoded pixel values,
ensuring correct positioning in both portrait and landscape modes.
- Add `isReaderActivity()` override to all reader sub-screens (menu,
chapter selection, percent selection, footnotes, QR display, KOReader
sync), so sleep popups rotate correctly when entering sleep from any
reader context.

## Additional Context

<img
src="https://github.com/user-attachments/assets/47d88c2c-ffc5-41a7-b3f2-af272ea0150e"
width="400" height="240">

---

### 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.5)
2026-04-14 17:21:55 -05:00
Zach Nelson 23aad213fc refactor: Removed redundant FsFile close() calls (#1434)
## Summary

**What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

`DESTRUCTOR_CLOSES_FILE=1` is set in platformio.ini, which makes SdFat's
FsBaseFile destructor call close() automatically when a file goes out of
scope.

Three categories of file close calls remain untouched:
1. Close before Storage.remove() on the same path: ScreenshotUtil.cpp
closes the file before deleting it on write error. The remove might fail
if the file is still open.
2. Close before reopening the same variable: Epub.cpp writes a temp
NCX/nav file, closes it, then reopens it for reading. The
RecentBooksStore.cpp close before saveToFile() is the same pattern, it
rewrites the same file.
3. Close on member variables: BookMetadataCache.cpp (bookFile,
spineFile, tocFile), Section.cpp (file), XtcParser.cpp (m_file),
ZipFile.cpp (file). These persist beyond any single function scope, so
the destructor timing doesn't match the intended close point.

---

### 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**_
2026-04-14 16:41:05 -05:00
Xuan-Son Nguyen 243ae8b408 feat: show crash reason on boot (#1453)
## Summary

If the system reboots from crash, display the reason and tell user to
include `crash_report.txt` file.

To test this, simply add an `assert(false)` somewhere inside the code.


![screenshot-74453.bmp](https://github.com/user-attachments/files/26160373/screenshot-74453.bmp)

---

### 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**
2026-04-13 15:23:45 -05:00
zgredexandPatryk Radtke 4e9c7a787f feat: show full path bar in file browser (#1411)
![obraz](https://github.com/user-attachments/assets/f848a381-7c45-4724-912b-56168791bcf3)


## Summary

Adds a full path display at the bottom of the file browser with a
separator line matching the header style. Path uses the small font,
left-truncates to always show the deepest folder when path is too long.
Toggleable via Settings > System > Show Full Path (default 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: Patryk Radtke <patryk@Patryks-MacBook-Pro.local>
2026-04-13 15:12:49 -05:00
Ian Chasse 405ce0c3c8 feat: Rework "Cover + Custom" sleep screens to show covers only when currently reading (#1256)
## Summary

The cover/custom setting was a bit misleading to me. Usually after you
start reading a book you never see your customs again unless a cover
fails to render for some reason. With this, you can easily show your
custom images by just sleeping from the menus instead of the reader

## 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 >**_
2026-04-12 21:03:23 -04:00
fa2a3d2539 feat: add OPDS search support & next/prev page navigation (#1462)
## Summary

**What is the goal of this PR?**
Adds OPDS search support, allowing users to search a catalog directly
from the book browser when the server exposes an OpenSearch template.

**What changes are included?**
- `OpdsParser`: parses the OpenSearch template URL from feed-level
`<link rel="search">` elements and exposes it via `getSearchTemplate()`
- `OpdsBookBrowserActivity`: fetches and stores the search template
after each feed load; shows a Search hint on the Left button when a
template is available; launches the existing `KeyboardEntryActivity` for
query input; URL-encodes the query and fetches the result feed
- Absolute search result URLs are handled correctly in `fetchFeed`
(skips prepending the server base URL)
- A `consumeConfirm` guard prevents the Confirm release that submits the
keyboard from immediately triggering a book download on the first
browsing frame after search results load

## Additional Context

- Search is silently unavailable if the server does not advertise an
OpenSearch template — no UI change in that case
- Tested against a Calibre-Web OPDS endpoint which exposes `<link
rel="search" type="application/opensearchdescription+xml">`
- The inline URL encoder in `performSearch` was necessary as
`StringUtils` has no such utility; worth considering extracting to
`StringUtils` in a follow-up
- No new dependencies 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**_

---------

Co-authored-by: kira <rammah@tuta.io>
Co-authored-by: Justin Mitchell <justin@jmitch.com>
2026-04-12 12:24:27 -04:00
Mraulio 5c12f2f01e fix: avoid skipping chapter after screenshot (#1625)
## Summary

* **What is the goal of this PR?** 
Fixes skipping chapter when it's enabled in Settings and you take a
Screenshot.
* **What changes are included?**
A simple return if Power and Down were released before the skipChapter.

## Additional Context

* There is an Issue related #1595.
* Checked that it kept the Power button funcionality for Next page and
Suspend.

---

### 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**_
2026-04-11 20:28:00 +03:00
Jan IvanovandJan Ivanov 8d6b35b8e7 fix: back navigation from BMPViewer (#1597)
## Summary

This fixes navigating back from the BMP Viewer to the FileBrowser which
was broken when moving to the new ActivityManager

This is fixed by making FileBrowserActivity able to take a full file
path on enter and splitting the basePath and fileName from it and
navigating to the correct place.

fixes:
https://github.com/crosspoint-reader/crosspoint-reader/issues/1553

duplicates:
https://github.com/crosspoint-reader/crosspoint-reader/pull/910 to some
extend but mine has the file cursor at the correct file instead of the
first one in the folder

## 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: Jan Ivanov <jan.ivanov@sirma.com>
2026-04-11 20:20:46 +03:00
CaptainFrito 5349e81723 feat: Display file extensions in File Browser (#1019)
## Summary

![IMG_8136
Medium](https://github.com/user-attachments/assets/e33a8a71-4126-4bdc-8d3a-32132acba712)
![IMG_8137
Medium](https://github.com/user-attachments/assets/2289fbee-71e8-446c-ad28-9dd4a9a21396)
![IMG_8138
Medium](https://github.com/user-attachments/assets/0e912f72-9be1-4708-a909-4dab192d1aea)


## Additional Context

Do we want a setting to toggle this?

---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-04-09 09:54:46 -05:00
jpirnay ed0811c898 fix: Fix failing very first wifi connection attempt (#1521)
## Summary

* **What is the goal of this PR?** The very first Wifi connection
attempt with saved credentials failed, subsequent attempts succeeded.
This PR fixes the first-attempt-issue.
* **What changes are included?**

## Additional Context
Claude analysis for WifiSelectionActivity

In attemptConnection() ,there's no WiFi.disconnect() before WiFi.begin()
— unlike the scan path earlier which does do a disconnect first.

The root cause on ESP32 is the built-in auto-connect feature: the ESP32
WiFi stack saves credentials to NVS flash and automatically starts
trying to connect on boot before your application code runs. When your
attemptConnection() then calls WiFi.begin(), the stack is already in a
transitional CONNECTING state, and the new begin() call either gets
ignored or collides with the in-progress attempt.

The fix is to add WiFi.disconnect(true) + a short delay in
attemptConnection() before calling WiFi.begin(), and optionally call
WiFi.persistent(false) to stop the ESP32 from auto-connecting on its
own.

---

### 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 >**_
2026-04-08 21:26:12 +03:00
nscheung d29b8ee2f9 feat: Adjust Navigation at End of Book (#1425)
## Summary

* **What is the goal of this PR? (e.g., Implements the new feature for
file uploading.)**
Currently, pressing forward at the end of a book loops back to the last
page. This change will instead sends you to the home page instead.

* **What changes are included?**
Applies the change to the three supported format: EPUB, XTC, TXT

## Additional Context

* This is more of a QOL improvement than a new feature. If there's
interest, we could extend this to track a completed state for ebooks.

---

### 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**_
2026-04-08 21:09:28 +03:00
Zach Nelson 11984f8fef refactor: Use C++20 'requires' in ActivityResult constructor (#1420)
## Summary

**What is the goal of this PR?**

Replace SFINAE std::enable_if_t with a C++20 `requires` clause for
clearer constraint expression and better compiler diagnostics on
mismatch.

---

### 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**_
2026-04-07 08:30:52 -05:00
Justin Mitchell 9b3885135f feat: Initial support for the x3 (#875)
## Summary

Adds Xteink X3 hardware support to CrossPoint Reader. The X3 uses the
same SSD1677 e-ink controller as the X4 but with a different panel
(792x528 vs 800x480), different button layout, and an I2C fuel gauge
(BQ27220) instead of ADC-based battery reading.

All X3-specific behavior is gated by runtime device detection — X4
behavior is unchanged.

Depends on community-sdk X3 support: open-x4-epaper/community-sdk#19
(merged).

## Changes

### HAL Layer

**HalGPIO** (`lib/hal/HalGPIO.cpp/.h`)
- I2C-based device fingerprinting at boot: probes for BQ27220 fuel
gauge, DS3231 RTC, and QMI8658 IMU to distinguish X3 from X4
- Detection result cached in NVS for fast subsequent boots
- Exposes `deviceIsX3()` / `deviceIsX4()` helpers used throughout the
codebase
- X3 button mapping (7 GPIOs vs X4's layout)
- USB connection detection and wake classification for X3

**HalDisplay** (`lib/hal/HalDisplay.cpp/.h`)
- Calls `einkDisplay.setDisplayX3()` before init when X3 is detected
- Requests display resync after power button / flash wake events
- Runtime display dimension accessors (`getDisplayWidth()`,
`getDisplayHeight()`, `getBufferSize()`)
- Exposed as global `display` instance for use by image converters

**HalPowerManager** (`lib/hal/HalPowerManager.cpp/.h`)
- X3 battery reading via I2C fuel gauge (BQ27220 at 0x55, SOC register)
- X3 power button uses GPIO hold for deep sleep

### Display & Rendering

**GfxRenderer** (`lib/GfxRenderer/GfxRenderer.cpp/.h`)
- Buffer size and display dimensions are now runtime values (not
compile-time constants) to support both panel sizes
- X3 anti-aliasing tuning: only the darker grayscale level is applied to
avoid washed-out text on the X3 panel. X4 retains both levels via
`deviceIsX4()` gate

**Image Converters** (`lib/JpegToBmpConverter`, `lib/PngToBmpConverter`)
- Cover image prescale target uses runtime display dimensions from HAL
instead of hardcoded 800x480

### UI Themes

**BaseTheme / LyraTheme** (`src/components/themes/`)
- X3 button position mapping for the different physical layout
- Adjusted UI element positioning for 792x528 viewport

### Boot & Init

**main.cpp**
- X3 hardware detection logging
- Adjusted init sequence for X3 (no `HalSystem::begin()` dependency on
X3 path)

**HomeActivity**
- Uses runtime `renderer.getBufferSize()` instead of static
`GfxRenderer::getBufferSize()`

FYI I did not add support for the gyro page turner. That can be it's own
PR.
2026-04-04 10:25:43 -05:00
jpirnay 710055f02c feat: Make directories stand out more in local file browser: "[dir]" instead of "dir" (#1339)
## Summary

* **What is the goal of this PR?** It's difficult to distinguish
directory names from normal file entries, so they are displayed now as
"[dir]" instead of "dir" for classic theme
* **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? _**NO**_
2026-03-24 19:47:42 -05:00
Zach Nelson 0cbfaa007d fix: Overlapping battery percentage on image pages with anti-aliasing (#1452)
## Summary

**What is the goal of this PR?**

When viewing a page with images and anti-aliasing enabled, the
`imagePageWithAA` path renders the page twice with fast refreshes (blank
image area, then restore). Both passes called `renderStatusBar()`, which
reads the battery percentage live. If the value changed between the two
renders (e.g. 88% -> 87%), the digits would overlap on screen.
Fix: Removed the redundant `renderStatusBar()` from the second BW
render. The status bar is already drawn and displayed in the first pass,
and only the image area needs restoration.

---

### 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**_
2026-03-23 22:05:15 +00:00
LSTARandJake Kenneally 8dd365b4da feat: Implement silent pre-indexing for the next chapter in EpubReaderActivity (#979)
## Summary

* A simple tweak to pre-index the next chapter silently during normal
reading.
* Triggers silent pre-indexing of the next chapter when the penultimate
page of a chapter is rendered to reduce visible interruptions.
* Keeps existing indexing with popup when a reader jumps directly into
an unindexed chapter.

## Additional Context

* Reader input is temporarily blocked during silent indexing to avoid
navigation/index state conflicts.
* The penultimate page is used because readers typically spend longer
there than on the final page.
* This change optimizes linear reading flow while preserving reliable
indexing for non-linear navigation.

## Possible Improvements

* Add a setting for First Page Indexing vs Penultimate Page Pre-indexing
* Display an indexing icon in the status bar instead of using a popup
that overlaps book text.

Tested on device:

https://www.dropbox.com/scl/fi/29g5kjqgsi5e4hgujv38u/Silent-Indexing.MOV?rlkey=yemi4mosmev5vicaa7gpe49qw&dl=0

---

### 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: Jake Kenneally <jakekenneally@gmail.com>
2026-03-19 23:20:26 -05:00
jpirnay f9286709d1 feat: Show hidden directories in browser (#1288)
## Summary

* **What is the goal of this PR?** Add setting to display hidden files /
directories in filebrowser / web file browser
* **What changes are included?**

## 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 >**_
2026-03-18 09:57:58 -05:00
jpirnay 79b54b3a75 fix: Init lastSleepImage (edge case) (#1360)
## Summary

* **What is the goal of this PR?** fix edge case for definition

## Additional Context

If loadFromFile() returns false (no state file exists — first boot, or
SD missing), lastSleepImage is never set and contains garbage.
[SleepActivity.cpp:83] then uses it in a while comparison to avoid
repeating the same image. The JSON path (doc["lastSleepImage"] |
(uint8_t)0) handles it, but only if the file exists.

---

### 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 **_
2026-03-11 18:37:13 -05:00
jpirnay 11ca208ec2 chore: change label while on settings tab actions (#1325)
## Summary

* **What is the goal of this PR?** The "Toggle" label while on the "tab"
actions of settings screen was misleading. Will show "Select" now ,
while "Toggle" remains in place for all 'real' settings

* **What changes are included?**

## Additional Context
<img width="240" alt="1"
src="https://github.com/user-attachments/assets/dc198716-0aad-4c75-96fe-52595625e69d"
/>
<img width="240" alt="2"
src="https://github.com/user-attachments/assets/85ce5368-801c-489d-aa94-51f126c3ddc8"
/>


---

### 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 **_
2026-03-11 18:36:34 -05:00
7a28f90dad fix: back button in settings returns to tab bar first (#1354)
## Summary
- Pressing Back while browsing settings within a category now jumps
focus to the category tab bar
- Pressing Back again from the tab bar exits settings to home
- Previously, Back always exited directly to home regardless of scroll
position

Closes #797

## Test plan
- [ ] Scroll deep into a settings category → press Back → tab bar is
focused
- [ ] Press Back again from tab bar → exits to home screen
- [ ] Use category switching (continuous hold) → still works as before
- [ ] Settings are saved on exit (not on tab-bar jump)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: bkb <bkb@arcnode.xyz>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 18:35:48 -05:00
Adrian Wilkins-Caruana f1e9dc7f30 perf: font-compression improvements (#1056)
## Purpose

This PR includes some preparatory changes that are needed for an
upcoming performant CJK font feature. The changes have no impact on
render time and heap allocation for latin text. **Despite this, I think
these changes stand on their own as a better font
compression/decompression implementation.**

## Summary

- Font decompressor rewrite: Replaced the 4-slot LRU group cache with a
two-tier system — a page buffer (glyphs prewarmed before rendering
begins) and a hot-group fallback (last decompressed group retained for
non-prewarmed
  glyphs). 
- Byte-aligned compressed bitmap format: Glyph bitmaps within compressed
groups are now stored row-padded rather than tightly packed before
DEFLATE compression, improving compression ratios by making identical
pixel rows produce
identical byte patterns. Glyphs are compacted back to packed format on
demand at render time. Reduces flash size by 155 KB.
- Page prewarm system: Added `Page::collectText` and
`Page::getDominantStyle` to extract per-style glyph requirements before
rendering, and `GfxRenderer::prewarmFontCache` to pre-decompress only
the groups needed for the dominant style
   — eliminating mid-render decompression for the common case.
- UTF-8 robustness fixes: `utf8NextCodepoint` now validates continuation
bytes and returns a replacement glyph on malformed input;
`ChapterHtmlSlimParser` correctly preserves incomplete multi-byte
sequences across word-buffer flush
  boundaries rather than splitting them.

---

### 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**_ Architecture and
design was done by me, refined a bit by Claude. Code mostly by Claude,
but not entirely.
2026-03-11 21:05:46 +01:00
Uri Tauber cd508d27d5 refactor: reader utils (#1329)
## Summary

Extract shared reader utilities (`ReaderUtils.h`) to reduce duplication
across `EpubReaderActivity`, `TxtReaderActivity`, and (upcoming)
`MarkdownReaderActivity`.

  Utilities extracted:

   - `applyOrientation()` — orientation switch logic
   - `detectPageTurn()` — page navigation input detection
   - `renderAntiAliased()` — grayscale anti-aliasing pass
   - `displayWithRefreshCycle()` — refresh mode cadence
   - `GO_HOME_MS` — back button timing constant

  ## Impact

Flash: 32 bytes saved (6006441 → 6006409 bytes). Minimal immediate gain,
but meaningful once markdown reader and future reader types share these
functions.

Code quality: Eliminates ~100 lines of duplicated logic spread across
multiple files. All readers now follow the same patterns for
orientation, input handling, and rendering.

  ## Rationale

This refactor is preparation for markdown support, which requires
identical input and rendering logic. Instead of copy-pasting these
patterns a third time, all readers now share a single, tested
implementation. Future reader types can reuse `ReaderUtils` without
duplication.

  ---

  ## AI Usage

Did you use AI tools to help write this code? YES Claude extracted the
code, under my guidance. Tested on my device and seems to work fine.
2026-03-08 11:45:54 -05:00
Uri Tauber 4d22256745 feat: footnote anchor navigation (#1245)
## 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
2026-03-06 21:10:45 +03:00
Zach Nelson c3f1dbfa09 perf: Avoid creating strings for file extension checks (#1303)
## 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**_
2026-03-05 10:12:22 -06:00
jpirnay ce0b439aa3 feat: User setting for image display (#1291)
## 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 >**_
2026-03-03 09:59:06 -06:00
Xuan-Son Nguyen 307a6608f0 chore: remove rendundant xTaskCreate (#1264)
## 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**
2026-03-02 13:30:55 +01:00
jpirnay ef02737c89 feat: Prefer ".sleep" over "sleep" for custom image directory (#948)
## 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_
2026-03-02 13:28:14 +01:00
Arthur Tazhitdinov f0a549b680 refactor: rename MyLibrary to FileBrowser (#1260)
## 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 >**_
2026-03-02 11:00:53 +01:00
Zach Nelson 80d1856330 perf: Removed unused ConfirmationActivity member (#1234)
## 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**_
2026-03-01 10:04:52 -06:00
Lev Roland-Kalb 76681201bf fix: Hide unusable button hints when viewing empty directory (#1253)
## 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**_
2026-03-01 13:10:25 +11:00
jpirnay 04242fa221 refactor: Simplify new setting introduction (#1086)
## 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>**_
2026-03-01 12:54:58 +11:00