Compare commits

...

10 Commits

Author SHA1 Message Date
Justin Mitchell
5e546a27bb
Merge 6316c4346a into 7185e5d287 2026-01-19 21:50:44 +10:00
Nathan James
7185e5d287
feat: Change keyboard "caps" to "shift" & Wrap Keyboard (#377)
## Summary

* This PR solves issue
https://github.com/crosspoint-reader/crosspoint-reader/issues/357 in the
first commit
* I then added an additional commit which means when you reach the end
of the keyboard, if you go 'beyond', you wrap back to the other side.
* This replaces existing behaviour, so if you would rather this be
removed, let me know and I'll just do the `caps` -> `shift` change

## Additional Context

### Screenshots for the new shift display

I thought it might not fit and need column size changes, but ended up
fitting fine, see screenshots showing this below:

<img width="573" height="366" alt="image"
src="https://github.com/user-attachments/assets/b8f6a4ec-94f5-4f5e-b9a6-06cc5f250ddb"
/>

<img width="570" height="308" alt="image"
src="https://github.com/user-attachments/assets/7d775518-4784-4120-a20a-a9dc67af8565"
/>


### Gif showing the wrap-around of the text



![IMG_7648](https://github.com/user-attachments/assets/7eec9066-e1cc-49a1-8b6b-a61556038d31)

---

### AI Usage

Did you use AI tools to help write this code? **PARTIALLY** - used to
double check the text wrapping had no edge-cases. (It did also suggest
rewriting the function, but I decided that was too big of a change for a
working part of the codebase, for now!)
2026-01-19 22:50:34 +11:00
Eunchurn Park
12940cc546
fix: XTC 1-bit thumb BMP polarity inversion (#373)
## Summary

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

- Fix inverted colors in Continue Reading cover image for 1-bit XTC
files

## Additional Context

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

- Fix `grayValue = pixelBit ? 0 : 255` → `grayValue = pixelBit ? 255 :
0` in `lib/Xtc/Xtc.cpp`
- The thumb BMP generation had inverted polarity compared to cover BMP
generation
- bit=0 should be black, bit=1 should be white (matching the BMP palette
order)
- Update misleading comment about XTC polarity

---

### AI Usage

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

Did you use AI tools to help write this code? _**PARTIALLY**_
2026-01-19 22:41:48 +11: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
10 changed files with 96 additions and 26 deletions

View File

@ -107,6 +107,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
@ -155,6 +159,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

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

@ -203,7 +203,7 @@ bool Xtc::generateCoverBmp() const {
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
// Color palette (2 colors for 1-bit)
// XTC uses inverted polarity: 0 = black, 1 = white
// XTC 1-bit polarity: 0 = black, 1 = white (standard BMP palette order)
// Color 0: Black (text/foreground in XTC)
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
coverBmp.write(black, 4);
@ -506,8 +506,8 @@ bool Xtc::generateThumbBmp() const {
// Bounds check for buffer access
if (byteIdx < bitmapSize) {
const uint8_t pixelBit = (pageBuffer[byteIdx] >> bitIdx) & 1;
// XTC polarity: 1=black, 0=white
grayValue = pixelBit ? 0 : 255;
// XTC 1-bit polarity: 0=black, 1=white (same as BMP palette)
grayValue = pixelBit ? 255 : 0;
}
}

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

@ -73,7 +73,7 @@ int KeyboardEntryActivity::getRowLength(const int row) const {
case 3:
return 10; // zxcvbnm,./
case 4:
return 10; // caps (2 wide), space (5 wide), backspace (2 wide), OK
return 10; // shift (2 wide), space (5 wide), backspace (2 wide), OK
default:
return 0;
}
@ -145,6 +145,11 @@ void KeyboardEntryActivity::loop() {
// Clamp column to valid range for new row
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
} else {
// Wrap to bottom row
selectedRow = NUM_ROWS - 1;
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
}
updateRequired = true;
}
@ -154,16 +159,24 @@ void KeyboardEntryActivity::loop() {
selectedRow++;
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
} else {
// Wrap to top row
selectedRow = 0;
const int maxCol = getRowLength(selectedRow) - 1;
if (selectedCol > maxCol) selectedCol = maxCol;
}
updateRequired = true;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
const int maxCol = getRowLength(selectedRow) - 1;
// Special bottom row case
if (selectedRow == SPECIAL_ROW) {
// Bottom row has special key widths
if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) {
// In shift key, do nothing
// In shift key, wrap to end of row
selectedCol = maxCol;
} else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) {
// In space bar, move to shift
selectedCol = SHIFT_COL;
@ -180,10 +193,9 @@ void KeyboardEntryActivity::loop() {
if (selectedCol > 0) {
selectedCol--;
} else if (selectedRow > 0) {
// Wrap to previous row
selectedRow--;
selectedCol = getRowLength(selectedRow) - 1;
} else {
// Wrap to end of current row
selectedCol = maxCol;
}
updateRequired = true;
}
@ -204,7 +216,8 @@ void KeyboardEntryActivity::loop() {
// In backspace, move to done
selectedCol = DONE_COL;
} else if (selectedCol >= DONE_COL) {
// At done button, do nothing
// At done button, wrap to beginning of row
selectedCol = SHIFT_COL;
}
updateRequired = true;
return;
@ -212,9 +225,8 @@ void KeyboardEntryActivity::loop() {
if (selectedCol < maxCol) {
selectedCol++;
} else if (selectedRow < NUM_ROWS - 1) {
// Wrap to next row
selectedRow++;
} else {
// Wrap to beginning of current row
selectedCol = 0;
}
updateRequired = true;
@ -288,14 +300,14 @@ void KeyboardEntryActivity::render() const {
// Handle bottom row (row 4) specially with proper multi-column keys
if (row == 4) {
// Bottom row layout: CAPS (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
// Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
// Total: 11 visual columns, but we use logical positions for selection
int currentX = startX;
// CAPS key (logical col 0, spans 2 key widths)
const bool capsSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
renderItemWithSelector(currentX + 2, rowY, shiftActive ? "CAPS" : "caps", capsSelected);
// SHIFT key (logical col 0, spans 2 key widths)
const bool shiftSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
renderItemWithSelector(currentX + 2, rowY, shiftActive ? "SHIFT" : "shift", shiftSelected);
currentX += 2 * (keyWidth + keySpacing);
// Space bar (logical cols 2-6, spans 5 key widths)