Commit Graph
490 Commits
Author SHA1 Message Date
Danila Yudin 929f290042 fix: close leaked resource handles (#2040) 2026-05-23 21:26:13 +03:00
Julia f39ba7037f fix(settings): preserve quick resume timeout preference (#2101)
## Summary

### **What is the goal of this PR?**
This fixes an unintended settings side effect when cycling the `Sleep
Screen` option through `Quick Resume`.

Previously, selecting `Sleep Screen = Quick Resume` globally forced
`Quick Resume on Timeout = ON` and left it enabled even after the user
toggled `Sleep Screen` to another option within the same settings
session. Now the auto-enable behavior is scoped to the Settings screen
session:

- If `Quick Resume on Timeout` was already `ON` when entering Settings,
it stays `ON`.
- If it was `OFF`, selecting `Sleep Screen = Quick Resume` temporarily
turns it `ON`.
- If the user then switches away from `Quick Resume`, it turns back
`OFF`.

### **What changes are included?**

- Removes the global logic that permanently forced `Quick Resume on
Timeout` to `ON` whenever `Sleep Screen` was set to `Quick Resume`, even
if it was just due to toggling through the options.
- Adds Settings-screen session tracking so `Quick Resume on Timeout` is
only auto-enabled while the user has `Sleep Screen = Quick Resume`.
- Restores `Quick Resume on Timeout` back to `OFF` when the user
switches away, but only if it was `OFF` when they entered Settings.
- Preserves existing `ON` timeout preferences.
- Same behavior applies to the web settings

## Additional Context

- Tested this on device and via the settings UI
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-21 21:06:51 -04:00
WuTofu 2dd491b62e refactor: unify book cache clearing for epub, txt, and xtc files (#1875) 2026-05-21 14:44:10 +03:00
Jeremy Klein 4ffc2a7e7e fix: sleep from a WiFi activity instead of silent-rebooting (#2092)
Unify the two splash-skip signals (RTC silent-reboot flag, SD
seamless-sleep flag) into one BootResume enum driving a single switch.
Storage unchanged; behavior-preserving apart from the fix.

Holding power to sleep from a WiFi activity (Font Download, OPDS, web
server, Calibre, KOReader sync) rebooted to home instead of sleeping.
goToSleep() runs the outgoing activity's onExit(), and those activities
call silentRestart() to clear heap fragmentation, so the heap-defrag
reboot fired before deep sleep could start.

enterDeepSleep() now latches deepSleepInProgress before goToSleep();
silentRestart()/silentRestartToReader() no-op while it's set. Deep sleep
is a full chip reset on wake, so it already clears the fragmentation the
reboot existed for.



Did you use AI tools to help write this code? partial
2026-05-21 00:03:46 -04:00
Julia c44555007b feat(settings): modify "Page as Sleep Screen" to "Quick Resume" options (#2089)
## Summary

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

Adds a clearer Quick Resume sleep-screen flow. The previous “Page as
Sleep Screen” behavior is now exposed as a dedicated `Sleep Screen >
Quick Resume `option, with the timeout-only behavior controlled by a
renamed `Quick Resume on Timeout `setting.

**What changes are included?**

- Adds `Quick Resume` as a new `Sleep Screen` option.
- Renames the old `Page as Sleep Screen` setting to `Quick Resume on
Timeout`.
- Changes that setting’s choices from `Never / After Timeout / Always`
to `OFF / ON`.
- Makes `Quick Resume on Timeout = ON` equivalent to the old `After
Timeout` behavior.
- Makes `Sleep Screen > Quick Resume` equivalent to the old `Always`
behavior.
- Automatically forces `Quick Resume on Timeout` to `ON` when `Sleep
Screen` is set to `Quick Resume`.
- Renames internal setting references from `seamlessSleepScreen` to
`quickResumeSleepScreen`.
- Updates translations for the renamed setting label.

**Additional Context**

- This is mostly a settings/labeling restructure around existing
behavior, not a new rendering path.
- The runtime quick-resume behavior still uses the existing saved
framebuffer / last-screen sleep flow.
- Review focus areas:
  - Sleep entry behavior from manual sleep vs timeout sleep.
- The automatic dependency where selecting `Sleep Screen > Quick Resume`
sets `Quick Resume on Timeout` to `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 >**_

---
**New `Quick Resume` option for `Sleep Screen` will automatically set
`Quick Resume on Timeout` to `ON`**:
<img width="480" height="800" alt="quick resume"
src="https://github.com/user-attachments/assets/94c553fd-a122-47a8-add9-f29694f55566"
/>

**Example where a different sleep screen setting like `Cover` can be
used in combination with the `Quick Resume on Timeout` setting**:
<img width="480" height="800" alt="cover + quick resume"
src="https://github.com/user-attachments/assets/dd18ce18-230b-4b78-808b-ac85f5e7d5d8"
/>
2026-05-20 22:43:48 -04:00
Vadim Kaushan d9aa5b4de1 fix: take orientation into account for border generation in ScreenshotUtil (#1977)
## Summary

Previously `ScreenshotUtil` used physical display size to draw a border
around the screen contents. Because of this, in landscape orientation
the border was shown as a broken square. This PR changes border drawing
to use logical screen size instead of a physical display size to take
orientation into account.

## Additional Context

* Tested on X4 in all 4 reading orientations. Behavior is now correct,
however it doesn't look perfect on my X4: the border is much closer to
the physical top side of the display than to the other sides. This might
be related to assembly variation during manufacturing, but it might as
well be related to the way a eink controller is connected to the display
(controller supports bigger display sizes, so an offset may be present).

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-20 15:48:06 -04:00
Jeremy Klein 3800179595 fix: wire through silent restart clear resume state with sdk (#2033)
## Summary

After a silent reboot, there was a small window where the esp32 would
listen for button presses but the full refresh would hold the event
loop. This gave a UX experience where the silent reboot had completed to
the home screen, a user taps select (at any time during the process),
and they find themselves unexpectedly in a book.

## Additional Context

This must land after
https://github.com/crosspoint-reader/community-sdk/pull/11 and will need
the submodule SHA changes included in. 

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-20 08:58:43 -04:00
Jeremy Klein 0b9d1a7d23 fix: keep wifi OTA off the heap floor (#2074)
OTA install streams the full 5.8MB image over a multi-minute TLS session
while wifi/LWIP already holds the big internal arena. Measured on
device, the arena bottomed out at ~7.7KB free with the largest
contiguous block down to ~2.4KB; for 80% of the download there wasn't
even a contiguous 8KB block. It finishes on a clean heap but tips into
OOM for anyone carrying more pre-OTA fragmentation.

Two avoidable drains, both in OtaUpdater:

- The esp_http_client RX/TX buffers were 8192/8192 on both the version
check and the install. RX only has to hold response headers (bodies
stream through the parser / OTA writer) and TX only carries our GET, so
trim both to 4096/1024. 4096 still fits the github->CDN redirect
headers; the 512 IDF default truncates them, which is why they got
oversized in the first place.

- installUpdate fired the progress callback every ~100ms perform
iteration, waking the render task on every tick. Its framebuffer work
fights the TLS session for the same arena, and epd can't really repaint
faster than a percent anyway. Throttle it to whole-percent changes.

On device, combined: floor 7.7KB -> 19KB, worstcase contiguous block
2.4KB -> 21KB, zero sub 8KB iterations across the whole download.

KOReaderSyncClient already uses small buffers; HttpDownloader is on the
Arduino HTTPClient stack with no equivalent knob, so neither changed.



Did you use AI tools to help write this code? partial, heap-exploration
assisted by Claude.
2026-05-20 08:46:49 -04:00
Jeremy Klein 75764bc6eb fix: 0 -> 1, even more deep-sleep fix (#2073)
setTimeout(0) on the serial could trigger a subtle but obnoxious
underflow.

Eat a milli, save a reset button.


Did you use AI tools to help write this code? no
2026-05-19 22:40:44 -04:00
Eloren1andClaude Sonnet 4.6 41e6e15229 feat: Seamless sleep/wake screens for displaying book pages during deep sleep (#2064)
<img width="605" height="454" alt="image"
src="https://github.com/user-attachments/assets/bfd84afe-3b58-436e-9a5d-539af3ec3d4e"
/>

Actualized "Last" sleep screen setting from previous PRs, rebranded as a
~~`Seamless Sleep`~~ `Page as Sleep Screen` option with more
improvements.


https://github.com/user-attachments/assets/59029ba6-007e-4841-abfa-f680d7e98b79

---

New option: `Page as Sleep Screen` - `Never (default)`, `After Timeout`,
`Always`

When enabled, it seamlessly sleeps on timeout or power off, making a
fast refresh for the moon icon. When waking up, we still show the last
page, instead of the boot screen, making it fully seamless.

I tried different icons such as "refresh arrow" and others, but they
looked not as nice as 3 simple dots.

With this mode, the device turns off 4 seconds faster. And has 6 seconds
less delay when turning back on. Much more responsive.

Previously, even a 10-minute timeout sometimes wasn't enough, and I'd
worry about seeing the book cover. It's now easier to use a shorter
sleep timeout: if I get distracted during a reading session but don't
want to stop, the new screen is much more inviting to come back to.

---

Did you use AI tools to help write this code? _**PARTIALLY**_.

---

Test v1.3.0 firmware.bin file
[download](https://github.com/user-attachments/files/28015193/firmware.zip)

Based on PRs #410 and #495

Closes #400, #1649

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 18:45:28 -04:00
Jackson beb7876ccf feat: Make page turn naturally follow orientation (#2023) 2026-05-19 23:13:27 +03:00
Uri Tauber b69111bea5 refactor: Consolidate theme rendering into ThemeMetrics (#1868) 2026-05-19 22:50:26 +03:00
Jeremy Klein 86a9510957 fix: stabilize deep sleep wake on USB power (#2060)
When the device went into deep sleep while plugged into USB, a
power-button press would occasionally not wake it. The display held its
last frame, the chip stayed in deep sleep, and recovery required an
unplug + reset + hold-power cycle. On battery the symptom never surfaced
because the power button physically re-energises the chip.

Two peripherals were holding power domains alive across the deep sleep
boundary and interfering with the configured GPIO wake on the power
button:

1. HWCDC. Once Serial is initialized, the USB Serial/JTAG peripheral
keeps its power domain configured even with TX timeout at zero and no
host draining. Tear it down with Serial.end() in
HalPowerManager::startDeepSleep, gated by ENABLE_SERIAL_LOG to match the
Serial.begin site. This hit me if I was charging off my computer.

2. WiFi. enterDeepSleep had no WiFi teardown, so sleeping from any
network-using activity left the modem domain alive. Call
WiFi.disconnect(true) + WiFi.mode(WIFI_OFF) when WiFi is active. Wake
from deep sleep is effectively a chip reset, so no WiFi state needs to
survive. While this doesn't cause higher power drain, it apparently was
causing issues where I'd occasionally have the chip hang on sleep
transition from a wifi activity.

Confirmed on device.


Did you use AI tools to help write this code? partial
2026-05-19 12:04:38 -04:00
Uri Tauber 08461c08e7 fix: small QoL: return to the last selected menu location (#1629)
## Summary

* **What is the goal of this PR?** Small UX improvement to the Home
screen by preserving the last selected cursor position when returning to
it.

It supersedes #985 and #1103, which are both significantly outdated and
hundreds of commits behind master.

---

### AI Usage

Did you use AI tools to help write this code? _**< YES >**_
2026-05-19 08:41:39 -05:00
Jeremy Klein a14c8e762d perf: shrink HomeActivity cover cache from 48KB framebuffer to 16KB region (#2035)
On-device repro showed the cover snapshot pinning ~52KB of contiguous
heap (cloning the full 48KB framebuffer with malloc overhead). MaxAlloc
on Home was 61KB; nothing was leaving headroom for HTTPS, which needs
30-50KB contiguous for the mbedTLS handshake.

Add region-aware framebuffer helpers to GfxRenderer that translate a
logical rect through rotateCoordinates and copy only the byte range that
contains the rotated rect. HomeActivity records the tile rect it passes
to drawRecentBookCover and caches only that subregion.

Measured on device (X3, Portrait):

  Idle on Home    | Free 102K -> 139K  | MaxAlloc 61K -> 115K
  Mid-EPUB-read   | Free  81K -> 134K  | MaxAlloc 70K -> 115K
  Cover cache     |        ~52K -> ~16K (per allocation)

Works in all four orientations because the bounds helper samples the
four logical corners through the existing rotation, so the cached byte
range always covers the pixels the theme could have drawn into.

Savings will vary with theme, but should be significant across all.
2026-05-18 22:41:02 -04:00
Jeremy Klein a525606d7f fix: USB serial logs now flow on cold+warm boot without jiggle (#2034)
The "logs only flow if you unplug and replug the USB cable at the right
moment" symptom traced to two interacting problems with the ESP32-C3 USB
Serial/JTAG controller (HWCDC):

1. Serial.begin was gated on gpio.isUsbConnected(). That check sampled
USB state at one specific microsecond during boot. If USB enumeration on
the host hadn't completed by that moment (common after a reset that
auto- reconnects a moment later), Serial was never initialized and
stayed dead until the next boot where the timing happened to win.

2. HWCDC writes block for up to the configured TX timeout (default 250
ms) when the host has the port open but isn't actively draining — a
state the macOS USB CDC stack enters intermittently after reconnect. The
firmware then appears to hang on logging until a USB unplug+replug
cycles the peripheral and flushes the TX FIFO.

Fix: move the Serial init to the very top of setup() with a 250 ms stall
before Serial.begin (lets the USB peripheral power-on and host
enumeration complete on cold boot), and call logSerial.setTxTimeoutMs(0)
so writes drop bytes harmlessly when the host is slow instead of
stalling the firmware. Both warm reboot and cold power-on now produce
logs immediately.

Did you use AI tools to help write this code? partial
2026-05-18 22:26:25 -04:00
KemoNine df53faab91 feat: allow removing book from recent list (#2045)
## Summary

Add ability to long press 'confirm' on a book in the recent books list
to be prompted to remove it from the list.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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*

---------
2026-05-18 22:16:33 -04:00
KemoNine 6a98c2d865 feat: add setting that allows removing books from recent list when read (#2043) 2026-05-18 21:13:41 -04:00
Justinian cfd3a381ed feat: X3 clock display with DS3231 RTC and NTP sync (#1612) 2026-05-18 21:06:56 -04:00
Jeremy Klein 151bf1dae4 fix: silent-restart on exit from KOReader auth and OTA update (#2036)
PR #1908 silent-restarts on exit from any wifi-using activity to defuse
LWIP/mbedTLS heap fragmentation, but two of the wifi-using paths slipped
through that audit:

  KOReaderAuthActivity (Settings -> KOReader sync -> Authenticate)
  OtaUpdateActivity    (Settings -> Check for update, back-out paths)

Both used WiFi.disconnect + WiFi.mode(WIFI_OFF) on exit and returned
control to Settings, leaving ~50KB of contiguous heap stranded for the
rest of the session.

Mirror the FontDownloadActivity pattern: if WiFi was activated,
disconnect and silentRestart. OTA's success path is unchanged:
SHUTTING_DOWN already calls plain ESP.restart() so the new firmware
boots normally; only the cancel/fail/no-update back-out paths now go
through silentRestart().


Did you use AI tools to help write this code? partial
2026-05-18 21:04:11 -04:00
KemoNine 06d28d6ffa feat: port crossink 'read book move' feature to crosspoint (#2032) 2026-05-18 12:39:50 -04:00
CaptainFrito 8a11f44571 feat: Themed reader menus (#1072) 2026-05-18 17:35:27 +03:00
WuTofu 061c7688a4 chore: add 3-minute sleep option (#1948) 2026-05-18 00:17:24 +03:00
zgredexandJustin Mitchell c6d116024c fix: harden EPUB optimiser UI gating, size reporting, and picker teardown (#1947)
Co-authored-by: Justin Mitchell <justin@jmitch.com>
2026-05-17 12:12:56 +03:00
KemoNine 93e81daf41 fix: prune books missing form sd card in recent books list (#1959) 2026-05-16 22:03:49 +03:00
Blue 90d4c885e1 fix: update URL-encoded image during EPUB optimization (#1985)
## Summary

* **What is the goal of this PR?**  
Fix EPUB optimization when XHTML image references are URL-encoded.

* **What changes are included?**  
The optimizer already converts image files to `.jpg`, but XHTML files
could still reference the original URL-encoded image path, for example:

```html
<img src="images/wensday%201%20full%202.png">
````

The optimized EPUB then contained the converted file:

```text
images/wensday 1 full 2.jpg
```

but the XHTML still pointed to the old `.png`, so CrossPoint failed to
extract/render the image.

The issue was that the previous replacement logic matched only the plain
filename form, such as:

```text
wensday 1 full 2.png
```

but not the URL-encoded form:

```text
wensday%201%20full%202.png
```

This PR updates XHTML image `src` attributes through the existing
DOMParser pass by decoding and resolving the image path before matching
it against renamed images.

After this fix, the optimized EPUB correctly rewrites the XHTML image
reference to the generated `.jpg`, and the image renders correctly.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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**_

---

Please let me know if you have questions,
Thank you!
2026-05-15 21:30:49 -05:00
Kira ee947b06d1 fix: prevent card overflow on screens (#1943)
## Summary

prevent card overflow

## Additional Context

<img width="1920" height="1080" alt="bug"
src="https://github.com/user-attachments/assets/df84e233-908e-4ce1-8289-d0e9b579bc13"
/>
<img width="1920" height="1080" alt="Bug"
src="https://github.com/user-attachments/assets/cfd20f51-6421-4271-9a62-9c1987cc0dd0"
/>
<img width="1920" height="1080" alt="fix"
src="https://github.com/user-attachments/assets/4fbe83fe-5376-4393-bd45-a825f24e19e3"
/>
<img width="1920" height="1080" alt="fix2"
src="https://github.com/user-attachments/assets/4397848e-d8fa-4c51-ab4d-c32b2fcf33d1"
/>


---

### AI Usage

Did you use AI tools to help write this code? _**NO**_
2026-05-15 21:29:38 -05:00
marcinoktawian 28b907321e fix: use power button held time for shutdown logic (#1890)
## Summary

* **What is the goal of this PR?**
Fix incorrect power button long-press detection during shutdown/wake
verification by introducing dedicated power button timing logic.
* **What changes are included?**
* Added getPowerButtonHeldTime() to HalGPIO as a wrapper over input
manager logic
* Replaced generic getHeldTime() usage with power-button-specific timing
in verifyPowerButtonWakeup()
* Ensures shutdown/wake decision is based only on actual power button
hold duration, not any-button timing
  * Minor header update for new API exposure in HalGPIO.h
## Additional Context

This fixes a bug where holding another button while briefly pressing the
power button could incorrectly trigger shutdown behavior due to shared
timing state (getHeldTime()).

The change isolates power button timing to prevent cross-button
interference and makes shutdown logic reliable during multi-button
interactions.

No behavioral changes are expected outside of power-button handling
logic.

**Dependencies**
- SDK PR: https://github.com/crosspoint-reader/community-sdk/pull/3

This PR requires the `community-sdk` submodule to be updated after the
SDK change is merged.

- Fixes: #1881

---

### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
2026-05-15 21:28:52 -05:00
WuTofu 2f342508bc fix: several QoL updates for SD font's UI (#1965)
## Summary

* **What is the goal of this PR?**  
Improve the UI based on feedback from someone on discord

> Downloading ALL fonts feature.
> 1.1 Disable sleep when downloading, in my case went directly to sleep
just right after downloading.
> 1.2 It would be great to have and overall progress indicator as we
only have the indication of each font family
> 1.3 Any cancel or pause function might come in handy in case battery
is running out and then resume or retry with pending fonts

* **What changes are included?**  
- Now the UI can show overall progress across every file being
downloaded in the batch, not just progress inside the current family.
- Extended `HttpDownloader::downloadToFile()` to accept a cancel flag
and abort the download.
- Rendered a cancel button in the font download UI while a download is
in progress.
- `preventAutoSleep()` in `FontDownloadActivity.h` now returns true for
`state_ == COMPLETE` and `state_ == ERROR` in addition to
`LOADING_MANIFEST` and `DOWNLOADING`

## Additional Context

Not very satisfied with how `HttpDownloader.cpp` is right now, might try
to refactor it after v1.3.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? _**PARTIALLY**_
2026-05-15 21:25:48 -05:00
Jeremy Klein 7acc31bc34 fix: silent-reboot on wifi activity exit to clear heap fragmentation (#1908)
WiFi/LWIP/netif teardown scatters long-lived allocations across the
heap, leaving ~50KB of contiguous space unrecoverable without a reboot.

Reboot the SoC on exit from any wifi-using activity to guarantee a clean
heap. An RTC_NOINIT flag survives the reboot and tells setup() to skip
the boot splash and route the user back where they came from:
  - File transfer / Calibre / OPDS / Font download -> home
  - KOReader sync -> currently-open EPUB

Activities check WiFi.getMode() before rebooting, so backing out of the
network mode menu without joining doesn't trigger a cycle. KOSync also
esp_wifi_stop()s after the sync result so the radio is off while the
user reads it; full teardown happens at the reboot.


## Additional Context

The silent reboot skips the booting splash screen - it visibly looks
like a screen refresh. This does cause a disconnection/reconnection blip
for developers actively pulling logs over serial, but `pio device
monitor` and the like successfully reconnect and feed in the early boot
serial.
as an example: 
```
[256676] [DBG] [ACT] Exiting activity: KOReaderSync
[256706] [DBG] [MAIN] Silent restart (target=reader)

ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0xc (RTC_SW_CPU_RST),boot:0xf (SPI_FAST_FLASH_BOOT)
Saved PC:0x403872bc
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd72a0,len:0x990
load:0x403cbf10,len:0xac8
load:0x403ce710,len:0x4d28
entry 0x403cbf10
[22] [INF] [MAIN] Hardware detect: X4
[29] [SD] SD card detected
[43] [DBG] [CPS] Settings loaded from file
[58] [DBG] [KRS] Loaded KOReader credentials for user: jeremydk
[69] [DBG] [OPS] Loaded 1 OPDS servers from file
[69] [DBG] [UI] Using Lyra theme
[70] [DBG] [MAIN] Starting CrossPoint version 1.2.0-dev-detached-bde75787

...

[203] [DBG] [ACT] Entering activity: Reader
[211] [DBG] [EBP] Loading ePub: /Halting State - Charles Stross.epub
[221] [DBG] [BMC] Loaded cache data: 51 spine, 41 TOC entries
[246] [DBG] [CSS] Loaded 41 rules from cache
[247] [DBG] [EBP] Loaded ePub: /Halting State - Charles Stross.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-05-15 19:27:54 -05:00
Zach NelsonandJustin Mitchell b186529120 fix: Prepare SD card font caches from txt reader (#1973)
## Summary

SD card font fixes:
- `TxtReaderActivity` needs to call `renderer.ensureSdCardFontReady` to
build the advance lookup table to support rendering with SD card fonts.
This revealed that `TxtReaderActivity` was inconsistently performing
layout with `getTextWidth`, when the renderer actually uses
`getTextAdvanceX`, which can lead to minor inconsistencies in alignment.
- Avoid allocating one big `allText` string in
`ParsedText::layoutAndExtractLines`. Instead, pass the vector of word
strings directly to `SdCardFont::buildAdvanceTable`, where the algorithm
just needs to iterate codepoints anyway.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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: Justin Mitchell <justin@jmitch.com>
2026-05-15 09:50:41 -05:00
Zach Nelson aa43bd3569 chore: Removed unused icon header files (#1975)
## Summary

Deleted two unused header files containing binary icon data.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-14 07:34:35 -05:00
Zach Nelson 30b14f2ecf refactor: Removed SdCardFontGlobals.h (#1962)
## Summary

First of several changes to decouple and clean up SD card fonts
integration. This change eliminates SdCardFontGlobals.h:
- Simply declare the `extern SdCardFontSystem sdFontSystem` in
SdCardFontSystem.h.
- `ActivityManager::goToReader` should not care about loading SD card
fonts. Instead do the same work in `ReaderActivity::onEnter`, after the
previous activity has exited and after ReaderActivity has validated the
file path.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-13 08:26:56 -05:00
Zach Nelson 6f7f4c592a refactor: Eliminated relative path includes (#1961)
## Summary

Relative includes can hide inappropriate dependency relationships. In
this case, I found that lib/KOReaderSync/KOReaderCredentialStore.cpp was
dependent on src/JsonSettingsIO.h -- a lib -> app dependency, the
opposite direction dependencies should flow in this project.

This change replaces all relative includes with root-relative includes,
and corrects the KOReaderCredentialStore dependency by moving its JSON
settings serialization local to the KOReaderSync library.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-13 08:26:30 -05:00
zgredex bc57e5d64b fix: jump page on hold in font family and language selection (#1925)
## Summary
- Holding the navigation button in **Settings → Reader → Font family**
now advances the selection by a full visible page instead of one item at
a time.
- Same fix applied to **Settings → Language**, which had the same
one-item-only behavior.
- Mirrors the pattern already used in font download (ceb3fed) and
chapter selection screens.

## Test plan
- [ ] Settings → Reader → Font family: tap moves by one item; hold jumps
a page (wraps at ends)
- [ ] Settings → Language: tap moves by one item; hold jumps a page
(wraps at ends)
2026-05-12 17:37:56 -04:00
WuTofu bc6e090aa8 feat: separate into "Download All" and "Update All" in font manager (#1955)
## Summary

* **What is the goal of this PR?**  
Separate the font manager's combined `Download / Update All` action into
separate `Download All` and `Update All` rows

* **What changes are included?**  
- Updated multiple i18n translation files to add `STR_UPDATE_ALL` and
adjust `STR_DOWNLOAD_ALL` text.
- Added separate handlers: `downloadAll()` for fonts that have been
installed and `updateAll()` for fonts with updates.
  - `Download All` and `Update All` are only shown when applicable.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-12 12:17:01 -05:00
Jan Ivanov 8d1b86a893 feat: add next / prev labels to bmp viewer (#1852)
## Summary

Bind `prev` / `next` functionality to the left and right buttons in BMP
Viewer and adds labels

<img width="718" height="953" alt="image"
src="https://github.com/user-attachments/assets/c6dac14e-14f5-4cbf-9298-772cfc479f33"
/>

## 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-05-12 15:06:35 +03:00
Zach Nelson 63d5094f2e Revert "feat: closest-pt size selection instead of ordinal slot" (#1949)
Reverts crosspoint-reader/crosspoint-reader#1912

This was meant to be more robust with partial SD card fonts, but causes
trouble for folks using custom font sizes. We need a better approach to
decouple numeric font sizes from S/M/L/XL settings.
2026-05-11 12:44:52 -05:00
WuTofu a0037ced2b feat: add font family deletion functionality (#1919)
## Summary

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

1. Adds font family deletion support to the font download activity so
users can remove installed font families directly from the download
list.
2. Address an issue where a font family manually deleted in the file
browser by user will show as "update" in the `FontDownloadActivity`
 

* **What changes are included?**
* Updated button hint label to show `Delete` when deletion is available.
  * Added confirmation before deleting a selected installed font family.
* Refreshed font registry in `fetchAndParseManifest` (for the second
goal above)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-11 15:45:38 +02:00
Uri Tauber 20fee843c7 fix: Missing navigation button labels in Roundedraff theme (#1905) 2026-05-10 21:15:45 +03:00
KemoNine dd06e71b66 fix: remove percent rendering from activities (#1901) 2026-05-10 21:10:23 +03:00
Vincent Politzer 4e5e2fe40a feat: focus reading (#1670)
## Summary

This PR introduces **Focus Reading**, a generic implementation of
artificial fixation points (similar to Bionic Reading) designed to
improve reading speed and focus by bolding the initial characters of
words. This is achieved by dynamically bolding characters during
indexing.

<img width="500" alt="Focus Reading on X3"
src="https://github.com/user-attachments/assets/94a632a5-82da-47be-957c-538b35bf84d9"
/>

### Implementation Details
#### Core Text Engine (`ParsedText`)
- Modified `ParsedText::addWord` to implement a custom bolding
algorithm. It uses a 45% ratio for bolding, with a minimum of 1
character and a maximum of 9.
- UTF-8 Safety: Integrated `utf8NextCodepoint` to ensure character
counting and string slicing occur at safe byte boundaries, preventing
corruption of multi-byte characters (e.g., accented letters or smart
quotes).
- Intelligent Tokenization: This correctly identifies and separates
"word" characters (letters, apostrophes, hyphens) from "non-word"
characters (numbers, brackets, smart quotes).
- Formatting Preservation: The logic ensures that punctuation is not
"stolen" for the bolding count and that existing styles (like italics or
underlines) are preserved across the bold/regular split.
- Processing at indexing stage reduces CPU load at render-time and
ensures layout/fit is unaffected.
- Split details are tracked with `wordIsFocusSuffix`. After splitting
and layout, suffixes are merged back into their preceding word entries
to prevent a doubling of RAM usage.

#### Settings and UI
- Version Management: Bumped `SECTION_FILE_VERSION` to `21`
- Global Settings: Added `focusReadingEnabled` to `CrossPointSettings`.
- User Interface: Added a new toggle in the "Reader" section of the
settings menu, positioned after the "Embedded Style" option.
- Localization: Added the `STR_FOCUS_READING` string

#### Plumbing
- Plumbed the `focusReadingEnabled` boolean through
`EpubReaderActivity`, `Section`, and `ChapterHtmlSlimParser` to ensure
the user's setting reaches the `ParsedText` constructor during chapter
indexing.

## Additional Context

### Files Changed
- `lib/Epub/Epub/ParsedText.h / .cpp`: Core fixation logic and UTF-8
tokenization.
- `lib/Epub/Epub/Section.h / .cpp`: Cache header updates and
invalidation logic.
- `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h / .cpp`: Plumbing the
setting to text blocks.
- `src/CrossPointSettings.h`: Data persistence for the new setting.
- `src/SettingsList.h`: UI toggle implementation.
- `src/activities/reader/EpubReaderActivity.cpp`: Handling settings
changes during reading sessions.
- `lib/I18n/translations/*.yaml`: UI strings.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-10 20:02:14 +02:00
jpirnay 45441e0789 feat: closest-pt size selection instead of ordinal slot (#1912)
Add SdCardFontFamilyInfo::pickClosestSize(targetPtSize) and have the
manager and SdCardFontSystem drive size selection from a target point
size derived from the user's font-size enum (SMALL=12, MEDIUM=14,
LARGE=16, EXTRA_LARGE=18) rather than indexing the family's sorted size
list by enum ordinal.

The ordinal-slot approach mis-selects whenever a family doesn't ship the
canonical {12,14,16,18} set: a family with only [10,14,18] would map
SMALL/MEDIUM/LARGE/EXTRA_LARGE to 10/14/18/18 — fine for SMALL but
arbitrary for the rest. Closest-pt always picks the on-disk file nearest
to the user-intended point size, with a deterministic smaller-pt
tie-break.

No change for canonical-sized families.

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
* **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-05-10 11:01:24 -05:00
Uri Tauber 91de6ac278 fix: two roundedraff bugs (#1851) 2026-05-10 07:35:16 +03:00
WuTofu 3efc863038 feat: verify CRC32 checksum for font files (#1904)
## Summary

* **What is the goal of this PR?**  
Add end-to-end integrity verification for downloaded font files by
including CRC32 checksums in the font manifest and validating downloaded
`.cpfont` files on device.

* **What changes are included?**  
- `generate-font-manifest.py`: compute and include `crc32` for each
`.cpfont` asset in the generated `fonts.json` manifest.
- `FontDownloadActivity.h`: extend manifest file metadata with `crc32`
and declare checksum helper.
- `FontDownloadActivity.cpp`: parse `crc32` from manifest, compute CRC32
of downloaded files using `esp_rom_crc32_le`.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-09 21:46:59 -05:00
pablohc bf30964982 fix: overlap in download font list layout (#1900)
## Summary

Fix font download list layout overlap in Classic and RoundedRaff themes.
The font description was shown as a right-aligned value, causing text to
overlap with the font name on narrow screens.

### Changes
- Move font description from `rowValue` to `rowSubtitle` (second line)
- Show only status ("Installed"/"Update Available") as `rowValue`
- Fix Classic and RoundedRaff themes `rowValue` truncation (was fixed
60px, now dynamic)

## Screenshots

### Classic
| RC 1.3.0 | #1900 |
|-|-|
|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/534434d6-d7a6-4f73-9d80-8fe5d060907c"
/>|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/e616c4b7-43b9-49a8-8aa6-ab19f29865f1"
/>|

### Lyra
| RC 1.3.0 | #1900 |
|-|-|
|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/5b42bad4-2860-4863-a025-5292aa391c1a"
/>|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/4c27dcbb-8e8e-4ef0-8771-f92975210f11"
/>|

### RoundedRaff
| RC 1.3.0 | #1900 |
|-|-|
|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/f0510c6f-7712-4cec-8fc8-4386951b1795"
/>|<img width="480" height="800" alt="image"
src="https://github.com/user-attachments/assets/017596b8-fe95-4541-884b-5de034c24193"
/>|

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please 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-09 22:11:13 +02:00
zgredex ceb3fed392 fix: remove duplicate 'Download Fonts' menu entry and improve navigation (#1893)
## Summary
Removes duplicate "Download Fonts" menu entry and adds complete
navigation support to the font download activity.

## Problem
"Download Fonts" was appearing in **both** Reader settings and System
settings, creating a confusing duplicate menu entry.

## Changes

### 1. Remove Duplicate Menu Entry
- Removed `STR_DOWNLOAD_FONTS` from `systemSettings` in
`SettingsActivity.cpp`
- "Download Fonts" now only appears in **Reader settings**, positioned
right after the font family setting
- Rationale: Font settings logically belong together in the Reader
category

### 2. Navigation Improvements
- **Single-item navigation**: Replaced manual bounds checking with
`ButtonNavigator::nextIndex()` and `previousIndex()` methods
  - Navigation now wraps from last item to first (and vice versa)
  
- **Page navigation**: Added continuous navigation handlers
  - Long-pressing up/down buttons now navigates by page
  - Uses `UITheme::getNumberOfItemsPerPage()` for consistent behavior

## Files Changed
- `src/activities/settings/SettingsActivity.cpp`: Removed duplicate
entry
- `src/activities/settings/FontDownloadActivity.cpp`: Navigation
improvements

## Testing
- Build:  Compiled successfully
- Device testing: Recommended before merge
2026-05-09 18:51:36 +02:00
Wylan SwetsandClaude Sonnet 4.6 bf894fd343 fix: improve KOSync bidirectional position matching accuracy (#1897)
## Summary

**Goal:** Fix bidirectional KOSync position matching between CrossPoint
and KOReader so that syncing in either direction lands on the correct
page with character-level accuracy.

**Changes included:**

**Download — `toCrossPoint` (server XPath → CrossPoint page)**
- **XPath ancestry mode for structured elements**: The previous
`ParagraphStreamer` only tracked `<p>` elements. Replaced with a full
ancestor-walking mode that correctly resolves XPaths pointing into
`<li>`, `<ul>`, and other structured elements. Char offset within the
target element is bounded to the matched element's content only.
- **Slash-in-attribute-value corrupts depth tracking**:
`processByteInTag()` treated every `/` byte as a self-closing tag
marker, including `/` inside quoted attribute values (e.g.
`xmlns="http://..."`, `src="Links/image.jpg"`). This drove `htmlDepth`
to 0 prematurely, causing the ancestry search to exit far short of the
target paragraph. Fixed with `inAttrQuote` tracking.
- **Off-by-one in page formula**: `intra * totalPages` rounds up
incorrectly for last-page positions. Changed to `intra * (totalPages -
1)` to map the `[0, 1]` intra fraction correctly onto the `[0,
totalPages-1]` page range. Example: page 14 of 17 was returned as 15.

**Upload — `toKOReader` (CrossPoint page → server XPath)**
- **Off-by-one in page-to-intra formula**: Symmetric fix — `pageNumber /
totalPages` changed to `pageNumber / (totalPages - 1)`, with the guard
updated from `> 0` to `> 1` to avoid division by zero.
- **`<li>`-based XPath generation**: When the current page starts on a
list item, `findXPathForProgress` now generates `ul[N]/li[M]` XPaths
rather than falling back to the preceding `<p>`. Requires the new
`listItemIndex` field in `PageLutEntry` (section cache version bumped to
23).
- **Text-node precision with correct `text()[N].M` format**: KOReader
expects `text()[N].M` where `N` is the 1-based index of the specific
text node within the element. The previous attempt generated `text().M`
(no brackets), which caused KOReader to jump to the front of the book.
Implements a per-element text-node index stack in
`XPathProgressResolver` — parallel to the existing element path stack —
that correctly tracks text node indices relative to each element. Empty
text nodes from bare anchor elements (`<a id="anchor"/>`) are
intentionally skipped, matching KOReader's own text node counting
behavior.

**Reviewer-caught bugs**
- **Double `onCloseTag()` on malformed `</br/>`**: Both the `tagIsClose`
path and the self-closing `/` check were firing, double-decrementing
`htmlDepth`. Fixed with a `!tagIsClose` guard.
- **Dangling pointer in `LOG_DBG`**:
`std::to_string(*nextParagraphPage).c_str()` passed a pointer to a
temporary destroyed before the variadic call. Fixed with `snprintf` into
a stack `char[8]` buffer.

## Additional Context

- Section cache version bumped from 22 → 23 due to the new
`listItemIndex` field in `PageLutEntry`. Users upgrading will see a
one-time re-render of all cached sections on first load — no data loss.
- The `textNodeIndexStack` in `XPathProgressResolver` is a
`std::vector<int>` that mirrors the existing `path` and `parentStates`
stacks — same depth, same lifetime. No additional heap pressure beyond
what was already present.
- All fixes verified on device with *Gentle and Lowly* by Dane C.
Ortlund (spine 21, 17 pages). Download syncs land on the correct page;
upload syncs land at the correct paragraph with character-level offset.

## Test plan

- [ ] Download: sync from KOReader → CrossPoint lands on correct page
for `text()[N].M` XPaths
- [ ] Download: ancestry correctly resolves `<li>` positions inbound
from KOReader
- [ ] Upload: sync from CrossPoint → KOReader lands within one page for
mid-paragraph positions
- [ ] Upload: sync from CrossPoint → KOReader correctly targets `<li>`
elements when page starts on a list item
- [ ] Upload: `text()[N].M` format XPaths do not cause KOReader to jump
to front of book
- [ ] Section cache version 23: delete `.crosspoint/` and verify clean
re-parse with no crashes

---

### AI Usage

Did you use AI tools to help write this code? **YES** — developed with
Claude Code (Anthropic).

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:22:24 -04:00
Wylan SwetsandClaude Sonnet 4.6 e64155ed63 fix: free Epub RAM and simplify KOSync navigation via ActivityManager (#1860)
## Summary

* **What is the goal of this PR?** Fix KOSync failing with "Network
error" on large/complex EPUBs, and simplify the sync navigation flow by
removing the callback/result pattern.

* **What changes are included?**

This PR combines the approaches from #1855 and #1760 into a single,
cleaner solution:

  **Memory fix (from #1855):**
- `EpubReaderActivity` pre-computes the local KOReader position and
chapter name, then explicitly releases `epub` and `section` before
launching `KOReaderSyncActivity`. This frees ~65KB measured on device,
giving the TLS handshake sufficient heap. The root cause was
`MBEDTLS_ERR_X509_ALLOC_FAILED` (-0x2880) when a 3-cert chain consumed
~48KB during the handshake with only ~50KB available.
- `KOReaderSyncActivity` no longer receives a `shared_ptr<Epub>` at
construction — it lazy-loads the Epub after TLS only if remote progress
is found (`ensureEpubLoaded()`).
- Added `MIN_HEAP_FOR_TLS = 55000` guard in `KOReaderSyncClient` —
returns `LOW_MEMORY` early if aggregate free heap is too low before
attempting a TLS connection.

  **Navigation simplification (from #1760):**
- Replaced `startActivityForResult` + callback with
`activityManager.replaceActivity` / `activityManager.goToReader`.
Progress is saved to `progress.bin` before the epub is released
(cancel/upload paths) and in `saveProgressAndReturn` (apply remote
path). The reader re-launches from the saved position naturally via
`goToReader`, eliminating the need to reload the epub in a callback.
- Extracted `ReaderUtils::saveProgress()` as a shared helper used by
both `EpubReaderActivity` and `KOReaderSyncActivity`.
- Added `STR_SAVE_PROGRESS_FAILED` to all 22 language files for the case
where writing the synced position to SD fails.

  **Orientation fix (found during device testing):**
- `EpubReaderActivity::onExit()` resets the renderer to portrait before
destruction. With `replaceActivity` the reader is fully torn down before
KOSync starts, so KOSync was always rendering in portrait even when
reading in landscape. Fixed by calling `ReaderUtils::applyOrientation`
in `KOReaderSyncActivity::onEnter()`.

## Additional Context

Heap measurements on device (large EPUB with complex CSS):

| Metric | Before | After |
|---|---|---|
| Heap before Epub release | 88,156 bytes | — |
| Heap after Epub release | — | 153,892 bytes (+65,736) |
| Heap at TLS handshake | ~50,000 bytes (fails) | ~116,384 bytes
(passes) |
| Min-free-ever during sync session | 2,600 bytes | 33,052 bytes |
| TLS result | `MBEDTLS_ERR_X509_ALLOC_FAILED` | HTTP 200 |

Tested on device: sync from inside a large EPUB in both portrait and
landscape, cancel, apply remote progress, upload local progress.

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:01:31 -05:00
7993b2bb97 feat: add SD card font support with on-device download and web management
Add a complete SD card font subsystem that enables users to install and
use custom fonts beyond the three built-in families. This combines the
back-end firmware support (#1327) with the font configuration, build
pipeline, CI distribution, and user-facing management UI (#1392).

Core font system:
- Custom .cpfont binary format (v4) with multi-style support (regular,
  bold, italic, bold-italic) packed into a single file per size
- On-demand glyph loading from SD card with two-pass prewarm rendering
  to bulk-read glyphs per page, achieving near-flash performance for
  Latin text (~697ms vs ~681ms) and viable CJK rendering (~32% slower)
- Persistent advance cache for layout measurement without SD I/O
- Overflow ring buffer for glyph cache misses during rendering
- Memory-conscious design: only advance tables kept in RAM; glyph
  bitmaps, kern tables, and ligatures loaded on demand from SD

Font management:
- On-device WiFi download from GitHub Releases with manifest-based
  discovery, install/update detection, and progress UI
- Web interface font upload, listing, and deletion via /fonts page
- Manual SD card copy to /fonts/ or /.fonts/ directories
- Font selection integrated into Settings > Reader > Font Family

Build pipeline:
- Declarative YAML config (sd-fonts.yaml) as single source of truth
  for the 17-family font library (serif, sans, mono, accessibility)
- Python converter (fontconvert_sdcard.py) for TTF/OTF to .cpfont with
  FreeType rasterization, class-based kerning, and ligature extraction
- Parallel build orchestrator with variable font instance extraction
- CI workflow publishing versioned + stable releases to a dedicated
  crosspoint-fonts repository with auto-incrementing revision tags
- Centralized version constants (cpfont_version.py) shared across
  build tooling and CI, with firmware headers as manual sync points

Additional fixes:
- CJK characters no longer get hyphens inserted at line breaks
- Advance table eliminates 30+ second stalls during CJK section
  indexing for paragraphs with >512 unique codepoints

Closes #930

Co-authored-by: Zach Nelson <zach@zdnelson.com>
Co-authored-by: Justin <itsthisjustin@users.noreply.github.com>
Co-authored-by: jpirnay <jens@pirnay.com>
Co-authored-by: mcrosson <kemonine@kemonine.info>
2026-05-08 21:50:06 -05:00