Skip to content

refactor: dev lut#14

Open
pablohc wants to merge 104 commits into
masterfrom
dev-lut
Open

refactor: dev lut#14
pablohc wants to merge 104 commits into
masterfrom
dev-lut

Conversation

@pablohc
Copy link
Copy Markdown
Owner

@pablohc pablohc commented May 18, 2026

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 >

Summary by CodeRabbit

  • New Features

    • .pxc grayscale viewer with sleep-cover support; power-button menu for quick Sleep/Refresh/Screenshot.
    • New "XTC Render Quality" setting.
  • Improvements

    • Enhanced grayscale rendering and factory-quality display paths; improved 2-bit grayscale screenshots.
    • UI/layout and wallpaper/file support improvements (including .pxc).
    • EPUB/web conversion now supports device-targeted optimization.
  • Settings

    • Added 3-minute sleep option and "Move finished books to Read" setting.
  • Localization

    • Added translation strings for new UI entries across many languages.

Review Change Stack

Patryk Radtke and others added 30 commits April 23, 2026 22:44
Add GRAY2 absolute encoding for 4-shade e-ink rendering via factory LUTs.
Introduce FactoryFast and FactoryQuality GrayscaleMode variants with X3
fallback to Differential. Add direct-pixel BMP rendering path, 2-bit XTC
plane support in Xtc parser, and HALF_REFRESH display mode in HalDisplay.
displayXtchPlanes uses Differential on X3 (factory LUT not calibrated).
Skip fadingFix power-down on first BW render after factory LUT to avoid
redundant power cycle (display already off after 0xC7 sequence).
Add PxcViewerActivity for PXC image display using factory LUT grayscale.
Update BmpViewerActivity and SleepActivity with X3-aware grayscale mode.
EPUB reader uses FactoryQuality on X4 for image pages, Differential on X3.
XTC reader pre-flashes to white on entry; periodic FULL_REFRESH every 32 pages.
Add UITheme metrics for PXC viewer layout.
Add ScreenshotUtil to detect factory LUT display state and re-render
the current page via renderGrayscale for accurate screenshot output.
X3 falls back to Differential mode for all grayscale paths.
…m Home

When entering sleep from Home (not Reader), the display particle state
is in BW mode from FAST_REFRESH renders. Without pre-conditioning, the
subsequent factory LUT render produces a dirty appearance. Added a
clearScreen + HALF_REFRESH before displayXtchPlanes to reset particles
to a known state.
Replace the 2-bit BMP thumbnail generation for XTCH files with 1-bit BMP
using Atkinson dithering (matching EPUB thumbnail quality). Key changes:
- Load both XTCH bitplanes separately for area-averaged downsampling
- Apply Atkinson 1-bit dithering (3 error rows, 6 neighbors, 6/8 error)
- Apply 1.2x contrast boost for better tonal separation
- Inverted bitplane luminance to match display polarity
- Falls back to 2-bit BMP on memory allocation failure
Replace hash-based noise dithering with Atkinson 1-bit dithering to match
EPUB thumbnail quality. Key changes:
- Atkinson 1-bit dithering (3 error rows, 6 neighbors, 6/8 error)
- 1.2x contrast boost for better edge definition
- Same BMP convention as XTCH path (memset 0xFF + clear bit for dark)
Eliminate ~80 lines of duplicated code:
- Replace inline Atkinson dithering with Atkinson1BitDitherer class
  (same class used by EPUB thumbnail generation)
- Extract computeSrcRange() helper for downscale coordinate calc
  (was duplicated 4 times across XTCH/XTC paths)
After reader sessions, heap fragmentation reduces MaxAlloc to ~65KB,
preventing the primary path from allocating both planes simultaneously
(96KB). The fallback produced a raw 2-bit BMP with no dithering,
resulting in poor visual quality on the Home Screen.

Unify to a single sequential path: load plane1 (48KB) -> majority-vote
-> free, load plane2 (48KB) -> combine -> Atkinson dithering -> 1-bit BMP.
Peak memory ~52KB, always within MaxAlloc constraints.

This matches the same approach used by XTC 1-bit thumbnails, ensuring
consistent quality across all themes and memory states.
Standardize all grayscale rendering to use FactoryQuality mode instead of conditionally using Differential mode for X3 devices. Also inverts the useFactoryGray logic in EpubReaderActivity to enable factory grayscale for X3 devices on image pages.
Remove X3-specific factory gray rendering path and associated pre-flash logic. Factory gray mode now only applies to image pages when text anti-aliasing is enabled, regardless of device type.
Remove obsolete comments about factory gray mode and text-only antialiasing display handling that are no longer relevant to the current implementation.
The 1-bit XTC sleep path unconditionally cleared the screen and issued a
HALF_REFRESH before calling displayXtcBwPage, which itself runs
clearScreen + FAST_REFRESH. Match the 2-bit branch by skipping the
pre-flash when the previous activity was the reader, avoiding the
redundant refresh.

Also dedupes a few render callbacks while in the area:
- Extract the 'Entering sleep' overlay lambda into drawEnteringSleepOverlay.
- Promote PageRenderCtx + grayFn to a private nested struct and static
  renderPageCallback shared by renderContents and onScreenshotRequest.
- Consolidate PxcCtx and the rendering/overlay callbacks into file-scope
  statics behind a renderPxcToFramebuffer helper used by both onEnter
  and renderGrayscaleImage.
- Reject PXC files whose dimensions don't exactly match the runtime
  screen size. The previous abs(w-sw) > 1 tolerance allowed a one-pixel
  oversize, which would cause out-of-bounds writes in DirectPixelWriter
  (no bounds checking on writePixel). Strict equality is correct per
  device since renderer.getScreenWidth/Height are device-aware
  (X4 800x480 vs X3 792x528).
- Make renderPxcSleepScreen return bool so the /sleep.pxc path can fall
  through to /sleep.bmp when the PXC is missing/invalid, instead of
  short-circuiting straight to the default sleep screen. Other callers
  (random wallpaper picker, screenshot replay) explicitly fall back to
  renderDefaultSleepScreen on failure to preserve prior behaviour.
- In onScreenshotRequest, store the BW page before the factory-gray
  render and restore it before cleanupGrayscaleWithFrameBuffer, so the
  controller's BW state is synced to the actual page rather than a
  cleared framebuffer. Mirrors the pattern in the normal render path.
The clearScreen + cleanupGrayscaleWithFrameBuffer in onScreenshotRequest
intentionally puts the controller into a 'BW = white' state. This is the
correct precondition for the HALF_REFRESH pre-flash done by the next
SleepActivity render (triggered by goToSleep on the way into deep sleep)
and the panel resets across deep sleep, so no preservation of the
displayed image is needed here.
pablohc and others added 29 commits May 19, 2026 01:00
…int-reader#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)
## 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**_
…der#1951)

## Summary

* **What is the goal of this PR?** Fix regression of
Calibre-Web-Automated authentication
* **What changes are included?** Trivial fix using ESP HTTP Client
primitives

## Additional Context

Calibre-Web-Automated KOSync stopped working after refactoring HTTP
client to use `ESP HTTP Client`, resulting in a _Server error (try again
later)_ error.

---

### AI Usage

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

---------

Co-authored-by: drbourbon <fabio@MacBook-Air-di-Fabio.local>
…leanup (crosspoint-reader#1832)

## Summary

Pared down version of crosspoint-reader#1418.

Following up on b5df6cb. Added
lib/Memory/Memory.h with:
- `makeUniqueNoThrow<T>` a `nothrow` wrapper for `std::make_unique` that
return `nullptr` on OOM instead of calling `abort()` (the behavior of
bare `new` with `-fno-exceptions`)
- `ScopedCleanup` a helper to call a cleanup lambda on scope exit.

These utilities help to write code that handles OOM scenarios
gracefully, and consistently cleans up resources on scope exit.

JpegToBmpConverter.cpp has been converted to use these utilities. Other
files can be converted later.

This will simplify some of the SD card font resource management in
crosspoint-reader#1327.

---

### AI Usage

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

Did you use AI tools to help write this code? _**NO**_
## Summary

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**_
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
)

## 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>
…er#1990)

Document the Xteink Unlocker tool requirement for third-party purchased
xteink units that ship with USB flashing locked. Include warnings about
bricking risks when flashing unsupported firmwares (e.g. Papyrix) on
locked devices, as they may permanently lock the device with no recovery
path.
…sspoint-reader#1812)

## Summary

As noted in
[crosspoint-reader#1680](crosspoint-reader#1680 (comment)),
the README hasn't been updated in a while and has fallen behind the
actual firmware. This PR brings it up to date.

Beyond the feature list, I added a section acknowledging community forks
worth knowing about. I also took some deliberate editorial choices
around how CrossPoint is framed — I think it has the potential to be
more than just "an alternative Xteink firmware", and the wording
reflects that.

A note on process: I wrote the bulk of the text myself, but used AI
tools to scan the codebase and catch features I might have missed, and
to clean up my English (I'm fluent but not a native speaker). If any
line reads as unnatural or AI-sounding, please flag it — I'd rather fix
it than leave it.

---

One thing outside the scope of this PR: I think the cover photo could
use a refresh, ideally replaced with a small gallery showing different
CrossPoint screens. If you have a professional camera and an Xteink
device and want to help with that, let me know.

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>
## Summary

This release adds SD card fonts — the most-requested feature since
launch — brings the X3 to first-class status, redesigns the on-screen
keyboard, overhauls OPDS, and ships SD-card firmware updates. 144
changes from 53 contributors, 32 of whom are new to the project.

**🔠 SD Card Fonts**
Custom fonts are here. A complete font subsystem lets you install and
use fonts beyond the three built-in families. A new `.cpfont` binary
format packs multiple styles (regular, bold, italic, bold-italic) into a
single file per size, with on-demand glyph loading from the SD card. A
two-pass prewarm renderer bulk-reads glyphs per page, achieving
near-flash performance for Latin text and viable CJK rendering. Fonts
can be downloaded over WiFi directly from the device, uploaded via the
web interface, or copied manually to the SD card. The build pipeline
ships a 17-family font library (serif, sans, mono, accessibility) with
CI distribution via a dedicated crosspoint-fonts repository. As a bonus,
CJK characters no longer get spurious hyphens at line breaks, and an
advance-table cache eliminates 30+ second stalls during CJK section
indexing.

**📱 X3 Comes of Age**
The X3 graduates from initial bring-up to a proper target. Grayscale
antialiasing is sharper, EPUB images render correctly, OTA updates work,
and sleep screen dimensions are dialed in. The headline addition:
gyroscope-based tilt page turning via the QMI8658 IMU — tilt the device
to turn pages hands-free. SD-card firmware update support and X3
bootloader compatibility mean users can update without a USB connection.

**⌨️ Redesigned On-Screen Keyboard**
The keyboard has been completely redesigned with improved layout, better
key feedback, and a fix for the space key barely moving the cursor. Text
entry across WiFi setup, OPDS search, and KOSync login is noticeably
smoother.

**👁️ Focus Reading**
A new reading mode bolds the initial characters of each word (similar to
Bionic Reading) to create artificial fixation points, helping improve
reading speed and focus. The bolding ratio is 45%, with a minimum of 1
character and a maximum of 9, applied dynamically during indexing.

**📚 OPDS Overhaul**
OPDS gains in-catalog search with next/prev page navigation, support for
multiple servers, correct handling of relative paths and query
parameters (fixing CopyParty compatibility), and KOReader-compatible
download filenames.

**🔤 Text Rendering Refinements**
Combining marks (diacritics) now use font metrics for positioning
instead of heuristics, proportional numeral spacing is supported, and
differential rounding eliminates uneven inter-glyph gaps. Hyphenation
now recognizes ISO 639-2 language codes, nested block-level CSS styles
are tracked correctly, and horizontal CSS insets are capped at 2em to
prevent runaway margins. Bookerly has been replaced with Noto Serif for
licensing reasons.

**🎨 New Theme: RoundedRaff**
A new rounded theme joins the theme picker, with fixes for sleep cover
crop grid artifacts.

**🔋 Battery & Power**
Battery percentage smoothing on the X4 eliminates jittery readings. A
short press on the power button can be set to trigger a manual screen
refresh — handy for clearing ghosting.

**📶 WiFi & Networking**
WiFi connections now self-heal from transient drops without manual
intervention, and a dBm signal strength indicator appears during web
server sessions. WiFi networks can be edited directly from the web UI.

**🔄 KOSync**
Reading position sync is significantly more accurate. The old
character-offset approach frequently landed on the wrong paragraph after
syncing between devices — the new xpath-based mapping syncs at the
paragraph level, matching KOReader's own behavior. A separate fix
switches the HTTP layer to `esp_http_client`, and the reader now
releases ~65KB of EPUB heap before the TLS handshake — together these
eliminate the out-of-memory crashes that plagued KOSync on large books.

**🛡️ Stability**
Two memory leaks patched, a wild pointer crash in JPEGDEC MCU_SKIP
handling fixed, boot loops with large XTC files eliminated, legacy XTC
headers supported, the OTA updater now streams GitHub release JSON
instead of buffering it in RAM, and a JPEG downscaler y-axis scale
factor bug is corrected.

**🌐 Languages**
Slovenian is new. Russian, Ukrainian, Swedish, Italian, and Spanish
translations received significant updates.

---

Also in this release: **SD-card firmware updates without USB**, **file
extensions in the file browser**, **full path bar navigation**,
**end-of-book navigation improvements**, **XTC status bar**, **smarter
"Cover + Custom" sleep screens**, **set sleep cover from the BMP
viewer**, **orientation-aware popups**, **page turn buttons that follow
orientation**, **long-press delete for directories**, **context-aware
screenshot filenames with book title**, **crash reason displayed on
boot**, **empty line rendering in the TXT reader**, **wallpaper recency
buffer to prevent clustering**, **font family deletion from the
device**, **next/prev labels in the BMP viewer**, **non-breaking space
justification fix**, **README guidance for USB-locked third-party Xteink
units**, and a long tail of web UI polish, i18n memory optimizations,
and code quality improvements.

## What's Changed

### Features

* feat: add SD card font support with on-device download and web
management by @adriancaruana, @znelson, @itsthisjustin, @jpirnay, and
@mcrosson
* feat: Initial support for the x3 by @itsthisjustin in
crosspoint-reader#875
* feat: X3 grayscale antialiasing improvements by @juicecultus in
crosspoint-reader#1607
* feat: X3 gyroscope-based tilt page turning via QMI8658 IMU by
@juicecultus in
crosspoint-reader#1636
* feat(update): SD-card firmware update + X3 bootloader compatibility by
@eunchurn in
crosspoint-reader#1786
* feat: self-heal from transient WiFi loss, add dBm indicator during
WebServerActivity by @jeremydk in
crosspoint-reader#1780
* feat: edit wifi networks in webui by @osteotek in
crosspoint-reader#1743
* feat: add OPDS search support & next/prev page navigation by @rxmmah
in crosspoint-reader#1462
* feat: Support for multiple OPDS servers by @osteotek in
crosspoint-reader#1209
* feat: Adjust Navigation at End of Book by @nscheung in
crosspoint-reader#1425
* feat: Display file extensions in File Browser by @CaptainFrito in
crosspoint-reader#1019
* feat: show full path bar in file browser by @zgredex in
crosspoint-reader#1411
* feat: enable manual screen refresh on power button short press by
@bdeshi in
crosspoint-reader#1626
* feat: Rework "Cover + Custom" sleep screens to show covers only when
currently reading by @iandchasse in
crosspoint-reader#1256
* feat: Set sleep cover from BMP viewer by @el in
crosspoint-reader#1104
* feat: show crash reason on boot by @ngxson in
crosspoint-reader#1453
* feat: Support for proportional numeral spacing by @znelson in
crosspoint-reader#1414
* feat: add orientation-aware popups for reader activities by @mrtnvgr
in crosspoint-reader#1428
* feat: smooth battery percentage for x4 by @jonvex in
crosspoint-reader#1635
* feat: context-aware screenshot filenames with book title by
@jonstieglitz in
crosspoint-reader#1589
* feat(theme): add roundedraff theme and fix sleep cover crop grid
artifacts by @bunsoootchi in
crosspoint-reader#918
* feat: Page turn button orientation change by @mchuck in
crosspoint-reader#1069
* feat: Status bar for XTC files by @leecming82 in
crosspoint-reader#1849
* feat: enhance long press action to delete both files and directories
by @WuTofu in
crosspoint-reader#1803
* feat: Added Slovenian translation by @thehijacker in
crosspoint-reader#1551
* feat: focus reading by @vjapolitzer in
crosspoint-reader#1670
* feat: add next / prev labels to bmp viewer by @Telemaniaka in
crosspoint-reader#1852
* feat: add font family deletion functionality by @WuTofu in
crosspoint-reader#1919
* feat: separate into "Download All" and "Update All" in font manager by
@WuTofu in
crosspoint-reader#1955
* feat: verify CRC32 checksum for font files by @WuTofu in
crosspoint-reader#1904
* feat: increase default weight of Bitter font for improved rendering by
@uxjulia in
crosspoint-reader#1922
* feat: allow unnamed intervals by @steka in
crosspoint-reader#1903

### Fixes

* fix: epub images not rendering correctly on x3 by @itsthisjustin in
crosspoint-reader#1572
* fix: OTA update on x3 and progress bar on x4 and x3 by @itsthisjustin
in crosspoint-reader#1805
* fix: boot looping when opening large XTC files by @itsthisjustin in
crosspoint-reader#1648
* fix: Wild pointer crash in JPEGDEC MCU_SKIP handling by @itsthisjustin
in crosspoint-reader#1627
* fix: two small memory leaks by @Uri-Tauber in
crosspoint-reader#1628
* fix: use esp_http_client for KOSync to prevent TLS OOM on ESP32-C3 by
@trilwu in
crosspoint-reader#1381
* fix: Read GH release JSON as stream in OTA updater by @znelson in
crosspoint-reader#1810
* fix: support legacy XTC file headers where pageTableOffset=48 by
@uxjulia in
crosspoint-reader#1816
* fix: Use font metrics for combining mark positioning by @znelson in
crosspoint-reader#1310
* fix: Use differential rounding for consistent inter-glyph spacing by
@znelson in
crosspoint-reader#1413
* fix: Support hyphenation for EPUBs using ISO 639-2 language codes by
@znelson in
crosspoint-reader#1461
* fix: Track block style stack for nested styles by @daveallie in
crosspoint-reader#1582
* fix: cap per-side horizontal CSS inset at 2em by @rhoopr in
crosspoint-reader#1694
* fix: increase loadable epub size by @CSCMe in
crosspoint-reader#1638
* fix: Switch to xpath map for paragraph level syncing in KOSync by
@itsthisjustin in
crosspoint-reader#1686
* fix: free Epub RAM and simplify KOSync navigation via ActivityManager
by @wylanswets in
crosspoint-reader#1860
* fix: improve KOSync bidirectional position matching accuracy by
@wylanswets in
crosspoint-reader#1897
* fix: Fix failing very first wifi connection attempt by @jpirnay in
crosspoint-reader#1521
* fix: avoid skipping chapter after screenshot by @Mraulio in
crosspoint-reader#1625
* fix: back navigation from BMPViewer by @Telemaniaka in
crosspoint-reader#1597
* fix: Fix ghosting on exit of BMPViewer by @jpirnay in
crosspoint-reader#1432
* fix: make footnotes consider orientation for gutters by @Telemaniaka
in crosspoint-reader#1665
* fix: footnote link text by @steka in
crosspoint-reader#1666
* fix: Erroneous navigation with long filenames in footnote links by
@CSCMe in
crosspoint-reader#1723
* fix: prevent wallpaper clustering with 16-entry recency buffer by
@zgredex in
crosspoint-reader#1606
* fix: webserver /delete API backward compatibility by @DianaNites in
crosspoint-reader#1475
* fix: relative opds paths and query param with copyparty by @philips in
crosspoint-reader#1535
* fix: use same file name as KOReader for OPDS downloads by @spfenwick
in crosspoint-reader#1286
* fix: pressing space barely moves input cursor (crosspoint-reader#1729) by @pablohc in
crosspoint-reader#1733
* fix: keyboard feedback crosspoint-reader#1644 by @pablohc in
crosspoint-reader#1697
* fix: pluralize folder/file counts correctly in file list summary by
@fain182 in
crosspoint-reader#1701
* fix: rendering bug of scrollbar in RoundedRaff theme by @Uri-Tauber in
crosspoint-reader#1814
* fix: two roundedraff bugs by @Uri-Tauber in
crosspoint-reader#1851
* fix: overlap in download font list layout by @pablohc in
crosspoint-reader#1900
* fix: remove duplicate 'Download Fonts' menu entry and improve
navigation by @zgredex in
crosspoint-reader#1893
* fix: Add common ligatures to SD font conversion ranges by @znelson
* fix: capture instantiateVariableFont return value by @jpirnay in
crosspoint-reader#1911
* fix: Roundraff theme home menu offset with no recent books by @znelson
in crosspoint-reader#1845
* fix: Missing navigation button labels in Roundedraff theme by
@Uri-Tauber in
crosspoint-reader#1905
* fix: gracefully resolve fonts missing variants by @Uri-Tauber in
crosspoint-reader#1921
* fix: distribute justifyExtra to non-breaking space tokens by
@prawnwhoyawns in
crosspoint-reader#1783
* fix: remove percent rendering from activities by @mcrosson in
crosspoint-reader#1901
* fix: Restore performance in fontconvert_sdcard.py by @znelson in
crosspoint-reader#1924
* fix: Prepare SD card font caches from txt reader by @znelson in
crosspoint-reader#1973
* fix: make script help paths lightweight by @sabraman in
crosspoint-reader#1937
* fix: Replaced Bookerly with Noto Serif for licensing reasons by
@znelson in
crosspoint-reader#1736
* fix: incorrect y-axis scale factor in jpeg nearest-neighbor downscaler
by @WuTofu in
crosspoint-reader#1807
* fix: display empty lines in txt reader by @Uri-Tauber in
crosspoint-reader#1841
* fix: short-press power action triggered after screenshot combo release
by @pablohc in
crosspoint-reader#1853
* fix: correct Russian auto-turn translations by @a-ignatev in
crosspoint-reader#1566
* fix: Update Ukrainian translations for footnotes (issue 1409) by
@mirus-ua in
crosspoint-reader#1585
* fix: missing swedish translations by @steka in
crosspoint-reader#1667
* fix: Add swedish keyboard translations by @steka in
crosspoint-reader#1726
* fix: swedish translations by @steka in
crosspoint-reader#1762
* fix: swedish translation by @steka in
crosspoint-reader#1829
* fix: swedish translation by @steka in
crosspoint-reader#1888
* fix: Polish translation by @th0m4sek in
crosspoint-reader#1909
* fix: Ukrainian-translation by @KymAndriy in
crosspoint-reader#1946
* fix: Ukrainian translation by @KymAndriy in
crosspoint-reader#1939
* fix: python requirements files by @steka in
crosspoint-reader#1768
* fix: missing requirement by @steka in
crosspoint-reader#1896
* fix: Use LOG_ macros in loc functions by @znelson in
crosspoint-reader#1794

### Internal

* refactor: redesign on-screen keyboard by @pablohc in
crosspoint-reader#1644
* refactor: replace picojpeg with JPEGDEC for cover art conversion by
@jpirnay in
crosspoint-reader#1517
* refactor: Refactor drawArc / fillArc for faster execution by @jpirnay
in crosspoint-reader#1540
* perf: replace i18n pointer tables with offset tables, strip unused
strings by @jpirnay in
crosspoint-reader#1408
* refactor: Store only unique localization strings in offset buffers by
@znelson in
crosspoint-reader#1802
* refactor: Move language setting into JSON settings by @znelson in
crosspoint-reader#1796
* refactor: Use C++20 'requires' in ActivityResult constructor by
@znelson in
crosspoint-reader#1420
* refactor: Use default member initializers for JpegContext and
PngContext by @znelson in
crosspoint-reader#1435
* refactor: logPrintf and predefined log level strings by @CSCMe in
crosspoint-reader#1546
* refactor: RAII scoped open/close for ZipFile by @znelson in
crosspoint-reader#1433
* refactor: Deduplicated BMP header writing in Xtc by @znelson in
crosspoint-reader#1439
* refactor: Added shared XML parser teardown helper by @znelson in
crosspoint-reader#1438
* refactor: Removed redundant FsFile close() calls by @znelson in
crosspoint-reader#1434
* refactor: Deduplicate battery drawing code and fix Lyra charging
indicator by @znelson in
crosspoint-reader#1437
* refactor: Deduplicate Roundraff battery drawing by @znelson in
crosspoint-reader#1847
* refactor: Simplify sort in GfxRenderer::fillPolygon by @znelson in
crosspoint-reader#1817
* refactor: Avoid vector for page turn rates list by @znelson in
crosspoint-reader#1818
* refactor: Use std::size instead of sizeof/sizeof by @znelson in
crosspoint-reader#1819
* refactor: Use fixed-size integers for BookMetadataCache data by
@znelson in
crosspoint-reader#1844
* refactor: Simplify isReaderActivity bookkeeping by @znelson in
crosspoint-reader#1838
* refactor: Simplify XtcReaderActivity with detectPageTurn by @znelson
in crosspoint-reader#1837
* refactor: change ukrainian translation to adaptation and add missing
lines by @KymAndriy in
crosspoint-reader#1828
* chore: drop JPEGDEC patch in favour of upstream fix by @martinbrook in
crosspoint-reader#1465
* chore: clang-format.fix.ps1 script: Add .venv to list of path
exclusions by @jpirnay in
crosspoint-reader#1515
* chore: Updating sleep screen dimensions for X3 by @jensechu in
crosspoint-reader#1688
* chore: Clarify X3 RTC in SCOPE.md by @znelson in
crosspoint-reader#1687
* chore: Improved Italian translations by @znelson in
crosspoint-reader#1685
* chore: change ukrainian translation to adaptation by @KymAndriy in
crosspoint-reader#1684
* chore: Update spanish.yaml by @mvidelatraduc in
crosspoint-reader#1717
* chore: One Italian translation tweak by @znelson in
crosspoint-reader#1718
* chore: git pre-commit hook for format fix by @osteotek in
crosspoint-reader#1730
* chore: Update SDK to fork in CrossPoint org by @znelson in
crosspoint-reader#1836
* chore: Added RAM to firmware_size_history.py script by @znelson in
crosspoint-reader#1830
* chore: Updated docs to reflect DESTRUCTOR_CLOSES_FILE=1 by @znelson in
crosspoint-reader#1878
* feat: cap compressed group size at 64 KB by @jpirnay in
crosspoint-reader#1913
* fix: build-script bug fixes for fontconvert{,_sdcard}.py by @jpirnay
in crosspoint-reader#1910
* feat: include short SHA in CROSSPOINT_VERSION by @osteotek in
crosspoint-reader#1728
* feat: show long branch names by @steka in
crosspoint-reader#1727
* feat: enable pio build cache by @Uri-Tauber in
crosspoint-reader#1769
* style: put page name first in browser titles by @fain182 in
crosspoint-reader#1703
* style: unify page headers across web UI by @fain182 in
crosspoint-reader#1702
* style: move file type badges into Type column by @fain182 in
crosspoint-reader#1793
* style: align action buttons vertically with page title by @fain182 in
crosspoint-reader#1795
* docs: Update README with firmware flashing instructions by @ryneches
in crosspoint-reader#1654
* docs: fix typos by @kianmeng in
crosspoint-reader#1705
* docs: update README.md to reflect the current state of crosspoint by
@Uri-Tauber in
crosspoint-reader#1812
* docs: Add documentation for USB-locked Xteink devices by
@itsthisjustin in
crosspoint-reader#1990
* docs: expand first use of OPDS acronym and provide a wikipedia link by
@sizezero in
crosspoint-reader#1824
* docs: fix KOReader sync guide link by @sabraman in
crosspoint-reader#1930
* docs: fix hyphenation updater script name by @sabraman in
crosspoint-reader#1931
* fix: sd font download urls in docs by @mcrosson in
crosspoint-reader#1945
* fix: sd font folder paths in documentation by @mcrosson in
crosspoint-reader#1944
* chore: Add verbose mode to build-sd-fonts.py by @znelson in
crosspoint-reader#1923

## New Contributors
* @a-ignatev made their first contribution in
crosspoint-reader#1566
* @CSCMe made their first contribution in
crosspoint-reader#1546
* @thehijacker made their first contribution in
crosspoint-reader#1551
* @Telemaniaka made their first contribution in
crosspoint-reader#1597
* @Mraulio made their first contribution in
crosspoint-reader#1625
* @rxmmah made their first contribution in
crosspoint-reader#1462
* @bdeshi made their first contribution in
crosspoint-reader#1626
* @DianaNites made their first contribution in
crosspoint-reader#1475
* @ryneches made their first contribution in
crosspoint-reader#1654
* @zgredex made their first contribution in
crosspoint-reader#1411
* @jonvex made their first contribution in
crosspoint-reader#1635
* @KymAndriy made their first contribution in
crosspoint-reader#1684
* @jensechu made their first contribution in
crosspoint-reader#1688
* @kianmeng made their first contribution in
crosspoint-reader#1705
* @philips made their first contribution in
crosspoint-reader#1535
* @fain182 made their first contribution in
crosspoint-reader#1701
* @mvidelatraduc made their first contribution in
crosspoint-reader#1717
* @bunsoootchi made their first contribution in
crosspoint-reader#918
* @rhoopr made their first contribution in
crosspoint-reader#1694
* @spfenwick made their first contribution in
crosspoint-reader#1286
* @trilwu made their first contribution in
crosspoint-reader#1381
* @jonstieglitz made their first contribution in
crosspoint-reader#1589
* @uxjulia made their first contribution in
crosspoint-reader#1816
* @mchuck made their first contribution in
crosspoint-reader#1069
* @sizezero made their first contribution in
crosspoint-reader#1824
* @leecming82 made their first contribution in
crosspoint-reader#1849
* @jeremydk made their first contribution in
crosspoint-reader#1780
* @WuTofu made their first contribution in
crosspoint-reader#1803
* @wylanswets made their first contribution in
crosspoint-reader#1860
* @sabraman made their first contribution in
crosspoint-reader#1930
* @prawnwhoyawns made their first contribution in
crosspoint-reader#1783
* @mcrosson made their first contribution as co-author on SD card font
support

**Full Changelog**:
crosspoint-reader/crosspoint-reader@1.2.0...release/1.3.0

---------

Co-authored-by: Justin Mitchell <justin@jmitch.com>
Co-authored-by: Chun Ming Lee <95391408+leecming82@users.noreply.github.com>
Co-authored-by: Uri Tauber <uritaube@gmail.com>
…rosspoint-reader#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_
## Summary
* **What is the goal of this PR?** Update the Italian translation.
* **What changes are included?** Took the latest `english.yaml` as
reference and updated `italian.yaml` accordingly, translating new
strings and revising existing ones where needed. Specific changes can be
inspected from the diff.

## Additional Context
* Nothing special to flag — happy to adjust any wording the reviewer
disagrees with.

---
### 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**_ — Claude
provided a first-pass draft; I revised and rewrote a substantial portion
by hand.
## 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**_
## Summary

* Add swedish hyphenation using scripts/update_hypenation.sh
* Add hyphenation test data using the Swedish translation of Andy Weir's
Project Hail Mary

---

### 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**_
…#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: crosspoint-reader/community-sdk#3

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

- Fixes: crosspoint-reader#1881

---

### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
## 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**_
…ader#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!
Co-authored-by: muhas <mail@muhas.name>
Adds a 5th short press power button option 'Action Menu' that opens a
navigable popup with Sleep, Refresh Screen, and Screenshot actions.
Includes theme-aware popup rendering (Classic/Lyra/RoundedRaff) using
drawDialogBackground and drawPopupSelection virtual methods.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/activities/reader/KOReaderSyncActivity.cpp (1)

296-303: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only tear down Wi‑Fi when this activity actually brought it up.

wifiActivated is set before the already-connected fast path, so entering this screen while Wi‑Fi is already up will still disconnect that existing session and force a silent restart on exit. That makes KOReaderSyncActivity steal ownership from the caller that originally enabled Wi‑Fi.

Suggested fix
-  // Past this point every path uses WiFi.
-  wifiActivated = true;
-
   // Check if already connected (e.g. from settings page auth)
   if (WiFi.status() == WL_CONNECTED) {
     LOG_DBG("KOSync", "Already connected to WiFi");
     onWifiSelectionComplete(true);
     return;
   }
+
+  // We only own WiFi teardown when this activity had to bring it up.
+  wifiActivated = true;

Also applies to: 315-319

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/activities/reader/KOReaderSyncActivity.cpp` around lines 296 - 303, The
code sets wifiActivated before taking the "already connected" fast path, causing
KOReaderSyncActivity to later tear down Wi‑Fi it did not start; move the
assignment of wifiActivated = true so it only runs after you know this activity
actually enabled Wi‑Fi (i.e., after the branch that checks WiFi.status() ==
WL_CONNECTED and returns), and ensure any teardown logic (the code paths
referenced around onWifiSelectionComplete and the block at the other mentioned
spot) checks wifiActivated before disabling or restarting Wi‑Fi so only sessions
started by KOReaderSyncActivity are stopped.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/Epub/Epub.cpp`:
- Around line 641-642: Check the result of
readItemContentsToStream(coverImageHref, coverJpg, 1024, deadline) (and the
similar call at the 678-679 spot) and abort the subsequent cover-conversion path
if the stream read failed or the deadline was exceeded: if the read returns a
failure/false or an exception occurs, ensure the temp file is closed/removed and
return or throw early instead of calling the cover conversion routine; update
the code around readItemContentsToStream, coverImageHref and coverJpg to gate
the conversion step on a successful read and propagate/handle the error
appropriately.

In `@lib/Epub/Epub/hyphenation/HyphenationCommon.cpp`:
- Around line 23-25: The parity-based Latin Extended-A lowering incorrectly maps
U+0130 to U+0131; update the parity-based lowering block to special-case cp ==
0x0130 and return 0x0069 before applying the existing parity rules: check if (cp
== 0x0130) return 0x0069; then keep the existing if ((cp >= 0x0100 ... ) ...) {
return cp + 1; } logic so U+0130 maps to LATIN SMALL LETTER I (0x0069) instead
of U+0131.

In `@lib/I18n/translations/russian.yaml`:
- Line 321: Fix the typo in the Russian translation for the font installation
failure message by updating the value of STR_FONT_INSTALL_FAILED to "Не удалось
установить шрифт" (correcting "удалоь" → "удалось") so the user-facing string is
spelled correctly.
- Line 347: The translation string STR_KB_HINT_CLEAR_TEXT contains a typo:
replace the incorrect word "очитисть" with the correct "очистить" so the value
becomes "Удерживайте <— чтобы очистить весь текст"; update the value for the
STR_KB_HINT_CLEAR_TEXT key in the translations file accordingly.

In `@src/activities/network/CalibreConnectActivity.cpp`:
- Around line 55-59: The current exit path in CalibreConnectActivity.cpp calls
silentRestart() whenever WiFi is active, causing a full device reboot on normal
exit; change the flow so WiFi.getMode() != WIFI_MODE_NULL only triggers
WiFi.disconnect(false) and delay(30) and then returns/continues without calling
silentRestart(). Remove the unconditional silentRestart() from this normal-exit
branch (or move it behind an explicit fatal-error condition), and ensure any
remaining reboot behavior is only invoked from an explicit error handler, not
from the normal exit code in this activity.

In `@src/activities/network/CrossPointWebServerActivity.cpp`:
- Around line 79-87: The reboot path currently jumps to silentRestart() without
shutting down the web stack; call the CrossPointWebServer stop routine before
reboot so active WebSocket uploads are aborted and partial files cleaned up.
Locate the block around WiFi.getMode() and, before calling silentRestart(),
invoke the web server cleanup (e.g. CrossPointWebServer::stop() or the instance
method like crossPointWebServer.stop()), guarding for a null/uninitialized
instance or already-stopped state so the call is safe; ensure this happens
regardless of isApMode/softAPdisconnect vs WiFi.disconnect().

In `@src/activities/settings/FontDownloadActivity.cpp`:
- Around line 458-477: When retrying a failed single-family download the
progress counters aren't reset, so ensure you set currentFileIndex_ = 0 and
currentFileTotal_ = family.files.size() before calling downloadFamily(family) on
retry; locate the retry branch that currently calls downloadFamily() directly
(the same branch that handles non-installed or hasUpdate when using
familyIndexFromList(selectedIndex_)) and add the same counter initialization
used elsewhere (the blocks that call downloadAll() and updateAll()) so progress
text reflects the restarted download.
- Around line 191-197: updateAll() is calling downloadFamily() for families_[i]
with hasUpdate set, but downloadFamily() currently uses the destructive install
path that deletes an existing family on abort/failure; change the update flow so
that updates are non-destructive: modify downloadFamily() (or add a new
downloadFamilyForUpdate() overload/flag) to download into a temporary location
and only replace the installed family on successful completion, and on
abort/failure/cancel leave the existing installed family untouched; update
callers (updateAll(), and other places that invoked downloadFamily() for
updates) to pass the update-mode flag or call the new non-destructive helper,
and ensure error/cancel branches do not call the deletion/cleanup that removes
the existing installed font.

In `@src/components/themes/BaseTheme.cpp`:
- Around line 459-474: When drawBitmap(bitmap, ...) returns false, clear the
cached-cover state so stale buffers aren't reused: in the else branch where
hasCoverImage is set to false and book dimensions recomputed, also set
coverRendered = false and coverBufferStored = false (and if there's a cover
buffer pointer/handle, reset/clear it too); update the else branch near the
drawBitmap call and ensure the symbols coverRendered, coverBufferStored, and any
cover buffer handle are reset there.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp`:
- Around line 49-68: The code currently leaves hasCover true when
Storage.openFileForRead("HOME", coverBmpPath, file) returns false; update the
open-failure branch in Lyra3CoversTheme.cpp (the block using
Storage.openFileForRead, Bitmap, renderer.drawBitmap and the hasCover flag) to
explicitly set hasCover = false when openFileForRead fails, and only call
file.close() or proceed with Bitmap parsing/drawing when the file was
successfully opened (ensure the early-fail path does not attempt to use file or
skip the empty-cover fallback).

In `@src/components/UITheme.cpp`:
- Around line 74-104: UITheme::getScreenSafeArea currently ignores the
hasSideButtonHints parameter, so side button hint areas are not subtracted from
the returned safeArea; update the switch handling in UITheme::getScreenSafeArea
to also subtract currentMetrics->buttonHintsHeight from the appropriate edges
when hasSideButtonHints is true (for LandscapeClockwise and
LandscapeCounterClockwise adjust x/width appropriately; for
Portrait/PortraitInverted adjust y/height if side hints apply there), preserving
the existing hasFrontButtonHints logic and using the same
currentMetrics->buttonHintsHeight value.

In `@src/main.cpp`:
- Around line 510-515: Menu-triggered screenshots use the normal framebuffer
path unconditionally, causing blank/incorrect images in the FactoryLut display
state; update the PowerButtonMenuActivity::MenuAction::SCREENSHOT case so after
acquiring RenderLock it checks renderer->getDisplayState() for
GfxRenderer::DisplayState::FactoryLut and, if so, invoke the same
FactoryLut-specific screenshot logic used by the combo handler (mirror that code
path), otherwise call ScreenshotUtil::takeScreenshot(renderer); keep RenderLock
scope the same and reuse the exact FactoryLut-handling function or code block
the combo handler uses to ensure identical behavior.

In `@src/network/html/FilesPage.html`:
- Around line 4906-4908: The code currently sets ok = !conversionFailed &&
conversionSucceeded which treats fallback-to-original as a failure; instead,
mark the file as successful because this block runs after a successful upload.
Replace the ok calculation (in the save-to-log path using saveToFileBatchLog and
the variables conversionFailed, conversionSucceeded, convOriginalSize,
convNewSize) so the file is considered successful (ok = true) and pass sizes
accordingly: use convOriginalSize as the original size and use convNewSize only
when conversionSucceeded, otherwise use convOriginalSize for the delivered size
so fallback uploads are not counted as failed.
- Around line 4430-4440: The code decodes src with decodeHref for matching but
then writes back decodedSrc which strips percent-encodings; instead, keep the
original encoded src for the DOM update: use decodeHref(src) and
resolvePath(xhtmlPath, decodedSrc) to find a matching entry in renamed, but when
updating img.setAttribute('src') replace only the last path segment of the
original src (the one after the final '/') with the new basename
(newPath.split('/').pop()) and URL-encode that basename (e.g. via
encodeURIComponent) before inserting so percent-encoding is preserved; update
modified = true as before.

---

Outside diff comments:
In `@src/activities/reader/KOReaderSyncActivity.cpp`:
- Around line 296-303: The code sets wifiActivated before taking the "already
connected" fast path, causing KOReaderSyncActivity to later tear down Wi‑Fi it
did not start; move the assignment of wifiActivated = true so it only runs after
you know this activity actually enabled Wi‑Fi (i.e., after the branch that
checks WiFi.status() == WL_CONNECTED and returns), and ensure any teardown logic
(the code paths referenced around onWifiSelectionComplete and the block at the
other mentioned spot) checks wifiActivated before disabling or restarting Wi‑Fi
so only sessions started by KOReaderSyncActivity are stopped.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 88c20477-194a-48df-847b-e7af688728b4

📥 Commits

Reviewing files that changed from the base of the PR and between f896238 and ed1f342.

⛔ Files ignored due to path filters (2)
  • lib/Epub/Epub/hyphenation/generated/hyph-pl.trie.h is excluded by !**/generated/**
  • lib/Epub/Epub/hyphenation/generated/hyph-sv.trie.h is excluded by !**/generated/**
📒 Files selected for processing (73)
  • .skills/SKILL.md
  • README.md
  • lib/EpdFont/SdCardFont.cpp
  • lib/EpdFont/SdCardFont.h
  • lib/EpdFont/scripts/sd-fonts.yaml
  • lib/Epub/Epub.cpp
  • lib/Epub/Epub.h
  • lib/Epub/Epub/ParsedText.cpp
  • lib/Epub/Epub/hyphenation/HyphenationCommon.cpp
  • lib/Epub/Epub/hyphenation/Hyphenator.cpp
  • lib/Epub/Epub/hyphenation/LanguageRegistry.cpp
  • lib/GfxRenderer/GfxRenderer.cpp
  • lib/GfxRenderer/GfxRenderer.h
  • lib/I18n/translations/english.yaml
  • lib/I18n/translations/italian.yaml
  • lib/I18n/translations/russian.yaml
  • lib/I18n/translations/spanish.yaml
  • lib/JpegToBmpConverter/JpegToBmpConverter.cpp
  • lib/JpegToBmpConverter/JpegToBmpConverter.h
  • lib/KOReaderSync/KOReaderSyncClient.cpp
  • lib/Memory/Memory.h
  • lib/PngToBmpConverter/PngToBmpConverter.cpp
  • lib/PngToBmpConverter/PngToBmpConverter.h
  • lib/ZipFile/ZipFile.cpp
  • lib/ZipFile/ZipFile.h
  • lib/hal/HalGPIO.cpp
  • lib/hal/HalGPIO.h
  • platformio.ini
  • scripts/update_hyphenation.sh
  • src/CrossPointSettings.h
  • src/CrossPointState.h
  • src/SdCardFontGlobals.h
  • src/SdCardFontSystem.h
  • src/SettingsList.h
  • src/SilentRestart.h
  • src/activities/ActivityManager.cpp
  • src/activities/boot_sleep/BootActivity.cpp
  • src/activities/boot_sleep/SleepActivity.cpp
  • src/activities/browser/OpdsBookBrowserActivity.cpp
  • src/activities/network/CalibreConnectActivity.cpp
  • src/activities/network/CrossPointWebServerActivity.cpp
  • src/activities/network/CrossPointWebServerActivity.h
  • src/activities/reader/KOReaderSyncActivity.cpp
  • src/activities/reader/KOReaderSyncActivity.h
  • src/activities/reader/ReaderActivity.cpp
  • src/activities/reader/TxtReaderActivity.cpp
  • src/activities/settings/FontDownloadActivity.cpp
  • src/activities/settings/FontDownloadActivity.h
  • src/activities/settings/FontSelectionActivity.cpp
  • src/activities/settings/LanguageSelectActivity.cpp
  • src/activities/settings/SettingsActivity.cpp
  • src/activities/util/PowerButtonMenuActivity.cpp
  • src/activities/util/PowerButtonMenuActivity.h
  • src/components/UITheme.cpp
  • src/components/UITheme.h
  • src/components/icons/cog.h
  • src/components/icons/settings.h
  • src/components/themes/BaseTheme.cpp
  • src/components/themes/BaseTheme.h
  • src/components/themes/lyra/Lyra3CoversTheme.cpp
  • src/components/themes/lyra/LyraTheme.cpp
  • src/components/themes/lyra/LyraTheme.h
  • src/components/themes/roundedraff/RoundedRaffTheme.cpp
  • src/components/themes/roundedraff/RoundedRaffTheme.h
  • src/images/CrossLarge.h
  • src/main.cpp
  • src/network/CrossPointWebServer.cpp
  • src/network/HttpDownloader.cpp
  • src/network/HttpDownloader.h
  • src/network/html/FilesPage.html
  • test/hyphenation_eval/HyphenationEvaluationTest.cpp
  • test/hyphenation_eval/resources/polish_hyphenation_tests.txt
  • test/hyphenation_eval/resources/swedish_hyphenation_tests.txt
💤 Files with no reviewable changes (5)
  • src/SdCardFontGlobals.h
  • src/components/icons/cog.h
  • src/activities/ActivityManager.cpp
  • src/components/icons/settings.h
  • src/activities/network/CrossPointWebServerActivity.h
✅ Files skipped from review due to trivial changes (6)
  • src/activities/settings/SettingsActivity.cpp
  • src/SdCardFontSystem.h
  • platformio.ini
  • src/images/CrossLarge.h
  • scripts/update_hyphenation.sh
  • lib/I18n/translations/english.yaml
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/activities/reader/ReaderActivity.cpp
  • src/SettingsList.h
  • src/CrossPointSettings.h
  • src/components/UITheme.h
  • src/activities/boot_sleep/SleepActivity.cpp
  • lib/GfxRenderer/GfxRenderer.cpp

Comment thread lib/Epub/Epub.cpp
Comment on lines +641 to 642
readItemContentsToStream(coverImageHref, coverJpg, 1024, deadline);
// Explicitly close() file before reopening for reading
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle failed cover extraction before conversion.

If the deadline is exceeded (or stream read fails), conversion still proceeds on partial/invalid temp data. This defeats early cancellation and wastes work.

💡 Proposed fix
-    readItemContentsToStream(coverImageHref, coverJpg, 1024, deadline);
+    if (!readItemContentsToStream(coverImageHref, coverJpg, 1024, deadline)) {
+      coverJpg.close();
+      Storage.remove(coverJpgTempPath.c_str());
+      return false;
+    }
@@
-    readItemContentsToStream(coverImageHref, coverPng, 1024, deadline);
+    if (!readItemContentsToStream(coverImageHref, coverPng, 1024, deadline)) {
+      coverPng.close();
+      Storage.remove(coverPngTempPath.c_str());
+      return false;
+    }

Also applies to: 678-679

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Epub/Epub.cpp` around lines 641 - 642, Check the result of
readItemContentsToStream(coverImageHref, coverJpg, 1024, deadline) (and the
similar call at the 678-679 spot) and abort the subsequent cover-conversion path
if the stream read failed or the deadline was exceeded: if the read returns a
failure/false or an exception occurs, ensure the temp file is closed/removed and
return or throw early instead of calling the cover conversion routine; update
the code around readItemContentsToStream, coverImageHref and coverJpg to gate
the conversion step on a successful read and propagate/handle the error
appropriately.

Comment on lines +23 to +25
if ((cp >= 0x0100 && cp <= 0x0137 && (cp % 2 == 0)) || (cp >= 0x0139 && cp <= 0x0148 && (cp % 2 == 1)) ||
(cp >= 0x014A && cp <= 0x0177 && (cp % 2 == 0)) || (cp >= 0x0179 && cp <= 0x017E && (cp % 2 == 1))) {
return cp + 1;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the Unicode simple lowercase mapping for U+0130 (LATIN CAPITAL LETTER I WITH DOT ABOVE)?

💡 Result:

The Unicode simple lowercase mapping for U+0130 (LATIN CAPITAL LETTER I WITH DOT ABOVE) is U+0069 (LATIN SMALL LETTER I) [1]. This is the “simple” (locale-neutral) lowercase mapping from the Unicode character’s simple lowercase mapping data: U+0130 → U+0069 [1]. Note that Unicode also defines a special Turkish/Azeri locale-aware behavior where lowercasing U+0130 produces the two-code-point sequence U+0069 U+0307 (i plus combining dot above), but that is not the “simple” mapping asked for [2].

Citations:


🏁 Script executed:

# First, locate the file
find . -name "HyphenationCommon.cpp" -type f

Repository: pablohc/crosspoint-reader

Length of output: 119


🏁 Script executed:

# Read the file around lines 23-25 to verify the code snippet
sed -n '1,50p' ./lib/Epub/Epub/hyphenation/HyphenationCommon.cpp | cat -n

Repository: pablohc/crosspoint-reader

Length of output: 1990


🏁 Script executed:

# Verify U+0130 falls into the problematic range and parity check
python3 << 'EOF'
cp = 0x0130
print(f"U+0130 = {cp} decimal")
print(f"In range 0x0100-0x0137? {0x0100 <= cp <= 0x0137}")
print(f"Parity (even)? {cp % 2 == 0}")
print(f"Would be mapped to: 0x{cp + 1:04X}")
print()
print("Correct mapping should be: 0x0069 (LATIN SMALL LETTER I)")
EOF

Repository: pablohc/crosspoint-reader

Length of output: 224


🏁 Script executed:

# Check if there are any existing special cases for U+0130 elsewhere in the function
grep -n "0x0130\|0x0131" ./lib/Epub/Epub/hyphenation/HyphenationCommon.cpp

Repository: pablohc/crosspoint-reader

Length of output: 51


Special-case U+0130 before parity-based Latin Extended-A lowering.

The parity rule maps U+0130 to U+0131, which violates Unicode case-mapping semantics. The correct simple lowercase mapping for U+0130 (LATIN CAPITAL LETTER I WITH DOT ABOVE) is U+0069 (LATIN SMALL LETTER I), not U+0131. This causes incorrect normalization of Turkish and other language text.

Proposed fix
 uint32_t toLowerLatinImpl(const uint32_t cp) {
+  // Unicode special casing: LATIN CAPITAL LETTER I WITH DOT ABOVE
+  // (simple lowercase mapping in single-codepoint form).
+  if (cp == 0x0130) {
+    return 0x0069;  // i
+  }
+
   if (cp >= 'A' && cp <= 'Z') {
     return cp - 'A' + 'a';
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Epub/Epub/hyphenation/HyphenationCommon.cpp` around lines 23 - 25, The
parity-based Latin Extended-A lowering incorrectly maps U+0130 to U+0131; update
the parity-based lowering block to special-case cp == 0x0130 and return 0x0069
before applying the existing parity rules: check if (cp == 0x0130) return
0x0069; then keep the existing if ((cp >= 0x0100 ... ) ...) { return cp + 1; }
logic so U+0130 maps to LATIN SMALL LETTER I (0x0069) instead of U+0131.

STR_LOADING_FONT_LIST: "Загрузка списка шрифтов..."
STR_NO_FONTS_AVAILABLE: "Шрифты недоступны"
STR_FONT_INSTALLED: "Шрифт установлен!"
STR_FONT_INSTALL_FAILED: "Не удалоь установить шрифт"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in installation failure message.

"Не удалоь установить шрифт" has a typo and should be "Не удалось установить шрифт".

Proposed fix
-STR_FONT_INSTALL_FAILED: "Не удалоь установить шрифт"
+STR_FONT_INSTALL_FAILED: "Не удалось установить шрифт"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
STR_FONT_INSTALL_FAILED: "Не удалоь установить шрифт"
STR_FONT_INSTALL_FAILED: "Не удалось установить шрифт"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/I18n/translations/russian.yaml` at line 321, Fix the typo in the Russian
translation for the font installation failure message by updating the value of
STR_FONT_INSTALL_FAILED to "Не удалось установить шрифт" (correcting "удалоь" →
"удалось") so the user-facing string is spelled correctly.

STR_KB_TIPS: "Подсказки:"
STR_KB_HINT_RETURN_KEYBOARD: "Нажмите ВНИЗ, чтобы вернуться к клавиатуре"
STR_KB_HINT_EXIT_URL_MODE: "Нажмите ABC для выхода из режима URL"
STR_KB_HINT_CLEAR_TEXT: "Удерживайте <— чтобы очитисть весь текст"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in keyboard hint text.

"очитисть" should be "очистить".

Proposed fix
-STR_KB_HINT_CLEAR_TEXT: "Удерживайте <— чтобы очитисть весь текст"
+STR_KB_HINT_CLEAR_TEXT: "Удерживайте <— чтобы очистить весь текст"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
STR_KB_HINT_CLEAR_TEXT: "Удерживайте <— чтобы очитисть весь текст"
STR_KB_HINT_CLEAR_TEXT: "Удерживайте <— чтобы очистить весь текст"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/I18n/translations/russian.yaml` at line 347, The translation string
STR_KB_HINT_CLEAR_TEXT contains a typo: replace the incorrect word "очитисть"
with the correct "очистить" so the value becomes "Удерживайте <— чтобы очистить
весь текст"; update the value for the STR_KB_HINT_CLEAR_TEXT key in the
translations file accordingly.

Comment on lines +55 to +59
if (WiFi.getMode() != WIFI_MODE_NULL) {
WiFi.disconnect(false);
delay(30);
silentRestart();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid rebooting on normal Calibre activity exit.

This path now restarts the whole device whenever Wi‑Fi is active, so a normal “exit” becomes a full reboot. That’s a high-impact UX/regression risk for a routine navigation flow.

💡 Proposed fix
 void CalibreConnectActivity::onExit() {
   Activity::onExit();
 
-  if (WiFi.getMode() != WIFI_MODE_NULL) {
-    WiFi.disconnect(false);
-    delay(30);
-    silentRestart();
-  }
+  stopWebServer();
+  MDNS.end();
+  if (WiFi.getMode() != WIFI_MODE_NULL) {
+    WiFi.disconnect(true);
+    delay(30);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/activities/network/CalibreConnectActivity.cpp` around lines 55 - 59, The
current exit path in CalibreConnectActivity.cpp calls silentRestart() whenever
WiFi is active, causing a full device reboot on normal exit; change the flow so
WiFi.getMode() != WIFI_MODE_NULL only triggers WiFi.disconnect(false) and
delay(30) and then returns/continues without calling silentRestart(). Remove the
unconditional silentRestart() from this normal-exit branch (or move it behind an
explicit fatal-error condition), and ensure any remaining reboot behavior is
only invoked from an explicit error handler, not from the normal exit code in
this activity.

Comment on lines +49 to 68
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
float coverHeight = static_cast<float>(bitmap.getHeight());
float coverWidth = static_cast<float>(bitmap.getWidth());
float ratio = coverWidth / coverHeight;
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight);
float cropX = 1.0f - (tileRatio / ratio);

if (!renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
tileWidth - 2 * hPaddingInSelection,
Lyra3CoversMetrics::values.homeCoverHeight, cropX)) {
hasCover = false;
}
} else {
hasCover = false;
}
file.close();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle failed cover file open explicitly.

When Storage.openFileForRead(...) fails, hasCover stays true, so the empty-cover fallback is skipped. Set hasCover = false on the open-failure path.

Proposed fix
             FsFile file;
             if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
               Bitmap bitmap(file);
               if (bitmap.parseHeaders() == BmpReaderError::Ok) {
                 float coverHeight = static_cast<float>(bitmap.getHeight());
                 float coverWidth = static_cast<float>(bitmap.getWidth());
                 float ratio = coverWidth / coverHeight;
                 const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
                                         static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight);
                 float cropX = 1.0f - (tileRatio / ratio);

                 if (!renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
                                          tileWidth - 2 * hPaddingInSelection,
                                          Lyra3CoversMetrics::values.homeCoverHeight, cropX)) {
                   hasCover = false;
                 }
               } else {
                 hasCover = false;
               }
               file.close();
+            } else {
+              hasCover = false;
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
float coverHeight = static_cast<float>(bitmap.getHeight());
float coverWidth = static_cast<float>(bitmap.getWidth());
float ratio = coverWidth / coverHeight;
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight);
float cropX = 1.0f - (tileRatio / ratio);
if (!renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
tileWidth - 2 * hPaddingInSelection,
Lyra3CoversMetrics::values.homeCoverHeight, cropX)) {
hasCover = false;
}
} else {
hasCover = false;
}
file.close();
}
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
float coverHeight = static_cast<float>(bitmap.getHeight());
float coverWidth = static_cast<float>(bitmap.getWidth());
float ratio = coverWidth / coverHeight;
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight);
float cropX = 1.0f - (tileRatio / ratio);
if (!renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
tileWidth - 2 * hPaddingInSelection,
Lyra3CoversMetrics::values.homeCoverHeight, cropX)) {
hasCover = false;
}
} else {
hasCover = false;
}
file.close();
} else {
hasCover = false;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp` around lines 49 - 68, The
code currently leaves hasCover true when Storage.openFileForRead("HOME",
coverBmpPath, file) returns false; update the open-failure branch in
Lyra3CoversTheme.cpp (the block using Storage.openFileForRead, Bitmap,
renderer.drawBitmap and the hasCover flag) to explicitly set hasCover = false
when openFileForRead fails, and only call file.close() or proceed with Bitmap
parsing/drawing when the file was successfully opened (ensure the early-fail
path does not attempt to use file or skip the empty-cover fallback).

Comment on lines +74 to +104
Rect UITheme::getScreenSafeArea(const GfxRenderer& renderer, bool hasFrontButtonHints, bool hasSideButtonHints) {
auto orientation = renderer.getOrientation();
const int screenWidth = renderer.getScreenWidth();
const int screenHeight = renderer.getScreenHeight();
Rect safeArea = Rect{0, 0, screenWidth, screenHeight};
switch (orientation) {
case GfxRenderer::Orientation::Portrait:
if (hasFrontButtonHints) {
safeArea.height -= currentMetrics->buttonHintsHeight;
}
break;
case GfxRenderer::Orientation::LandscapeClockwise:
if (hasFrontButtonHints) {
safeArea.x += currentMetrics->buttonHintsHeight;
safeArea.width -= currentMetrics->buttonHintsHeight;
}
break;
case GfxRenderer::Orientation::PortraitInverted:
if (hasFrontButtonHints) {
safeArea.y += currentMetrics->buttonHintsHeight;
safeArea.height -= currentMetrics->buttonHintsHeight;
}
break;
case GfxRenderer::Orientation::LandscapeCounterClockwise:
if (hasFrontButtonHints) {
safeArea.width -= currentMetrics->buttonHintsHeight;
}
break;
}
return safeArea;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor hasSideButtonHints in the safe-area calculation.

The new helper accepts hasSideButtonHints, but the flag is never used. Callers that pass true will still lay out content into the full rect, so side button hints can overlap the UI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/UITheme.cpp` around lines 74 - 104, UITheme::getScreenSafeArea
currently ignores the hasSideButtonHints parameter, so side button hint areas
are not subtracted from the returned safeArea; update the switch handling in
UITheme::getScreenSafeArea to also subtract currentMetrics->buttonHintsHeight
from the appropriate edges when hasSideButtonHints is true (for
LandscapeClockwise and LandscapeCounterClockwise adjust x/width appropriately;
for Portrait/PortraitInverted adjust y/height if side hints apply there),
preserving the existing hasFrontButtonHints logic and using the same
currentMetrics->buttonHintsHeight value.

Comment thread src/main.cpp
Comment on lines +510 to +515
case PowerButtonMenuActivity::MenuAction::SCREENSHOT:
LOG_DBG("MAIN", "Power button menu: screenshot");
{
RenderLock lock;
ScreenshotUtil::takeScreenshot(renderer);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mirror the FactoryLut screenshot path here too.

The combo handler special-cases GfxRenderer::DisplayState::FactoryLut because the normal framebuffer screenshot is wrong in that state. This menu action still uses the normal path unconditionally, so menu-triggered screenshots can come out blank/incorrect on the same screen.

💡 Suggested fix
       case PowerButtonMenuActivity::MenuAction::SCREENSHOT:
         LOG_DBG("MAIN", "Power button menu: screenshot");
         {
           RenderLock lock;
-          ScreenshotUtil::takeScreenshot(renderer);
+          if (renderer.getDisplayState() == GfxRenderer::DisplayState::FactoryLut) {
+            if (Activity* activity = activityManager.getCurrentActivity()) {
+              ScreenshotUtil::prepareFactoryLutScreenshot(renderer);
+              activity->onScreenshotRequest();
+            }
+          } else {
+            ScreenshotUtil::takeScreenshot(renderer);
+          }
         }
         break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case PowerButtonMenuActivity::MenuAction::SCREENSHOT:
LOG_DBG("MAIN", "Power button menu: screenshot");
{
RenderLock lock;
ScreenshotUtil::takeScreenshot(renderer);
}
case PowerButtonMenuActivity::MenuAction::SCREENSHOT:
LOG_DBG("MAIN", "Power button menu: screenshot");
{
RenderLock lock;
if (renderer.getDisplayState() == GfxRenderer::DisplayState::FactoryLut) {
if (Activity* activity = activityManager.getCurrentActivity()) {
ScreenshotUtil::prepareFactoryLutScreenshot(renderer);
activity->onScreenshotRequest();
}
} else {
ScreenshotUtil::takeScreenshot(renderer);
}
}
break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.cpp` around lines 510 - 515, Menu-triggered screenshots use the
normal framebuffer path unconditionally, causing blank/incorrect images in the
FactoryLut display state; update the
PowerButtonMenuActivity::MenuAction::SCREENSHOT case so after acquiring
RenderLock it checks renderer->getDisplayState() for
GfxRenderer::DisplayState::FactoryLut and, if so, invoke the same
FactoryLut-specific screenshot logic used by the combo handler (mirror that code
path), otherwise call ScreenshotUtil::takeScreenshot(renderer); keep RenderLock
scope the same and reuse the exact FactoryLut-handling function or code block
the combo handler uses to ensure identical behavior.

Comment on lines +4430 to +4440
const src = img.getAttribute('src');
if (src) {
const decodedSrc = decodeHref(src);
const resolvedSrc = resolvePath(xhtmlPath, decodedSrc);

const match = Object.entries(renamed).find(([oldPath]) => resolvedSrc === oldPath);

if (match) {
const [oldPath, newPath] = match;
img.setAttribute('src', decodedSrc.replace(oldPath.split('/').pop(), newPath.split('/').pop()));
modified = true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

When rewriting XHTML attributes, does decoding%20withdecodeURIComponentand then writing the decoded string back viasetAttribute('src', ...) risk breaking percent-encoded relative URLs in EPUB/XHTML?

💡 Result:

Yes—decoding a percent-encoded value like %20 to a literal space and then writing it back via setAttribute('src', ...) can risk breaking relative URLs inside EPUB/XHTML, because it changes the URL syntax that the reading system expects to match against the actual resource paths stored in the package. EPUB 3 specifies that “Parsing may replace some characters in the file path by their percent encoded alternative. For example, A/B/C/file name.xhtml becomes A/B/C/file%20name.xhtml” [1][2]. This indicates percent-encoded forms in paths are significant and are the normalized/expected representation. Additionally, real-world tooling bugs show failures when percent-escaped characters are not handled consistently. For example, epubcheck issue reports that resource lookup can fail when URL-escaped file paths are not correctly identified, and that URI-escaped filenames (including %20-style escapes) can pass where other handling fails [3]. Similarly, epub.js has reported image 404s related to encoded URLs, with debugging involving decoding URL strings and replacing encoded forms to correct requests [4]. On the JavaScript side, decodeURIComponent() decodes URI components and converts %20 into a space (and also throws if percent escapes are invalid) [5]. If you apply that directly to an authored src value and then set the DOM attribute with the decoded result, you have changed the URL string that will be serialized/used for fetching. Practical implication: if your goal is to preserve the original resource targeting in EPUB, don’t round-trip through decodeURIComponent() and then re-set the decoded string. Instead, either: - preserve percent-encoding as-is (recommended), or - if you must normalize, do it by percent-encoding correctly after any transformation (i.e., produce a percent-encoded path form rather than a literal space) [6][1]. In short: decoding %20 to spaces is exactly the change EPUB parsing/fetching behavior is designed to avoid; it can lead to mismatches and missing resources in EPUB readers.

Citations:


Preserve URI encoding when rewriting img src attributes.

decodeHref(src) for matching is correct, but writing decodedSrc.replace(...) back to the DOM converts percent-encoded characters (e.g., %20 → space) and breaks the XHTML reference. EPUB 3 treats percent-encoded filenames as the canonical form; changing My%20Cover.png to My Cover.png will fail to locate the resource. Decode only for comparison; when rewriting, either preserve the original src encoding during the replacement or re-encode the result before setting it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/network/html/FilesPage.html` around lines 4430 - 4440, The code decodes
src with decodeHref for matching but then writes back decodedSrc which strips
percent-encodings; instead, keep the original encoded src for the DOM update:
use decodeHref(src) and resolvePath(xhtmlPath, decodedSrc) to find a matching
entry in renamed, but when updating img.setAttribute('src') replace only the
last path segment of the original src (the one after the final '/') with the new
basename (newPath.split('/').pop()) and URL-encode that basename (e.g. via
encodeURIComponent) before inserting so percent-encoding is preserved; update
modified = true as before.

Comment on lines +4906 to 4908
const ok = !conversionFailed && conversionSucceeded;
saveToFileBatchLog(file.name, ok, ok ? convOriginalSize : 0, ok ? convNewSize : 0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't count fallback uploads as failed files.

This path only runs after the file was uploaded successfully. If optimization falls back to the original EPUB, the batch summary currently increments filesFailed, so the exported log says the file failed even though delivery succeeded.

💡 Suggested fix
-        const ok = !conversionFailed && conversionSucceeded;
-        saveToFileBatchLog(file.name, ok, ok ? convOriginalSize : 0, ok ? convNewSize : 0);
+        const optimized = conversionSucceeded && !conversionFailed;
+        saveToFileBatchLog(file.name, true, optimized ? convOriginalSize : 0, optimized ? convNewSize : 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ok = !conversionFailed && conversionSucceeded;
saveToFileBatchLog(file.name, ok, ok ? convOriginalSize : 0, ok ? convNewSize : 0);
}
const optimized = conversionSucceeded && !conversionFailed;
saveToFileBatchLog(file.name, true, optimized ? convOriginalSize : 0, optimized ? convNewSize : 0);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/network/html/FilesPage.html` around lines 4906 - 4908, The code currently
sets ok = !conversionFailed && conversionSucceeded which treats
fallback-to-original as a failure; instead, mark the file as successful because
this block runs after a successful upload. Replace the ok calculation (in the
save-to-log path using saveToFileBatchLog and the variables conversionFailed,
conversionSucceeded, convOriginalSize, convNewSize) so the file is considered
successful (ok = true) and pass sizes accordingly: use convOriginalSize as the
original size and use convNewSize only when conversionSucceeded, otherwise use
convOriginalSize for the delivered size so fallback uploads are not counted as
failed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.