Compare commits

...

65 Commits

Author SHA1 Message Date
Zach Nelson fea4d1b81b Merge branch 'master' into fix/nested-block-styles
# Conflicts:
#	lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
2026-04-20 15:24:45 -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 Mitchell 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
Ang e8645ed92e docs: fix typos (#1705)
## Summary

Fix typos found via `codespell -S
*.txt,*.yaml,generate_kerning_ligature_epub.py -L
currenty,flate,ser,localy,logicaly,ans,clen,portugues,notin,curren`

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

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-20 09:12:40 +03:00
Zach Nelson 64f5ef018a feat: Support for proportional numeral spacing (#1414)
## Summary

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

Reading a book with frequent numbers, I noticed that the spacing between
numeral glyphs was strangely large. This was because Bookerly and Noto
Sans default to tabular figures, where every digit gets an identical
advance width. This is designed for column alignment in spreadsheets,
but in rendering prose it produces visually wide gaps between digits.

This change adds a `--pnum` flag to fontconvert.py that applies the
font's OpenType `pnum` (proportional numerals) feature during
conversion. When active, the converter:
- Parses the GSUB table for pnum SingleSubst lookups
- Resolves substitute glyph indices via fonttools' glyph order
- Loads the proportional alternate glyphs instead of the tabular
defaults
- Includes substitute glyph names in kern pair extraction, so kerning
data that references proportional alternates is captured

Bookerly's proportional alternates also carry digit-digit and
digit-punctuation kerning that the tabular glyphs lack (e.g., at 16pt
7->4 at -1.69px, 7->. at -2.31px, 7->1 at +1.00px).

Noto Sans gains proportional advances but no new kerning (its
proportional glyphs have no kern class data in the font).

OpenDyslexic is unaffected. Its `cmap` already points to proportional
glyphs, so `--pnum` is a no-op. `--pnum` is intentionally omitted from
OpenDyslexic in the build script for deliberately uniform digit spacing
as an accessibility choice.

UI fonts (Ubuntu, notosans_8) also omit `--pnum` to preserve tabular
alignment for page numbers, battery percentages, etc.

| Before | After |
| -- | -- |
| <img
src="https://github.com/user-attachments/files/26042238/screenshot-31673.bmp"
width="300" /> | <img
src="https://github.com/user-attachments/files/26042241/screenshot-124075.bmp"
width="300" /> |

---

### 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-18 17:03:04 -05:00
Zach Nelson 3cdfc6c781 chore: Improved Italian translations (#1685)
## Summary

Improved Italian translations provided by @alan0ford, closes #1578.

---

### 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-18 16:30:46 -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
Justin Mitchell fedcb2f53d fix: boot looping when opening large XTC files (#1648)
Opening XTC files with a high page count (e.g. *The Magic Mountain* at
4,187 pages) causes an immediate `abort()` crash and reboot loop. The
device becomes unusable until the book is removed from the SD card.

**Crash log:**
```
abort() was called at 0x4214a5fb on core 0
```

### Root cause

During `XtcParser::open()`, the parser calls
`m_pageTable.resize(pageCount)` to load the entire page table into RAM.
Each `PageInfo` entry is 16 bytes, so:

- 4,187 pages x 16 bytes = **66,992 bytes (~65KB)** as a single
contiguous heap allocation

On the ESP32-C3 with ~380KB total RAM (no PSRAM), this allocation fails
after firmware, fonts, and the activity system are already loaded.
Because the firmware is compiled with `-fno-exceptions`, the failed
`new` inside `std::vector::resize()` calls `abort()` instead of
throwing.

This affects any XTC file with roughly 3,000+ pages, depending on heap
state at the time of loading.

## Solution

Replace the bulk page table allocation with on-demand reads from the SD
card. Instead of loading all page table entries into a vector at file
open, we now:

1. Read only the **first** page table entry at open time (to get default
page dimensions)
2. Read a **single** 16-byte entry from the SD card each time a page is
loaded

This reduces page table memory usage from `pageCount * 16` bytes to
**zero bytes**, regardless of how many pages the file contains.

### Changes

| File | What changed |
|------|-------------|
| `XtcParser.h` | Removed `std::vector<PageInfo> m_pageTable`. Added
`readPageTableEntry()` for on-demand reads. |
| `XtcParser.cpp` | Replaced `readPageTable()` with
`readFirstPageInfo()`. Updated `getPageInfo()`, `loadPage()`, and
`loadPageStreaming()` to seek and read individual entries from the file.
|

## Trade-offs

### Performance

Each page turn now requires one additional SD card seek + 16-byte read
to look up the page table entry before reading the page data itself.

- SD card sequential read latency: ~0.1-0.5ms for a 16-byte read
- E-ink full display refresh: ~1,000-2,000ms

I personally can't see any performance difference while reading and the
trade off of not boot looping seems to make this well worth it.

### Memory

| Metric | Before | After |
|--------|--------|-------|
| Page table RAM (4,187 pages) | ~65KB | 0 bytes |
| Page table RAM (1,000 pages) | ~16KB | 0 bytes |
| Page table RAM (max 65,535 pages) | ~1MB (impossible) | 0 bytes |
2026-04-18 14:22:34 -05:00
Zach Nelson a888978f95 chore: Clarify X3 RTC in SCOPE.md (#1687)
## Summary

Clarify that X3 device has a reliable RTC chip, so clock feature can be
in-scope depending on device.

---

### 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-17 18:39:06 -05:00
Jensen Kuras 23f60a3407 chore: Updating sleep screen dimensions for X3 (#1688)
## Summary

Updating the docs so it has the accurate dimensions for sleep screens on
the X3 and the X4.

---

### 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-17 18:09:21 -05:00
KymAndriy 2c5a47f9d7 refactor: change ukrainian translation to adaptation (#1684)
## Summary

* **What is the goal of this PR?** 
Make adaptation instead of pure translation.

* **What changes are included?**
Fix issues where text could not fit in line.
Fix didn't cover `KOReader` settings

**Additional Reviever**
@mirus-ua

---

### 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: Kym_Adnriy <kym_andr@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-17 14:29:16 -05:00
Zach Nelson ce1756e36f refactor: Added shared XML parser teardown helper (#1438)
## Summary

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

Added `destroyXmlParser()` helper to replace the repeated 4-line parser
cleanup block (stop, clear callbacks, free, null) that was copyied
across 6 XML parser files.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-04-16 19:42:05 -05:00
Jan 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
zgredex 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 0c5dee3c62 refactor: Refactor drawArc / fillArc for faster execution (#1540)
## Summary

* **What is the goal of this PR?** Replace the o(r^2) routines with a
o(r) scanline logic - will make fillArc roughly 50% faster and drawArc
roughly 5x faster. Still probably unnoticeable.
* **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-04-16 22:57:03 +03:00
Zach Nelson 187066c714 thx @coderabbitai 2026-04-16 01:06:30 -05:00
Zach Nelson 3e9a616988 fix: reset empty block style after image consumes wrapper spacing 2026-04-16 01:04:45 -05:00
Zach Nelson 13ee6df14f Bump SECTION_FILE_VERSION 2026-04-16 00:53:29 -05:00
Zach Nelson 10de3409d1 clang-format 2026-04-16 00:50:36 -05:00
Zach Nelson 944b9efc46 Simpler chained calls 2026-04-16 00:50:27 -05:00
Zach Nelson 86706e061c Correct CSS vertical margin merging 2026-04-16 00:46:22 -05:00
Zach Nelson ea55e6ec47 fix: remove empty-block restore that wiped deposited bottom margins 2026-04-16 00:44:56 -05:00
Zach Nelson 8b940db781 Combine approaches with #1454 2026-04-16 00:39:35 -05:00
Jon Vexler 81ae9dd779 feat: smooth battery percentage for x4 (#1635)
## Summary

Battery percentage is calculated from the voltage, which is not totally
stable. We smooth the battery percentage using a moving average.

## Additional Context
issue discussion:
https://github.com/crosspoint-reader/crosspoint-reader/issues/1444

---

### 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-16 00:28:43 -05:00
Zach Nelson 206bfe760e Merge branch 'master' into fix/nested-block-styles 2026-04-15 23:36:13 -05:00
jpirnay 40e4c96906 refactor: replace picojpeg with JPEGDEC for cover art conversion (#1517)
## Summary

- Removes the vendored `picojpeg` library and rewrites
`JpegToBmpConverter` to use the already-present `JPEGDEC` (bitbank2)
dependency
- Eliminates the redundancy of having two JPEG decoders in the firmware
- All BMP output (headers, fixed-point scaling, Atkinson/Floyd-Steinberg
dithering) is identical to before — cached cover BMPs are unaffected

## Size impact

| | Before | After | Delta |
|---|---|---|---|
| Flash | 5,754,089 bytes (87.8%) | 5,744,777 bytes (87.7%) | **−9,312
bytes** |
| RAM | 95,212 bytes (29.1%) | 92,852 bytes (28.3%) | **−2,360 bytes** |

## Implementation notes

- `bmpDrawCallback` receives MCU-sized blocks from JPEGDEC (up to 16
rows × MCU-width), accumulates them into a pre-allocated `mcuBuf`, and
applies the same scaling + dithering logic once each MCU row is complete
- File I/O uses a file-scope static `FsFile*` (safe in single-threaded
embedded context) via JPEGDEC's open/read/seek callbacks — same pattern
as `JpegToFramebufferConverter`
- Added a 52 KB free-heap guard before allocating the JPEGDEC object
(~17 KB)
- `lib/picojpeg/` deleted (2,087 lines of C removed)

## Test plan

- [ ] Build compiles without warnings
- [ ] Cover art BMP cache regenerates correctly for EPUB books
- [ ] Home screen thumbnails (1-bit BMP path) render correctly
- [ ] Custom-size thumbnails (`jpegFileToBmpStreamWithSize`) render
correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-15 23:20:24 -05:00
Stefan Blixten Karlsson 80772ff6b8 fix: footnote link text (#1666)
## Summary

Previouls where ALL whitespace (and square brackes) removed from the
footnote link text, however some link texts are multiworded, like "`turn
to 252`" which were truncated into "`turnto252`", (an example from the
first book "Flight from the Dark" of the Lone Wolf book series by Joe
Dever, see [link](https://www.projectaon.org/en/Main/FlightFromTheDark))

* This change will only remove whitespaces from the beginning and end of
the string
so "` [ 12 ] `" will become "`12`" just like before, and "` turn to 252
`" will become "`turn to 252`".

---

### 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-15 21:47:10 -05:00
Stefan Blixten Karlsson 57fc6555f2 fix: missing swedish translations (#1667)
## Summary

* Add missing swedish translations

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-04-15 19:05:09 -05: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
jpirnay 45cd00889e chore: clang-format.fix.ps1 script: Add .venv to list of path exclusions (#1515)
## Summary

* **What is the goal of this PR?** Dont format files in .venv directory
* **What changes are included?**

## Additional Context

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

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< YES | PARTIALLY | NO
>**_
2026-04-14 18:50:37 -05: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 1bd7a1de67 refactor: Deduplicate battery drawing code and fix Lyra charging indicator (#1437)
## Summary

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

Following up on #1427:
- Extracted shared battery drawing logic, including lightning bolt, to
reduce duplication.
- In Lyra with segmented battery the lightning bolt was hard to see, so
when charging Lyra now uses a solid battery fill.

---

### 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 17:05:06 -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
Zach Nelson 075ad7d021 fix: Use font metrics for combining mark positioning (#1310)
## Summary

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

Combining diacritical marks (U+0300–U+036F) were positioned using a
heuristic that centered them at the midpoint of the base glyph's
**advance width**. This worked acceptably for Bookerly but produced
visibly off-center marks for Noto Sans due to a fundamental difference
in how the two fonts design their combining mark metrics.

Builds on the work of #1037.

## The problem

The two built-in body fonts encode combining mark `left` offsets with
very different conventions:

| Mark | Bookerly `left` | Noto Sans `left` |
|---|---|---|
| U+0301 (acute) | -2 | -10 |
| U+0300 (grave) | -5 | -15 |
| U+0302 (circumflex) | -5 | -5 |
| U+0323 (dot below) | -2 | -11 |

Noto Sans uses large negative `left` values because its marks are
designed for placement at the post-advance cursor position, with `left`
pulling the bitmap back over the base glyph. Bookerly uses small offsets
because its marks sit closer to the glyph origin. The old `advance/2`
centering split the difference poorly — it happened to land close to
correct for Bookerly but placed Noto Sans marks roughly 6px left of
center on a typical lowercase letter.

There was also a bug in the vertical gap heuristic. It unconditionally
computed a `raiseBy` value to prevent above-baseline marks from
colliding with tall base glyphs, but it applied the same logic to
**below-baseline** marks like cedilla (U+0327), dot below (U+0323), and
ogonek (U+0328). For those marks, the math produced a large positive
raise (e.g., 24px for dot-below on 'a'), launching them above the
x-height instead of keeping them below the baseline.

## The fix

**Horizontal positioning**: Instead of centering at `advance/2`, align
the mark bitmap's visual midpoint directly over the base glyph bitmap's
visual midpoint. This uses the base glyph's actual `left` and `width`
rather than its advance width, producing correct results regardless of
how the font encodes its mark offsets.

**Vertical positioning**: The raise heuristic now checks `markTop -
markHeight > 0` and skips below-baseline marks entirely, leaving them at
their font-designed position.

**Consolidation**: The shared math is extracted into two `constexpr`
helpers (`combiningMark::centerOver` and
`combiningMark::raiseAboveBase`) in `EpdFontData.h`, eliminating the
previously triplicated inline calculations across `drawText`,
`drawTextRotated90CW`, and `getTextBounds`. The `MIN_COMBINING_GAP_PX`
constant is also centralized as `combiningMark::MIN_GAP_PX`.

| Before | After |
| -- | -- |
| <img
src="https://github.com/user-attachments/files/25752257/before-noto.bmp"
width="250" /> | <img
src="https://github.com/user-attachments/files/25752258/after-noto.bmp"
width="250" /> |
| <img
src="https://github.com/user-attachments/files/25752259/before-bookerly.bmp"
width="250" /> | <img
src="https://github.com/user-attachments/files/25752260/after-bookerly.bmp"
width="250" /> |

---

### 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 to analyze
differences between Noto Sans and Bookerly font metrics**_

---------

Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
2026-04-14 14:35:50 -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
zgredex 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
Russell Neches cc23aaa9c2 docs: Update README with firmware flashing instructions (#1654)
Added instructions for flashing firmware using esptool.

## Summary

Instructions for flashing a specific firmware from the terminal using
`esptool.py`.

## Additional Context

Not everyone has Chrome or Chromium installed. An alternative to the
web-based firmware utility shouldn't be hard to find.

---

### AI Usage

Did you use AI tools to help write this code? _**PARTIALLY**_ : `dmesg`
command analog for MacOS
2026-04-13 13:50:31 -05:00
Diana 05f8e6e12d fix: webserver /delete API backward compatibility (#1475)
## Summary

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

This fixes #682 by restoring API compatibility for external users, eg
deleting on-device books in bulk via calibre using the crosspoint plugin
works now.

* **What changes are included?**

The `/delete` webserver API now accepts the old `path` argument and maps
it to a JSON array element. It is made an error to provide both
arguments.

## Additional Context

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

I have compiled, flashed, and tested this on master at commit
0245972132. Before this change, calibre
cannot delete books, either individually or in bulk.

After this change, it can do so successfully.

---

### 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 08:59:07 +03: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
CSCMe 9bc5111c77 fix: increase loadable epub size (#1638)
## Summary

* **What is the goal of this PR?** Slightly increase the OOM limit when
loading epubs
* **What changes are included?** Switched vectors for parsing epubs to
deques, allowing use of more memory

## Additional Context

* Increases loadable epub size from 2000+ chapter/ToC entries loadable
to 5000+ chapter/ToC entries
* #1574, but without the complicated stuff

---

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

### Testing
| Commit | Book | Time |
|----------|------|-------|
| 83cd96bc2f | 1000.epub | ~30 sec |
| 83cd96bc2f | 5000.epub | crash |
| PR | 1000.epub | ~29 sec |
| PR | 5000.epub | ~2 min 20 sec |

=> No actual loading time regressions

[tested_epubs.zip](https://github.com/user-attachments/files/26645243/tested_epubs.zip)
2026-04-12 18:41:28 -05:00
bdeshi 9c11f3e4a2 feat: enable manual screen refresh on power button short press (#1626)
## Summary

Adds an option to allow manual screen refresh on power button short
press.

## Additional Context

there's an option to refresh the screen after a set number of pages. but
sometimes a manual refresh is needed to clear up stale ink pixels. this
works everywhere both in and out of reading mode.

resolves #550 

---

### 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**_, to
understand the project structure.
2026-04-12 22:31:26 +03:00
rxmmah 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 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
Justin Mitchell 83cd96bc2f fix: Wild pointer crash in JPEGDEC MCU_SKIP handling (#1627)
Adds a PlatformIO pre-build script to patch JPEGDEC library. When
decoding progressive JPEGs with AC coefficients, MCU_SKIP (-8) causes
array index 0xFFFFF8, creating a wild pointer ~33MB past the sMCUs
array. The patch redirects pMCU to sMCUs[0] when MCU_SKIP is active,
preventing store-access faults while maintaining correct behavior for
JPEG_SCALE_EIGHTH decoding. Devices with larger framebuffers (like the
x3) (792×528 = 52,272 bytes vs 800×480 = 48,000 bytes) have less free
heap, shifting the allocation and changing where the wild pointer lands.

Commit 8628297 guarded the DC coefficient write (pMCU[0]) with if (iMCU
>= 0), which prevents crashes for progressive JPEGs whose first scan is
DC-only (iScanEnd == 0). However, if the first scan includes AC
coefficients (iScanEnd > 0), the AC decode loop still writes through the
wild pointer and crashes.
2026-04-10 21:13:59 +01:00
thehijacker 14ec53a335 feat: Added Slovenian translation (#1551)
Adde

## Summary

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

Adding Slovenian translation

* **What changes are included?**

Just new slovenian.yaml file with translated strings

## Additional Context

I kindly ask to include it in next release.

---

### 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: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com>
2026-04-10 11:00:32 -05:00
Zach Nelson 5ba85290ab refactor: Deduplicated BMP header writing in Xtc (#1439)
## Summary

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

Replaced manual 1-bit BMP header logic in `Xtc::generateCoverBmp()` and
`Xtc::generateThumbBmp()` with calls to the existing `createBmpHeader()`
utility. Added a `BmpRowOrder` enum and param to support the top-down
row order of XTC cover images.

---

### 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-10 00:35:43 +03:00
Uri Tauber 104f391a29 fix: two small memory leaks (#1628)
## Summary

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

Fix two issues pointed out by `CodeRabbit` in #1433:

1. Free the output buffer in `readFileToMemory` on early error paths
(prevents memory leaks).
2. Check `out.write()` return value in the STORED path (same as in the
DEFLATED path).

I can confirm both issues were real (not hallucinations). Both fixes are
minimal — no behavior change on success.

---

### AI Usage

Did you use AI tools to help write this code? _**< NO >**_
2026-04-09 22:16:19 +03:00
Zach Nelson b3b43bb373 refactor: RAII scoped open/close for ZipFile (#1433)
## Summary

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

Added `ScopedOpenClose` RAII guard to eliminate repetitive
`wasOpen`/`close()` boilerplate across all ZipFile methods.

---

### 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-09 21:28:31 +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
Justinian 825ef56ad8 feat: X3 grayscale antialiasing improvements (#1607)
## Summary
Improves text antialiasing quality on the Xteink X3 (SSD1677) display to
bring it closer to X4 rendering quality. Addresses white lines through
letter strokes and ghosting artifacts during page turns and screen
transitions.

## Changes

### Display Driver (open-x4-sdk)
- Dedicated X3 grayscale LUTs with tuned VDL drive strengths for dark
gray (2 time units) and light gray (3 time units), with active GND hold
on non-gray transitions to prevent floating source crosstalk
- Tight scan timing: TP2/TP3 reduced to 1 (total gate-on 7 units vs 17),
minimizing parasitic charge leakage that caused white lines through
letter strokes
- Fast diff BB reinforcement: Added mild VDH reinforcing pulse to
lut_x3_bb_full so black pixels are actively driven during differential
refreshes, clearing gray residue/ghosting
- displayGrayBuffer() updated to use the dedicated gray LUT bank instead
of full refresh LUTs for X3

### Rendering Pipeline
- Re-enabled light gray rendering for X3 text and images, now safe with
dedicated gray LUTs providing proper 4-level gray
- Removed isLightGrayRestricted() gating that was limiting X3 to 3-level
gray
- Runtime display dimensions in DirectPixelWriter and ScreenshotUtil,
replaced hardcoded constants with runtime getters to support X3 792x528
resolution

### Note
The open-x4-sdk submodule references a commit on
juicecultus/community-sdk. A corresponding PR to
open-x4-epaper/community-sdk should be merged first so the submodule ref
resolves on upstream.

## Testing
Tested on physical X3 hardware. White lines through letters
significantly reduced, in-book ghosting improved via BB reinforcement,
antialiasing visually closer to X4 quality.

## AI Disclosure
Yes, AI was used to assist with the development of these changes.

---------
2026-04-08 21:58:59 -04: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
martin brook b898d53f7b chore: drop JPEGDEC patch in favour of upstream fix (#1465)
## Summary

The progressive JPEG fixes (AC table skip, MCU_SKIP guard) from PR #1136
have been fixed upstream in bitbank2/JPEGDEC@8628297. Pin to that commit
and remove the pre-build patch script.

## Additional Context

Tested on Strange Pictures epub

---

### 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 09:22:25 +01:00
CSCMe c656673b9a refactor: logPrintf and predefined log level strings (#1546)
## Summary

* More consistent string formatting in logPrintf
* Moved [ and ] from log level strings into new format string
* Behaviour change: Early exit if user string format fails

## Additional Context

* Should not have performance implications, debug monitor etc work as
before
* Clamp may be unnecessary due to information snprintf currently never
being able to exceed max buffer length, but not having it would bug me

---

### 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, to reason
about correctness**_
2026-04-07 22:25:08 -05:00
Zach Nelson 1398aeb1ed fix: Use differential rounding for consistent inter-glyph spacing (#1413)
## Summary

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

A tweak to the fixed-point x-advance and kerning calculations to ensure
that the spacing between any two glyphs is always calculated
consistently.

I noticed that sometimes I'd see common character pairs like "oo" more
than once on a page, and the distance between the two snapped to
different pixels depending on the running accumulated error for the line
of text.

This change uses a differential rounding approach where each glyph's
x-advance plus the kerning relative to the next glyph are combined in
fixed-point precision, then snapped to a pixel to draw the next glyph.
This results in a consistent inter-glyph spacing any time the same two
glyphs show up adjacent to each other, regardless of the accumulated
error across the line.

---

### 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-07 22:20:50 -05:00
Justin Mitchell 6cd19f5619 fix: epub images not rendering correctly on x3 (#1572)
Replace hardcoded DISPLAY_WIDTH, DISPLAY_HEIGHT, and DISPLAY_WIDTH_BYTES
constants with runtime values from display object to support multiple
device models (X3 and X4) with different screen dimensions.
2026-04-07 19:16:59 -05:00
Mirus cff3e12a0a fix: Update Ukrainian translations for footnotes (issue 1409) (#1585)
## Summary

* **What is the goal of this PR?** 
Solve the issue
https://github.com/crosspoint-reader/crosspoint-reader/issues/1409
* **What changes are included?**
Updated translation for footnotes

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

Did you use AI tools to help write this code? _**NO**_
2026-04-07 09:21:32 -05:00
Andrei Ignatev fa3c7d96a0 fix: correct Russian auto-turn translations (#1566)
## Summary

* **What is the goal of this PR?**
Fix inaccurate Russian UI text for the reader auto-turn feature and make
the affected Russian labels consistent with how adjacent UI strings are
formatted.

* **What changes are included?**
Updated Russian translations in `lib/I18n/translations/russian.yaml`:
- changed `Auto Turn` text from wording that implied screen rotation to
wording that means automatic page turning
- adjusted a few Russian prefix/separator strings to include spacing
where the UI concatenates labels with dynamic values

## Screenshots

| Before| After |
|--------|--------|
| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/db416dd2-6174-4a46-bd3a-6f52d1cc01cb"
/>| <img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/dd9055e0-d4f1-459d-bd40-60fc4970d458"
/>|



---

### 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 09:21:12 -05:00
Zach Nelson f429f9035c refactor: Use default member initializers for JpegContext and PngContext (#1435)
## Summary

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

Replace verbose constructor initializer lists with in-class default
member initializers in JpegContext and PngContext

---

### 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 09:13:21 -05: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
Zach Nelson 1c13331189 fix: Support hyphenation for EPUBs using ISO 639-2 language codes (#1461)
## Summary

EPUBs that use ISO 639-2 three-letter language codes in their
`dc:language` metadata (e.g. `<dc:language>eng</dc:language>`) got no
hyphenation. The hyphenator registry only matched ISO 639-1 two-letter
codes (`"en"`, `"fr"`, etc.), so `"eng"` produced a null hyphenator and
every word in the book was treated as unhyphenatable.
Added a normalization step in `hyphenatorForLanguage` that maps ISO
639-2 codes (both bibliographic and terminological variants) to their
two-letter equivalents before the registry lookup.

Discovered via *Project Hail Mary* (Random House), which uses
`<dc:language>eng</dc:language>`.

---

### 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-07 00:30:24 +01:00
Dave Allie 8288d63b8c Bump section cache version 2026-04-06 16:26:59 +10:00
Dave Allie 471fef703b Apply existing block margin to image 2026-04-06 16:26:36 +10:00
Dave Allie de1f649b79 Track block style stack for nested styles 2026-04-06 16:25:24 +10:00
169 changed files with 102042 additions and 101006 deletions
+20 -2
View File
@@ -12,7 +12,7 @@ Xteink firmware. It aims to match or improve upon the standard EPUB reading expe
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
customisation. The **Xteink X4** is an affordable, e-paper device, however the official firmware remains closed.
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truely unlock the device's
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truly unlock the device's
potential.
CrossPoint Reader aims to:
@@ -68,6 +68,24 @@ back to the other partition using the "Swap boot partition" button here https://
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
### Command line (specific firmware version)
1. Install [`esptool`](https://github.com/espressif/esptool) :
```bash
pip install esptool
```
2. Download the `firmware.bin` file from the release of your choice via the [releases page](https://github.com/crosspoint-reader/crosspoint-reader/releases)
3. Connect your Xteink X4 to your computer via USB-C.
4. Note the device location. On Linux, run `dmesg` after connecting. On MacOS, run :
```bash
log stream --predicate 'subsystem == "com.apple.iokit"' --info
```
5. Flash the firmware :
```bash
esptool.py --chip esp32c3 --port /dev/ttyACM0 --baud 921600 write_flash 0x10000 /path/to/firmware.bin
```
Change `/dev/ttyACM0` to the device for your system.
### Manual
See [Development](#development) below.
@@ -161,7 +179,7 @@ If you are new to the codebase, start with the [contributing docs](./docs/contri
If you're looking for a way to help out, take a look at the [ideas discussion board](https://github.com/crosspoint-reader/crosspoint-reader/discussions/categories/ideas).
If there's something there you'd like to work on, leave a comment so that we can avoid duplicated effort.
Everyone here is a volunteer, so please be respectful and patient. For more details on our goverance and community
Everyone here is a volunteer, so please be respectful and patient. For more details on our governance and community
principles, please see [GOVERNANCE.md](GOVERNANCE.md).
### To submit a contribution:
+6 -2
View File
@@ -27,6 +27,12 @@ usability over "swiss-army-knife" functionality.
* **Language Support:** E.g. Support for multiple languages both in the reader and in the interfaces.
* **Reference Tools:** E.g. Local dictionary lookup. Providing quick, offline definitions to enhance comprehension
without breaking focus.
* **Clock Display (device dependent):**
| Device | Scope |
| -- | -- |
| X3 | The X3 uses a dedicated DS3231 RTC, which maintains accurate time across sleep cycles and can be treated as a reliable wall clock. |
| X4 | The X4 relies on the ESP32-C3's internal RTC, which drifts significantly during deep sleep. NTP sync could correct this, with an appropriate user experience around connecting to the internet on wake or on demand. This causes some tension with the **Active Connectivity** section below, so please open a discussion about this UX if it's a feature you would find useful. |
### Out-of-Scope
@@ -43,8 +49,6 @@ usability over "swiss-army-knife" functionality.
*These features align with CrossPoint's goals but are impractical on the current hardware or produce poor UX.*
* **Clock Display:** The ESP32-C3's RTC drifts significantly during deep sleep; making the clock untrustworthy after any sleep cycle. NTP sync could help, but CrossPoint doesn't connect to the internet on every boot.
* **PDF Rendering:** PDFs are fixed-layout documents, so rendering them requires displaying pages as images rather than reflowable text — resulting in constant panning and zooming that makes for a poor reading experience on e-ink.
## 3. Idea Evaluation
+2 -1
View File
@@ -336,7 +336,8 @@ To use custom sleep images, set the sleep screen mode to **Custom** or **Cover +
> [!TIP]
> For best results:
> - Use uncompressed BMP files with 24-bit color depth
> - Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
---
+1
View File
@@ -97,6 +97,7 @@ $exclude = @(
'lib\Epub\Epub\hyphenation\generated'
'lib\uzlib'
'.pio'
'.venv'
)
function Test-Excluded($fullPath) {
+1
View File
@@ -26,6 +26,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an
## Italian
- [andreaturchet](https://github.com/andreaturchet)
- [fragolinux](https://github.com/fragolinux)
- [alan0ford](https://github.com/alan0ford)
## Russian
- [madebyKir](https://github.com/madebyKir)
+22 -17
View File
@@ -15,11 +15,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
return;
}
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
int lastBaseX = startX;
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
int lastBaseLeft = 0;
int lastBaseWidth = 0;
int lastBaseTop = 0;
constexpr int MIN_COMBINING_GAP_PX = 1;
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
uint32_t cp;
uint32_t prevCp = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
@@ -31,24 +31,29 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
const EpdGlyph* glyph = getGlyph(cp);
if (!glyph) {
prevCp = 0;
// Keep cursor movement stable when a base glyph is missing, but don't attach subsequent
// combining marks to stale base metrics.
if (!isCombining) {
lastBaseX += fp4::toPixel(prevAdvanceFP); // flush pending advance before resetting
prevCp = 0;
prevAdvanceFP = 0;
lastBaseLeft = 0;
lastBaseWidth = 0;
lastBaseTop = 0;
}
continue;
}
int raiseBy = 0;
if (isCombining) {
const int currentGap = glyph->top - glyph->height - lastBaseTop;
if (currentGap < MIN_COMBINING_GAP_PX) {
raiseBy = MIN_COMBINING_GAP_PX - currentGap;
}
}
const int raiseBy = isCombining ? combiningMark::raiseAboveBase(glyph->top, glyph->height, lastBaseTop) : 0;
if (!isCombining && prevCp != 0) {
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
}
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
const int glyphBaseX =
isCombining ? combiningMark::centerOver(lastBaseX, lastBaseLeft, lastBaseWidth, glyph->left, glyph->width)
: lastBaseX;
const int glyphBaseY = startY - raiseBy;
*minX = std::min(*minX, glyphBaseX + glyph->left);
@@ -57,10 +62,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
if (!isCombining) {
lastBaseX = cursorXPixels;
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
lastBaseLeft = glyph->left;
lastBaseWidth = glyph->width;
lastBaseTop = glyph->top;
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
prevAdvanceFP = glyph->advanceX; // 12.4 fixed-point
prevCp = cp;
}
}
+37 -4
View File
@@ -7,10 +7,12 @@
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
/// values (int8_t) share the same 4 fractional bits, so they can be freely
/// added into a single int32_t accumulator during text layout. The
/// accumulator is snapped to the nearest whole pixel only at render time,
/// which avoids the per-character rounding errors that plagued integer-only
/// layout.
/// added before snapping to whole pixels.
///
/// Rendering and measurement use "differential rounding": each glyph step
/// (previous advance + current kern) is combined in fixed-point and snapped
/// to a pixel as one unit. This guarantees identical character pairs always
/// produce the same pixel spacing, regardless of position on the line.
///
/// The helpers below eliminate the raw bit-shifts that would otherwise be
/// scattered across every layout / measurement call site.
@@ -28,6 +30,37 @@ constexpr int toPixel(int32_t fp) { return static_cast<int>((fp + HALF) >> FRAC_
constexpr float toFloat(int32_t fp) { return fp / static_cast<float>(1 << FRAC_BITS); }
} // namespace fp4
/// Helpers for positioning Unicode combining marks (U+0300 ff.) over a
/// preceding base glyph without GPOS anchor tables.
namespace combiningMark {
constexpr int MIN_GAP_PX = 1;
/// Compute the cursor-X at which to render a combining mark so its bitmap
/// is visually centered over the base glyph's bitmap.
constexpr int centerOver(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
return baseCursorPos + baseLeft + baseWidth / 2 - markWidth / 2 - markLeft;
}
/// Rotated-90CW variant of centerOver. In the rotated coordinate system
/// renderCharImpl uses (cursorY - left) instead of (cursorX + left), so
/// every left/width term inverts sign.
constexpr int centerOverRotated90CW(int baseCursorPos, int baseLeft, int baseWidth, int markLeft, int markWidth) {
return baseCursorPos - baseLeft - baseWidth / 2 + markWidth / 2 + markLeft;
}
/// For combining marks that sit entirely above the baseline, compute how many
/// pixels to raise the mark so there is at least MIN_GAP_PX between its bottom
/// edge and the top of the base glyph. Returns 0 for marks that extend to or
/// below the baseline (e.g. cedilla, dot-below, ogonek).
constexpr int raiseAboveBase(int markTop, int markHeight, int baseTop) {
if (markTop - markHeight <= 0) return 0;
const int gap = markTop - markHeight - baseTop;
return (gap < MIN_GAP_PX) ? (MIN_GAP_PX - gap) : 0;
}
} // namespace combiningMark
/// Fixed-point conventions used by EpdGlyph and EpdFontData:
/// advanceX: 12.4 unsigned fixed-point in uint16_t (use fp4::toPixel)
/// kernMatrix: 4.4 signed fixed-point in int8_t (use fp4::toPixel)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More