mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 15:47:39 +03:00
Compare commits
11 Commits
fe038cc1d4
...
dde14532ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde14532ad | ||
|
|
bb5fd0cee2 | ||
|
|
f02872542f | ||
|
|
32cffaf504 | ||
|
|
21277e03eb | ||
|
|
4eef2b5793 | ||
|
|
5a55fa1c6e | ||
|
|
c98ba142e8 | ||
|
|
c1c94c0112 | ||
|
|
eb84bcee7c | ||
|
|
d45f355e87 |
@ -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.
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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", "", "");
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user