Compare commits

...

11 Commits

Author SHA1 Message Date
Arthur Tazhitdinov
dde14532ad
Merge bb5fd0cee2 into 21277e03eb 2026-01-15 22:07:49 +05:00
Arthur Tazhitdinov
bb5fd0cee2 revert isAlphabetic change 2026-01-15 22:07:43 +05:00
Arthur Tazhitdinov
f02872542f refactor: unify punctuation trimming to handle footnotes in hyphenation logic 2026-01-15 21:48:32 +05:00
Arthur Tazhitdinov
32cffaf504 Merge remote-tracking branch 'origin/master' into hyphenation-v3 2026-01-15 20:20:25 +05:00
Luke Stein
21277e03eb
docs: Update User Guide to reflect release 0.14.0 (#376)
Some checks failed
CI / build (push) Has been cancelled
2026-01-15 23:27:17 +11:00
Luke Stein
4eef2b5793
feat: Add MAC address display to WiFi Networks screen (#381)
## Summary

* Implements #380, allowing the user to see the device's MAC address in
order to register on wifi networks

## Additional Context

* Although @markatlnk suggested showing on the settings screen, I
implemented display at the bottom of the WiFi Networks selection screen
(accessed via "File Transfer" > "Join a Network") since I think it makes
more sense there.
* Tested on my own device


![IMG_2873](https://github.com/user-attachments/assets/b82a20dc-41a0-4b21-81f1-20876aa2c6b0)


---

### AI Usage

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-15 23:26:39 +11:00
Nathan James
5a55fa1c6e
fix: also apply longPressChapterSkip setting to xtc reader (#378)
## Summary

* This builds upon the helpful PR
https://github.com/crosspoint-reader/crosspoint-reader/pull/341, and
adds support for the setting to also apply to the XTC reader, which I
believed has just been missed and was not intentionally left out.
* XTC does not have chapter support yet, but it does skip 10 pages when
long-pressed, and so I think this is useful.

---

### AI Usage

Did you use AI tools to help write this code? No
2026-01-15 23:25:18 +11:00
Maeve Andrews
c98ba142e8
fix: draw button hints correctly if orientation is not portrait (#363)
~~Quick~~ fix for
https://github.com/daveallie/crosspoint-reader/issues/362

(this just applies to the chapter selection menu:)

~~If the orientation is portrait, hints as we know them make sense to
draw. If the orientation is inverted, we'd have to change the order of
the labels (along with everything's position), and if it's one of the
landscape choices, we'd have to render the text and buttons vertically.
All those other cases will be more complicated.~~

~~Punt on this for now by only rendering if portrait.~~

Update: this now draws the hints at the physical button position no
matter what the orientation is, by temporarily changing orientation to
portrait.

---------

Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-15 23:23:36 +11:00
Jonas Diemer
c1c94c0112
Feature: Show img alt text (#168)
Let's start small by showing the ALT text of IMG. This is rudimentary,
but avoids those otherwise completely blank chapters.

I feel we will need this even when we can render images if that
rendering takes >1s - I would then prefer rendering optional and showing
the ALT text first.
2026-01-15 23:21:46 +11:00
Dave Allie
eb84bcee7c
chore: Pin links2004/WebSockets version 2026-01-15 23:15:30 +11:00
efenner
d45f355e87
feat: Add EPUB table omitted placeholder (#372)
## Summary

* **What is the goal of this PR?**: Fix the bug I reported in
https://github.com/daveallie/crosspoint-reader/issues/292
* **What changes are included?**: Instead of silently dropping table
content in EPUBs., replace with an italicized '[Table omitted]' message
where tables appear.

## Additional Context

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

---

### AI Usage

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

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

---------

Co-authored-by: Evan Fenner <evan@evanfenner.com>
Co-authored-by: Warp <agent@warp.dev>
2026-01-15 23:14:59 +11:00
12 changed files with 99 additions and 58 deletions

View File

@ -96,6 +96,10 @@ The Settings screen allows you to configure the device's behavior. There are a f
- Left, Right, Back, Confirm
- Left, Back, Confirm, Right
- **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter.
- "Chapter Skip" (default) - Long-pressing skips to next/previous chapter
- "Page Scroll" - Long-pressing scrolls a page up/down
- Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading.
- **Reader Font Family**: Choose the font used for reading:
- "Bookerly" (default) - Amazon's reading font
- "Noto Sans" - Google's sans-serif font
@ -144,6 +148,9 @@ If the **Short Power Button Click** setting is set to "Page Turn", you can also
* **Next Chapter:** Press and **hold** the **Right** (or **Volume Down**) button briefly, then release.
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
This feature can be disabled in **[Settings](#35-settings)** to help avoid changing chapters by mistake.
### System Navigation
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
* **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen.

View File

@ -71,6 +71,7 @@ bool isAlphabetic(const uint32_t cp) { return isLatinLetter(cp) || isCyrillicLet
bool isPunctuation(const uint32_t cp) {
switch (cp) {
case '-':
case '.':
case ',':
case '!':
@ -87,8 +88,11 @@ bool isPunctuation(const uint32_t cp) {
case 0x2019: //
case 0x201C: // “
case 0x201D: // ”
case 0x00A0: // no-break space
case '{':
case '}':
case '[':
case ']':
case '/':
case 0x203A: //
case 0x2026: // …
@ -107,18 +111,6 @@ bool isExplicitHyphen(const uint32_t cp) {
case 0x058A: // Armenian hyphen
case 0x2010: // hyphen
case 0x2011: // non-breaking hyphen
case 0x2012: // figure dash
case 0x2013: // en dash
case 0x2014: // em dash
case 0x2015: // horizontal bar
case 0x2043: // hyphen bullet
case 0x207B: // superscript minus
case 0x208B: // subscript minus
case 0x2212: // minus sign
case 0x2E17: // double oblique hyphen
case 0x2E3A: // two-em dash
case 0x2E3B: // three-em dash
case 0xFE58: // small em dash
case 0xFE63: // small hyphen-minus
case 0xFF0D: // fullwidth hyphen-minus
return true;
@ -129,7 +121,28 @@ bool isExplicitHyphen(const uint32_t cp) {
bool isSoftHyphen(const uint32_t cp) { return cp == 0x00AD; }
void trimSurroundingPunctuation(std::vector<CodepointInfo>& cps) {
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps) {
if (cps.empty()) {
return;
}
// Remove trailing footnote references like [12], even if punctuation trails after the closing bracket.
if (cps.size() >= 3) {
int end = static_cast<int>(cps.size()) - 1;
while (end >= 0 && isPunctuation(cps[end].value)) {
--end;
}
int pos = end;
if (pos >= 0 && isAsciiDigit(cps[pos].value)) {
while (pos >= 0 && isAsciiDigit(cps[pos].value)) {
--pos;
}
if (pos >= 0 && cps[pos].value == '[' && end - pos > 1) {
cps.erase(cps.begin() + pos, cps.end());
}
}
}
while (!cps.empty() && isPunctuation(cps.front().value)) {
cps.erase(cps.begin());
}
@ -152,27 +165,3 @@ std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
return cps;
}
void trimTrailingFootnoteReference(std::vector<CodepointInfo>& cps) {
if (cps.size() < 3) {
return;
}
int closing = static_cast<int>(cps.size()) - 1;
if (cps[closing].value != ']') {
return;
}
int pos = closing - 1;
if (pos < 0 || !isAsciiDigit(cps[pos].value)) {
return;
}
while (pos >= 0 && isAsciiDigit(cps[pos].value)) {
--pos;
}
if (pos < 0 || cps[pos].value != '[') {
return;
}
if (closing - pos <= 1) {
return;
}
cps.erase(cps.begin() + pos, cps.end());
}

View File

@ -21,6 +21,5 @@ bool isPunctuation(uint32_t cp);
bool isAsciiDigit(uint32_t cp);
bool isExplicitHyphen(uint32_t cp);
bool isSoftHyphen(uint32_t cp);
void trimSurroundingPunctuation(std::vector<CodepointInfo>& cps);
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
std::vector<CodepointInfo> collectCodepoints(const std::string& word);
void trimTrailingFootnoteReference(std::vector<CodepointInfo>& cps);

View File

@ -1,8 +1,5 @@
#include "Hyphenator.h"
#include <Utf8.h>
#include <algorithm>
#include <vector>
#include "HyphenationCommon.h"
@ -60,13 +57,10 @@ std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& w
// Convert to codepoints and normalize word boundaries.
auto cps = collectCodepoints(word);
trimSurroundingPunctuation(cps);
trimTrailingFootnoteReference(cps);
trimSurroundingPunctuationAndFootnote(cps);
const auto* hyphenator = cachedHyphenator_;
const size_t minPrefix = hyphenator ? hyphenator->minPrefix() : LiangWordConfig::kDefaultMinPrefix;
const size_t minSuffix = hyphenator ? hyphenator->minSuffix() : LiangWordConfig::kDefaultMinSuffix;
// Explicit hyphen markers (soft or hard) take precedence over heuristic breaks.
// Explicit hyphen markers (soft or hard) take precedence over language breaks.
auto explicitBreakInfos = buildExplicitBreakInfos(cps);
if (!explicitBreakInfos.empty()) {
return explicitBreakInfos;
@ -80,6 +74,8 @@ std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& w
// Only add fallback breaks if needed
if (includeFallback && indexes.empty()) {
const size_t minPrefix = hyphenator ? hyphenator->minPrefix() : LiangWordConfig::kDefaultMinPrefix;
const size_t minSuffix = hyphenator ? hyphenator->minSuffix() : LiangWordConfig::kDefaultMinSuffix;
for (size_t idx = minPrefix; idx + minSuffix <= cps.size(); ++idx) {
indexes.push_back(idx);
}

View File

@ -25,7 +25,7 @@ constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
const char* IMAGE_TAGS[] = {"img"};
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
const char* SKIP_TAGS[] = {"head", "table"};
const char* SKIP_TAGS[] = {"head"};
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
@ -63,13 +63,44 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// TODO: Start processing image tags
// Special handling for tables - show placeholder text instead of dropping silently
if (strcmp(name, "table") == 0) {
// Add placeholder text
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
if (self->currentTextBlock) {
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
}
// Skip table contents
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
// TODO: Start processing image tags
std::string alt;
if (atts != nullptr) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "alt") == 0) {
alt = "[Image: " + std::string(atts[i + 1]) + "]";
}
}
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
self->depth += 1;
self->characterData(userData, alt.c_str(), alt.length());
} else {
// Skip for now
self->skipUntilDepth = self->depth;
self->depth += 1;
return;
}
}
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
// start skip
self->skipUntilDepth = self->depth;

View File

@ -468,7 +468,10 @@ int GfxRenderer::getLineHeight(const int fontId) const {
}
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
const char* btn4) const {
const char* btn4) {
const Orientation orig_orientation = getOrientation();
setOrientation(Orientation::Portrait);
const int pageHeight = getScreenHeight();
constexpr int buttonWidth = 106;
constexpr int buttonHeight = 40;
@ -481,12 +484,15 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
// Only draw if the label is non-empty
if (labels[i] != nullptr && labels[i][0] != '\0') {
const int x = buttonPositions[i];
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
const int textWidth = getTextWidth(fontId, labels[i]);
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
}
}
setOrientation(orig_orientation);
}
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {

View File

@ -84,7 +84,7 @@ class GfxRenderer {
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
// UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
private:

View File

@ -45,9 +45,9 @@ lib_deps =
InputManager=symlink://open-x4-sdk/libs/hardware/InputManager
EInkDisplay=symlink://open-x4-sdk/libs/display/EInkDisplay
SDCardManager=symlink://open-x4-sdk/libs/hardware/SDCardManager
ArduinoJson @ 7.4.2
QRCode @ 0.0.1
links2004/WebSockets @ ^2.4.1
bblanchon/ArduinoJson @ 7.4.2
ricmoo/QRCode @ 0.0.1
links2004/WebSockets @ 2.7.3
[env:default]
extends = base

View File

@ -37,6 +37,14 @@ void WifiSelectionActivity::onEnter() {
savePromptSelection = 0;
forgetPromptSelection = 0;
// Cache MAC address for display
uint8_t mac[6];
WiFi.macAddress(mac);
char macStr[32];
snprintf(macStr, sizeof(macStr), "MAC address: %02x-%02x-%02x-%02x-%02x-%02x", mac[0], mac[1], mac[2], mac[3], mac[4],
mac[5]);
cachedMacAddress = std::string(macStr);
// Trigger first update to show scanning message
updateRequired = true;
@ -572,6 +580,9 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
}
// Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");

View File

@ -62,6 +62,9 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
// Password to potentially save (from keyboard or saved credentials)
std::string enteredPassword;
// Cached MAC address string for display
std::string cachedMacAddress;
// Whether network was connected using a saved password (skip save prompt)
bool usedSavedPassword = false;

View File

@ -127,7 +127,7 @@ void XtcReaderActivity::loop() {
return;
}
const bool skipPages = mappedInput.getHeldTime() > skipPageMs;
const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs;
const int skipAmount = skipPages ? 10 : 1;
if (prevReleased) {

View File

@ -128,8 +128,7 @@ std::string positionsToHyphenated(const std::string& word, const std::vector<siz
std::vector<size_t> hyphenateWordWithHyphenator(const std::string& word, const LanguageHyphenator& hyphenator) {
auto cps = collectCodepoints(word);
trimSurroundingPunctuation(cps);
trimTrailingFootnoteReference(cps);
trimSurroundingPunctuationAndFootnote(cps);
return hyphenator.breakIndexes(cps);
}