feat: focus reading (#1670)
## Summary This PR introduces **Focus Reading**, a generic implementation of artificial fixation points (similar to Bionic Reading) designed to improve reading speed and focus by bolding the initial characters of words. This is achieved by dynamically bolding characters during indexing. <img width="500" alt="Focus Reading on X3" src="https://github.com/user-attachments/assets/94a632a5-82da-47be-957c-538b35bf84d9" /> ### Implementation Details #### Core Text Engine (`ParsedText`) - Modified `ParsedText::addWord` to implement a custom bolding algorithm. It uses a 45% ratio for bolding, with a minimum of 1 character and a maximum of 9. - UTF-8 Safety: Integrated `utf8NextCodepoint` to ensure character counting and string slicing occur at safe byte boundaries, preventing corruption of multi-byte characters (e.g., accented letters or smart quotes). - Intelligent Tokenization: This correctly identifies and separates "word" characters (letters, apostrophes, hyphens) from "non-word" characters (numbers, brackets, smart quotes). - Formatting Preservation: The logic ensures that punctuation is not "stolen" for the bolding count and that existing styles (like italics or underlines) are preserved across the bold/regular split. - Processing at indexing stage reduces CPU load at render-time and ensures layout/fit is unaffected. - Split details are tracked with `wordIsFocusSuffix`. After splitting and layout, suffixes are merged back into their preceding word entries to prevent a doubling of RAM usage. #### Settings and UI - Version Management: Bumped `SECTION_FILE_VERSION` to `21` - Global Settings: Added `focusReadingEnabled` to `CrossPointSettings`. - User Interface: Added a new toggle in the "Reader" section of the settings menu, positioned after the "Embedded Style" option. - Localization: Added the `STR_FOCUS_READING` string #### Plumbing - Plumbed the `focusReadingEnabled` boolean through `EpubReaderActivity`, `Section`, and `ChapterHtmlSlimParser` to ensure the user's setting reaches the `ParsedText` constructor during chapter indexing. ## Additional Context ### Files Changed - `lib/Epub/Epub/ParsedText.h / .cpp`: Core fixation logic and UTF-8 tokenization. - `lib/Epub/Epub/Section.h / .cpp`: Cache header updates and invalidation logic. - `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h / .cpp`: Plumbing the setting to text blocks. - `src/CrossPointSettings.h`: Data persistence for the new setting. - `src/SettingsList.h`: UI toggle implementation. - `src/activities/reader/EpubReaderActivity.cpp`: Handling settings changes during reading sessions. - `lib/I18n/translations/*.yaml`: UI strings. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# Focus Reading
|
||||
|
||||
Focus Reading is a reading aid that bolds the first portion of each word, guiding your eyes to natural fixation points and helping you read faster with less effort. Some readers — particularly those with ADHD — find it helps them stay engaged with the text and reduces mind-wandering. It is inspired by the Bionic Reading technique.
|
||||
|
||||
<img src="./images/focus-reading/focus-reading.jpg" height="500" alt="Comparison of the same page with and without Focus Reading enabled" />
|
||||
|
||||
*Left: Focus Reading off. Right: Focus Reading on. Both using Literata.*
|
||||
|
||||
## Enabling Focus Reading
|
||||
|
||||
1. Open **Settings > Reader**
|
||||
2. Toggle **Focus Reading** on
|
||||
|
||||
Toggling the setting will trigger a re-index of your current book, the same as when changing font settings. Once indexing is complete, page turns proceed as normal. No changes are made to your EPUB files.
|
||||
|
||||
## Examples
|
||||
|
||||
<img src="./images/focus-reading/focus-reading-notoserif.jpg" height="500" alt="Focus Reading with Noto Serif font" />
|
||||
|
||||
*Focus Reading with Noto Serif font*
|
||||
|
||||
<img src="./images/focus-reading/focus-reading-merriweather.jpg" height="500" alt="Focus Reading with Merriweather font" />
|
||||
|
||||
*Focus Reading with Merriweather font*
|
||||
|
||||
<img src="./images/focus-reading/focus-reading-atkinson.jpg" height="500" alt="Focus Reading with Atkinson Hyperlegible Next font" />
|
||||
|
||||
*Focus Reading with Atkinson Hyperlegible Next font*
|
||||
|
||||
## Notes
|
||||
|
||||
- Focus Reading only applies to regular body text. Already-bold text (headings, emphasis) is left unchanged.
|
||||
- The setting is per-device, not per-book — it applies to all books while enabled.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
@@ -74,21 +74,170 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
||||
return renderer.getTextAdvanceX(fontId, sanitized.c_str(), style);
|
||||
}
|
||||
|
||||
// Checks if a UTF-8 codepoint should be counted as part of a word for Focus Reading
|
||||
bool isWordCharacter(uint32_t cp) {
|
||||
// ASCII range (Catches 95%+ of characters immediately)
|
||||
if (cp < 128) {
|
||||
// Bitwise trick: (cp | 0x20) converts uppercase ASCII to lowercase.
|
||||
// This checks for A-Z and a-z mathematically, avoiding memory lookups and <cctype>
|
||||
return ((cp | 0x20) >= 'a' && (cp | 0x20) <= 'z') || cp == '\'';
|
||||
}
|
||||
|
||||
// General Punctuation Block, Currency, Math, Arrows, & Symbols (0x2000 - 0x2BFF)
|
||||
if (cp >= 0x2000 && cp <= 0x2BFF) {
|
||||
// Explicitly allow smart quotes, reject all other general punctuation (em-dashes, etc.)
|
||||
return cp == 0x2018 || cp == 0x2019;
|
||||
}
|
||||
|
||||
// Latin-1 Punctuation Block (0x00A1 - 0x00BF)
|
||||
if (cp >= 0x00A1 && cp <= 0x00BF) {
|
||||
// Allow ordinal indicators and micro sign, reject the rest (¡, ¿, «, », etc.)
|
||||
return cp == 0x00AA || cp == 0x00B5 || cp == 0x00BA;
|
||||
}
|
||||
|
||||
// Rejects Two-em dash, Three-em dash, Double oblique hyphen, etc.
|
||||
if (cp >= 0x2E00 && cp <= 0x2E7F) return false;
|
||||
|
||||
// Rejects Modifier Minus (0x02D7), Small Hyphen (0xFE63), and Fullwidth Hyphen (0xFF0D)
|
||||
if (cp == 0x02D7 || cp == 0xFE63 || cp == 0xFF0D) return false;
|
||||
// Assume all other Unicode ranges (accented letters, Cyrillic, Greek, etc.) are valid
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline,
|
||||
const bool attachToPrevious) {
|
||||
if (word.empty()) return;
|
||||
|
||||
words.push_back(std::move(word));
|
||||
EpdFontFamily::Style combinedStyle = fontStyle;
|
||||
EpdFontFamily::Style baseStyle = fontStyle;
|
||||
if (underline) {
|
||||
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
|
||||
baseStyle = static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::UNDERLINE);
|
||||
}
|
||||
wordStyles.push_back(combinedStyle);
|
||||
wordContinues.push_back(attachToPrevious);
|
||||
}
|
||||
|
||||
// Already-bold text should stay fully bold; focus splitting would make its suffix regular later.
|
||||
if (!this->focusReadingEnabled || (baseStyle & EpdFontFamily::BOLD) != 0) {
|
||||
words.push_back(std::move(word));
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(attachToPrevious);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- FOCUS READING LOGIC BELOW ---
|
||||
|
||||
// Pre-reserve capacity to prevent mid-word heap reallocations.
|
||||
size_t maxPossibleNewTokens = word.length();
|
||||
size_t requiredSize = words.size() + maxPossibleNewTokens;
|
||||
|
||||
if (words.capacity() < requiredSize) {
|
||||
// Emulate standard geometric growth (doubling) to ensure we don't reallocate on every word.
|
||||
size_t newCapacity = words.capacity() * 2;
|
||||
|
||||
// Ensure the doubled capacity is actually enough for this specific word
|
||||
if (newCapacity < requiredSize) {
|
||||
newCapacity = requiredSize;
|
||||
}
|
||||
// Set a sensible minimum starting size so the first few words don't trigger tiny reallocations
|
||||
if (newCapacity < 16) {
|
||||
newCapacity = 16;
|
||||
}
|
||||
|
||||
words.reserve(newCapacity);
|
||||
wordStyles.reserve(newCapacity);
|
||||
wordContinues.reserve(newCapacity);
|
||||
wordIsFocusSuffix.reserve(newCapacity);
|
||||
}
|
||||
|
||||
// Lambda helper to process and push individual sub-segments of the string
|
||||
// Use std::string_view to avoid heap allocations when slicing
|
||||
auto processSegment = [&](std::string_view segment, bool isWord, bool attach) {
|
||||
if (!isWord) {
|
||||
// Punctuation and Numbers stay regular
|
||||
words.emplace_back(segment);
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(attach);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
} else {
|
||||
size_t charCount = 0;
|
||||
const unsigned char* countPtr = reinterpret_cast<const unsigned char*>(segment.data());
|
||||
const unsigned char* countEnd = countPtr + segment.length();
|
||||
|
||||
while (countPtr < countEnd) {
|
||||
utf8NextCodepoint(&countPtr);
|
||||
charCount++;
|
||||
}
|
||||
|
||||
// Target 45% for 1-bold at 4 chars and 3-bold at 7 chars with floor truncation
|
||||
constexpr size_t FOCUS_READING_PERCENT = 45;
|
||||
size_t targetBoldChars = (charCount * FOCUS_READING_PERCENT) / 100;
|
||||
targetBoldChars = std::clamp<size_t>(targetBoldChars, 1, 9);
|
||||
|
||||
if (targetBoldChars >= charCount) {
|
||||
// Whole segment is bold - no suffix split needed
|
||||
words.emplace_back(segment);
|
||||
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
||||
wordContinues.push_back(attach);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
} else {
|
||||
countPtr = reinterpret_cast<const unsigned char*>(segment.data());
|
||||
for (size_t i = 0; i < targetBoldChars; ++i) {
|
||||
utf8NextCodepoint(&countPtr);
|
||||
}
|
||||
size_t splitByteOffset = countPtr - reinterpret_cast<const unsigned char*>(segment.data());
|
||||
|
||||
// Bold prefix
|
||||
words.emplace_back(segment.substr(0, splitByteOffset));
|
||||
wordStyles.push_back(static_cast<EpdFontFamily::Style>(baseStyle | EpdFontFamily::BOLD));
|
||||
wordContinues.push_back(attach);
|
||||
wordIsFocusSuffix.push_back(false);
|
||||
|
||||
// Regular suffix - marked so extractLine can merge it back into single TextBlock entry
|
||||
words.emplace_back(segment.substr(splitByteOffset));
|
||||
wordStyles.push_back(baseStyle);
|
||||
wordContinues.push_back(true);
|
||||
wordIsFocusSuffix.push_back(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Tokenize the string by alternating states (Word vs. Non-Word)
|
||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(word.c_str());
|
||||
const unsigned char* end = ptr + word.length();
|
||||
|
||||
const unsigned char* segmentStart = ptr;
|
||||
uint32_t firstCp = utf8NextCodepoint(&ptr); // Consume the first char to determine initial state
|
||||
bool inWordSegment = isWordCharacter(firstCp);
|
||||
|
||||
bool isFirstSegment = true;
|
||||
|
||||
while (ptr < end) {
|
||||
const unsigned char* currentCpStart = ptr;
|
||||
uint32_t cp = utf8NextCodepoint(&ptr);
|
||||
bool isWordChar = isWordCharacter(cp);
|
||||
|
||||
// Whenever the character type flips, slice off the segment we just completed and process it
|
||||
if (isWordChar != inWordSegment) {
|
||||
size_t segmentLen = currentCpStart - segmentStart;
|
||||
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
|
||||
|
||||
// Only the very first segment inherits the original attachToPrevious flag.
|
||||
// Every subsequent segment MUST attach=true so it glues seamlessly to the prefix.
|
||||
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
|
||||
|
||||
// Setup for the next segment
|
||||
segmentStart = currentCpStart;
|
||||
inWordSegment = isWordChar;
|
||||
isFirstSegment = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the final remaining segment
|
||||
size_t segmentLen = end - segmentStart;
|
||||
std::string_view segment(reinterpret_cast<const char*>(segmentStart), segmentLen);
|
||||
processSegment(segment, inWordSegment, isFirstSegment ? attachToPrevious : true);
|
||||
}
|
||||
// Consumes data to minimize memory usage
|
||||
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
@@ -153,6 +302,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
words.erase(words.begin(), words.begin() + consumed);
|
||||
wordStyles.erase(wordStyles.begin(), wordStyles.begin() + consumed);
|
||||
wordContinues.erase(wordContinues.begin(), wordContinues.begin() + consumed);
|
||||
wordIsFocusSuffix.erase(wordIsFocusSuffix.begin(), wordIsFocusSuffix.begin() + consumed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +586,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
// Insert the remainder word (with matching style and continuation flag) directly after the prefix.
|
||||
words.insert(words.begin() + wordIndex + 1, remainder);
|
||||
wordStyles.insert(wordStyles.begin() + wordIndex + 1, style);
|
||||
// The hyphen remainder is not a focus suffix - it starts fresh on the next line.
|
||||
wordIsFocusSuffix.insert(wordIsFocusSuffix.begin() + wordIndex + 1, false);
|
||||
|
||||
// Continuation flag handling after splitting a word into prefix + remainder.
|
||||
//
|
||||
@@ -567,6 +719,63 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
}
|
||||
|
||||
processLine(
|
||||
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||
// Fast path: when no word on this line was split for focus reading, skip the merge work
|
||||
// entirely and pass empty boundary/suffixX vectors. TextBlock pays zero per-word RAM cost
|
||||
// for these annotations when the vectors are empty.
|
||||
bool lineHasFocusSplit = false;
|
||||
for (size_t i = 0; i < lineWordCount; i++) {
|
||||
if (wordIsFocusSuffix[lastBreakAt + i]) {
|
||||
lineHasFocusSplit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lineHasFocusSplit) {
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles),
|
||||
std::vector<uint8_t>{}, std::vector<uint16_t>{}, blockStyle));
|
||||
return;
|
||||
}
|
||||
|
||||
// Slow path: merge focus suffix tokens back into their preceding word entry so each
|
||||
// original word occupies one TextBlock slot. Splits are recorded as per-word annotations
|
||||
// applied at render time, cutting the token count significantly when the feature is active.
|
||||
std::vector<std::string> outWords;
|
||||
std::vector<int16_t> outXPos;
|
||||
std::vector<EpdFontFamily::Style> outStyles;
|
||||
std::vector<uint8_t> outBoundaries;
|
||||
std::vector<uint16_t> outSuffixX;
|
||||
outWords.reserve(lineWordCount);
|
||||
outXPos.reserve(lineWordCount);
|
||||
outStyles.reserve(lineWordCount);
|
||||
outBoundaries.reserve(lineWordCount);
|
||||
outSuffixX.reserve(lineWordCount);
|
||||
|
||||
for (size_t i = 0; i < lineWordCount; i++) {
|
||||
if (wordIsFocusSuffix[lastBreakAt + i] && !outWords.empty()) {
|
||||
// Focus suffix: merge string into the preceding bold-prefix entry.
|
||||
outWords.back() += lineWords[i];
|
||||
} else {
|
||||
// Normal word: check for a following focus suffix to record the byte boundary.
|
||||
uint8_t boundary = 0;
|
||||
uint16_t suffixX = 0;
|
||||
if (i + 1 < lineWordCount && wordIsFocusSuffix[lastBreakAt + i + 1]) {
|
||||
boundary = static_cast<uint8_t>(std::min(lineWords[i].size(), size_t{255}));
|
||||
// Suffix x offset = layout-time advance of the bold prefix, already known from xpos table.
|
||||
suffixX = static_cast<uint16_t>(lineXPos[i + 1] - lineXPos[i]);
|
||||
}
|
||||
outWords.push_back(std::move(lineWords[i]));
|
||||
outXPos.push_back(lineXPos[i]);
|
||||
// For focus entries with a suffix, strip BOLD from the stored style.
|
||||
// Render re-applies it to the prefix portion only, via the boundary field.
|
||||
const EpdFontFamily::Style storedStyle =
|
||||
boundary > 0 ? static_cast<EpdFontFamily::Style>(lineWordStyles[i] & ~EpdFontFamily::BOLD)
|
||||
: lineWordStyles[i];
|
||||
outStyles.push_back(storedStyle);
|
||||
outBoundaries.push_back(boundary);
|
||||
outSuffixX.push_back(suffixX);
|
||||
}
|
||||
}
|
||||
|
||||
processLine(std::make_shared<TextBlock>(std::move(outWords), std::move(outXPos), std::move(outStyles),
|
||||
std::move(outBoundaries), std::move(outSuffixX), blockStyle));
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ class GfxRenderer;
|
||||
class ParsedText {
|
||||
std::vector<std::string> words;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous (no space before it)
|
||||
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
bool focusReadingEnabled;
|
||||
|
||||
void applyParagraphIndent();
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
@@ -35,8 +37,11 @@ class ParsedText {
|
||||
|
||||
public:
|
||||
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||
const BlockStyle& blockStyle = BlockStyle())
|
||||
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||
const bool focusReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle())
|
||||
: blockStyle(blockStyle),
|
||||
extraParagraphSpacing(extraParagraphSpacing),
|
||||
hyphenationEnabled(hyphenationEnabled),
|
||||
focusReadingEnabled(focusReadingEnabled) {}
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||
|
||||
+15
-11
@@ -13,8 +13,8 @@ namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 23;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||
sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||
sizeof(uint32_t);
|
||||
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t);
|
||||
|
||||
struct PageLutEntry {
|
||||
uint32_t fileOffset;
|
||||
@@ -43,7 +43,8 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool embeddedStyle, const uint8_t imageRendering) {
|
||||
const bool embeddedStyle, const uint8_t imageRendering,
|
||||
const bool focusReadingEnabled) {
|
||||
if (!file) {
|
||||
LOG_DBG("SCT", "File not open for writing header");
|
||||
return;
|
||||
@@ -51,8 +52,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(uint32_t) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
serialization::writePod(file, SECTION_FILE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
@@ -64,6 +65,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
serialization::writePod(file, hyphenationEnabled);
|
||||
serialization::writePod(file, embeddedStyle);
|
||||
serialization::writePod(file, imageRendering);
|
||||
serialization::writePod(file, focusReadingEnabled);
|
||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
|
||||
@@ -74,7 +76,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
|
||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const uint8_t imageRendering) {
|
||||
const uint8_t imageRendering, const bool focusReadingEnabled) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
@@ -99,6 +101,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
bool fileHyphenationEnabled;
|
||||
bool fileEmbeddedStyle;
|
||||
uint8_t fileImageRendering;
|
||||
bool fileFocusReadingEnabled;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
@@ -108,13 +111,13 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
||||
serialization::readPod(file, fileHyphenationEnabled);
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
serialization::readPod(file, fileImageRendering);
|
||||
serialization::readPod(file, fileFocusReadingEnabled);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||
imageRendering != fileImageRendering) {
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
|
||||
file.close();
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache();
|
||||
@@ -148,7 +151,8 @@ bool Section::clearCache() const {
|
||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const uint8_t imageRendering, const std::function<void()>& popupFn) {
|
||||
const uint8_t imageRendering, const bool focusReadingEnabled,
|
||||
const std::function<void()>& popupFn) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||
|
||||
@@ -199,7 +203,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
return false;
|
||||
}
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering);
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
||||
std::vector<PageLutEntry> lut = {};
|
||||
|
||||
// Derive the content base directory and image cache path prefix for the parser
|
||||
@@ -219,7 +223,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
|
||||
|
||||
ChapterHtmlSlimParser visitor(
|
||||
epub, tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled,
|
||||
viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
||||
[this, &lut](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
||||
lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ class Section {
|
||||
|
||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
bool embeddedStyle, uint8_t imageRendering);
|
||||
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||
|
||||
public:
|
||||
@@ -33,11 +33,12 @@ class Section {
|
||||
~Section() = default;
|
||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
uint8_t imageRendering);
|
||||
uint8_t imageRendering, bool focusReadingEnabled);
|
||||
bool clearCache() const;
|
||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
uint8_t imageRendering, const std::function<void()>& popupFn = nullptr);
|
||||
uint8_t imageRendering, bool focusReadingEnabled,
|
||||
const std::function<void()>& popupFn = nullptr);
|
||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||
|
||||
// Look up the page number for an anchor id from the section cache file.
|
||||
|
||||
@@ -4,18 +4,44 @@
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||
// Validate iterator bounds before rendering
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", (uint32_t)words.size(),
|
||||
(uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
|
||||
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
||||
// When present, they must be sized in lockstep with words[].
|
||||
const bool hasFocus = !wordFocusBoundary.empty();
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
||||
LOG_ERR("TXB", "Render skipped: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
||||
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size(),
|
||||
(uint32_t)wordFocusBoundary.size(), (uint32_t)wordFocusSuffixX.size());
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
const int wordX = wordXpos[i] + x;
|
||||
const EpdFontFamily::Style currentStyle = wordStyles[i];
|
||||
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
||||
const uint8_t boundary = hasFocus ? wordFocusBoundary[i] : 0;
|
||||
|
||||
if (boundary > 0) {
|
||||
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
|
||||
// The bold prefix is bounded to 9 codepoints by the clamp on targetBoldChars in
|
||||
// ParsedText::addWord; 9 UTF-8 codepoints occupy at most 9 * 4 = 36 bytes, +1 for null = 37.
|
||||
// suffixX is computed at cache-creation time to avoid font metric lookups at render time.
|
||||
static constexpr size_t MAX_FOCUS_PREFIX_BYTES = 9 * 4 + 1;
|
||||
char boldBuf[40];
|
||||
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
||||
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
||||
const size_t boldLen = std::min<size_t>({static_cast<size_t>(boundary), words[i].size(), sizeof(boldBuf) - 1});
|
||||
memcpy(boldBuf, words[i].c_str(), boldLen);
|
||||
boldBuf[boldLen] = '\0';
|
||||
renderer.drawText(fontId, wordX, y, boldBuf, true, boldStyle);
|
||||
const int suffixX = wordX + wordFocusSuffixX[i];
|
||||
renderer.drawText(fontId, suffixX, y, words[i].c_str() + boldLen, true, currentStyle);
|
||||
} else {
|
||||
renderer.drawText(fontId, wordX, y, words[i].c_str(), true, currentStyle);
|
||||
}
|
||||
|
||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = words[i];
|
||||
@@ -42,9 +68,15 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
}
|
||||
|
||||
bool TextBlock::serialize(FsFile& file) const {
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
|
||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", words.size(),
|
||||
wordXpos.size(), wordStyles.size());
|
||||
// Focus annotations are optional; vectors are either empty (no splits in this block)
|
||||
// or sized in lockstep with words[].
|
||||
const bool hasFocus = !wordFocusBoundary.empty();
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() ||
|
||||
(hasFocus && (words.size() != wordFocusBoundary.size() || words.size() != wordFocusSuffixX.size()))) {
|
||||
LOG_ERR("TXB", "Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)\n",
|
||||
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(wordFocusBoundary.size()),
|
||||
static_cast<uint32_t>(wordFocusSuffixX.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,6 +85,13 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
for (const auto& w : words) serialization::writeString(file, w);
|
||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||
// Focus block: 1-byte presence flag, followed by per-word vectors only when present.
|
||||
// Saves 3 bytes/word when focus reading is disabled or no word on this line was split.
|
||||
serialization::writePod(file, static_cast<uint8_t>(hasFocus ? 1 : 0));
|
||||
if (hasFocus) {
|
||||
for (auto b : wordFocusBoundary) serialization::writePod(file, b);
|
||||
for (auto sx : wordFocusSuffixX) serialization::writePod(file, sx);
|
||||
}
|
||||
|
||||
// Style (alignment + margins/padding/indent)
|
||||
serialization::writePod(file, blockStyle.alignment);
|
||||
@@ -76,6 +115,8 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
std::vector<std::string> words;
|
||||
std::vector<int16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<uint8_t> wordFocusBoundary;
|
||||
std::vector<uint16_t> wordFocusSuffixX;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
@@ -94,6 +135,16 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
for (auto& w : words) serialization::readString(file, w);
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
// Focus block: presence flag, then vectors only if present. Empty vectors when absent
|
||||
// signal "no splits in this block" to render() (zero per-word RAM cost).
|
||||
uint8_t hasFocus;
|
||||
serialization::readPod(file, hasFocus);
|
||||
if (hasFocus) {
|
||||
wordFocusBoundary.resize(wc);
|
||||
wordFocusSuffixX.resize(wc);
|
||||
for (auto& b : wordFocusBoundary) serialization::readPod(file, b);
|
||||
for (auto& sx : wordFocusSuffixX) serialization::readPod(file, sx);
|
||||
}
|
||||
|
||||
// Style (alignment + margins/padding/indent)
|
||||
serialization::readPod(file, blockStyle.alignment);
|
||||
@@ -109,6 +160,7 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
serialization::readPod(file, blockStyle.textIndent);
|
||||
serialization::readPod(file, blockStyle.textIndentDefined);
|
||||
|
||||
return std::unique_ptr<TextBlock>(
|
||||
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
|
||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles),
|
||||
std::move(wordFocusBoundary), std::move(wordFocusSuffixX),
|
||||
blockStyle));
|
||||
}
|
||||
|
||||
@@ -15,14 +15,28 @@ class TextBlock final : public Block {
|
||||
std::vector<std::string> words;
|
||||
std::vector<int16_t> wordXpos;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
// Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
|
||||
// the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
|
||||
// N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
|
||||
// FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
|
||||
// Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
|
||||
// when focus reading is disabled, or on lines that happen to contain no splittable words).
|
||||
std::vector<uint8_t> wordFocusBoundary;
|
||||
// Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
|
||||
// Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
|
||||
// Empty in lockstep with wordFocusBoundary.
|
||||
std::vector<uint16_t> wordFocusSuffixX;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
|
||||
std::vector<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
|
||||
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
wordFocusBoundary(std::move(focus_boundary)),
|
||||
wordFocusSuffixX(std::move(focus_suffix_x)),
|
||||
blockStyle(blockStyle) {}
|
||||
~TextBlock() override = default;
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
|
||||
@@ -141,7 +141,7 @@ void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
anchorData.push_back({std::move(pendingAnchorId), static_cast<uint16_t>(completedPageCount)});
|
||||
pendingAnchorId.clear();
|
||||
}
|
||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, focusReadingEnabled, blockStyle));
|
||||
wordsExtractedInBlock = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class ChapterHtmlSlimParser {
|
||||
uint16_t viewportWidth;
|
||||
uint16_t viewportHeight;
|
||||
bool hyphenationEnabled;
|
||||
bool focusReadingEnabled;
|
||||
const CssParser* cssParser;
|
||||
bool embeddedStyle;
|
||||
uint8_t imageRendering;
|
||||
@@ -101,6 +102,7 @@ class ChapterHtmlSlimParser {
|
||||
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool focusReadingEnabled,
|
||||
const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn,
|
||||
const bool embeddedStyle, const std::string& contentBase,
|
||||
const std::string& imageBasePath, const uint8_t imageRendering = 0,
|
||||
@@ -116,6 +118,7 @@ class ChapterHtmlSlimParser {
|
||||
viewportWidth(viewportWidth),
|
||||
viewportHeight(viewportHeight),
|
||||
hyphenationEnabled(hyphenationEnabled),
|
||||
focusReadingEnabled(focusReadingEnabled),
|
||||
completePageFn(completePageFn),
|
||||
popupFn(popupFn),
|
||||
cssParser(cssParser),
|
||||
|
||||
@@ -259,6 +259,7 @@ STR_SECTION_PREFIX: "Раздзел"
|
||||
STR_UPLOAD: "Адправіць"
|
||||
STR_BOOK_S_STYLE: "Стыль кнігі"
|
||||
STR_EMBEDDED_STYLE: "Убудаваны стыль"
|
||||
STR_FOCUS_READING: "Фокуснае чытанне"
|
||||
STR_OPDS_SERVER_URL: "URL OPDS сервера"
|
||||
STR_SCREENSHOT_BUTTON: "Зрабіць здымак экрана"
|
||||
STR_IMAGES: "Выявы"
|
||||
|
||||
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secció "
|
||||
STR_UPLOAD: "Puja"
|
||||
STR_BOOK_S_STYLE: "Estil del llibre"
|
||||
STR_EMBEDDED_STYLE: "Estil incrustat"
|
||||
STR_FOCUS_READING: "Lectura enfocada"
|
||||
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||
STR_FOOTNOTES: "Notes al peu"
|
||||
STR_NO_FOOTNOTES: "No hi ha notes al peu en aquesta pàgina"
|
||||
|
||||
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Sekce"
|
||||
STR_UPLOAD: "Nahrát"
|
||||
STR_BOOK_S_STYLE: "Styl knihy"
|
||||
STR_EMBEDDED_STYLE: "Vložený styl"
|
||||
STR_FOCUS_READING: "Soustředěné čtení"
|
||||
STR_OPDS_SERVER_URL: "URL serveru OPDS"
|
||||
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
|
||||
STR_TILT_PAGE_TURN: "Otáčení stránek nakloněním"
|
||||
|
||||
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Afsnit "
|
||||
STR_UPLOAD: "Upload"
|
||||
STR_BOOK_S_STYLE: "Bogens stil"
|
||||
STR_EMBEDDED_STYLE: "Indlejret stil"
|
||||
STR_FOCUS_READING: "Fokuslæsning"
|
||||
STR_OPDS_SERVER_URL: "OPDS Server URL"
|
||||
STR_FOOTNOTES: "Fodnoter"
|
||||
STR_NO_FOOTNOTES: "Ingen fodnoter på denne side"
|
||||
|
||||
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Sectie "
|
||||
STR_UPLOAD: "Uploaden"
|
||||
STR_BOOK_S_STYLE: "Stijl van boek"
|
||||
STR_EMBEDDED_STYLE: "Ingebedde stijl"
|
||||
STR_FOCUS_READING: "Gefocust lezen"
|
||||
STR_OPDS_SERVER_URL: "OPDS-server URL"
|
||||
STR_FOOTNOTES: "Voetnoten"
|
||||
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
|
||||
|
||||
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Section "
|
||||
STR_UPLOAD: "Upload"
|
||||
STR_BOOK_S_STYLE: "Book's Style"
|
||||
STR_EMBEDDED_STYLE: "Embedded Style"
|
||||
STR_FOCUS_READING: "Focus Reading"
|
||||
STR_OPDS_SERVER_URL: "OPDS Server URL"
|
||||
STR_SET_SLEEP_COVER: "Set Cover"
|
||||
STR_FOOTNOTES: "Footnotes"
|
||||
|
||||
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Osio "
|
||||
STR_UPLOAD: "Lähetä"
|
||||
STR_BOOK_S_STYLE: "Kirjan tyyli"
|
||||
STR_EMBEDDED_STYLE: "Upotettu tyyli"
|
||||
STR_FOCUS_READING: "Keskittynyt lukeminen"
|
||||
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
|
||||
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
|
||||
STR_TILT_PAGE_TURN: "Sivunkääntö kallistamalla"
|
||||
|
||||
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Section "
|
||||
STR_UPLOAD: "Envoyer"
|
||||
STR_BOOK_S_STYLE: "Style du livre"
|
||||
STR_EMBEDDED_STYLE: "Style intégré"
|
||||
STR_FOCUS_READING: "Lecture focalisée"
|
||||
STR_OPDS_SERVER_URL: "URL serveur OPDS"
|
||||
STR_FOOTNOTES: "Notes de bas de page"
|
||||
STR_NO_FOOTNOTES: "Aucune note sur cette page"
|
||||
|
||||
@@ -289,6 +289,7 @@ STR_SECTION_PREFIX: "Abschnitt"
|
||||
STR_UPLOAD: "Hochladen"
|
||||
STR_BOOK_S_STYLE: "Buch-Stil"
|
||||
STR_EMBEDDED_STYLE: "Eingebetteter Stil"
|
||||
STR_FOCUS_READING: "Fokus-Lesen"
|
||||
STR_OPDS_SERVER_URL: "OPDS-Server-URL"
|
||||
STR_SCREENSHOT_BUTTON: "Screenshot aufnehmen"
|
||||
STR_FOOTNOTES: "Fußnoten"
|
||||
|
||||
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Szakasz "
|
||||
STR_UPLOAD: "Feltöltés"
|
||||
STR_BOOK_S_STYLE: "Könyv stílusa"
|
||||
STR_EMBEDDED_STYLE: "Beágyazott stílus"
|
||||
STR_FOCUS_READING: "Fókuszált olvasás"
|
||||
STR_OPDS_SERVER_URL: "OPDS szerver URL"
|
||||
STR_FOOTNOTES: "Lábjegyzetek"
|
||||
STR_NO_FOOTNOTES: "Nincsenek lábjegyzetek ezen az oldalon"
|
||||
|
||||
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Sezione "
|
||||
STR_UPLOAD: "Carica"
|
||||
STR_BOOK_S_STYLE: "Stile libro"
|
||||
STR_EMBEDDED_STYLE: "Stile integrato dell'epub"
|
||||
STR_FOCUS_READING: "Lettura focalizzata"
|
||||
STR_OPDS_SERVER_URL: "Server OPDS"
|
||||
STR_FOOTNOTES: "Note a piè pagina"
|
||||
STR_NO_FOOTNOTES: "Nessuna nota in questa pagina"
|
||||
|
||||
@@ -258,6 +258,7 @@ STR_SECTION_PREFIX: "Бөлім "
|
||||
STR_UPLOAD: "Жүктеп салу"
|
||||
STR_BOOK_S_STYLE: "Кітап стилі"
|
||||
STR_EMBEDDED_STYLE: "Кірістірілген стиль"
|
||||
STR_FOCUS_READING: "Зейінді оқу"
|
||||
STR_OPDS_SERVER_URL: "OPDS сервері URL"
|
||||
STR_NO_FILES_FOUND: "Файлдар табылмады"
|
||||
STR_IMAGES: "Суреттер"
|
||||
|
||||
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Dalis "
|
||||
STR_UPLOAD: "Įkelti"
|
||||
STR_BOOK_S_STYLE: "Knygos stilius"
|
||||
STR_EMBEDDED_STYLE: "Integruotas stilius"
|
||||
STR_FOCUS_READING: "Sufokusuotas skaitymas"
|
||||
STR_OPDS_SERVER_URL: "OPDS URL"
|
||||
STR_FOOTNOTES: "Išnašos"
|
||||
STR_NO_FOOTNOTES: "Šiame psl. išnašų nėra"
|
||||
|
||||
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sekcja "
|
||||
STR_UPLOAD: "Wyślij"
|
||||
STR_BOOK_S_STYLE: "Styl książki"
|
||||
STR_EMBEDDED_STYLE: "Style wbudowane w EPUB"
|
||||
STR_FOCUS_READING: "Czytanie skupione"
|
||||
STR_OPDS_SERVER_URL: "URL serwera OPDS"
|
||||
STR_SET_SLEEP_COVER: "Ustaw okładkę"
|
||||
STR_FOOTNOTES: "Przypisy"
|
||||
|
||||
@@ -264,6 +264,7 @@ STR_SECTION_PREFIX: "Seção"
|
||||
STR_UPLOAD: "Enviar"
|
||||
STR_BOOK_S_STYLE: "Estilo do livro"
|
||||
STR_EMBEDDED_STYLE: "Estilo embutido"
|
||||
STR_FOCUS_READING: "Leitura focada"
|
||||
STR_OPDS_SERVER_URL: "URL do servidor OPDS"
|
||||
STR_SCREENSHOT_BUTTON: "Capturar tela"
|
||||
STR_TILT_PAGE_TURN: "Virar página por inclinação"
|
||||
|
||||
@@ -287,6 +287,7 @@ STR_SECTION_PREFIX: "Secţiune "
|
||||
STR_UPLOAD: "Încărcare"
|
||||
STR_BOOK_S_STYLE: "Stilul cărţii"
|
||||
STR_EMBEDDED_STYLE: "Stil încorporat"
|
||||
STR_FOCUS_READING: "Lectură concentrată"
|
||||
STR_OPDS_SERVER_URL: "URL server OPDS"
|
||||
STR_FOOTNOTES: "Note de subsol"
|
||||
STR_NO_FOOTNOTES: "Nicio notă de subsol"
|
||||
|
||||
@@ -291,6 +291,7 @@ STR_SECTION_PREFIX: "Раздел "
|
||||
STR_UPLOAD: "Отправить"
|
||||
STR_BOOK_S_STYLE: "Стиль книги"
|
||||
STR_EMBEDDED_STYLE: "Встроенный стиль"
|
||||
STR_FOCUS_READING: "Фокусное чтение"
|
||||
STR_OPDS_SERVER_URL: "URL OPDS сервера"
|
||||
STR_SCREENSHOT_BUTTON: "Сделать снимок экрана"
|
||||
STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
|
||||
|
||||
@@ -284,6 +284,7 @@ STR_SECTION_PREFIX: "Razdelek "
|
||||
STR_UPLOAD: "Naloži"
|
||||
STR_BOOK_S_STYLE: "Slog knjige"
|
||||
STR_EMBEDDED_STYLE: "Vgrajen slog"
|
||||
STR_FOCUS_READING: "Fokusirano branje"
|
||||
STR_OPDS_SERVER_URL: "URL OPDS strežnika"
|
||||
STR_FOOTNOTES: "Opombe"
|
||||
STR_NO_FOOTNOTES: "Na tej strani ni opomb"
|
||||
|
||||
@@ -288,6 +288,7 @@ STR_SECTION_PREFIX: "Secc.:"
|
||||
STR_UPLOAD: "Subir"
|
||||
STR_BOOK_S_STYLE: "Estilo del libro"
|
||||
STR_EMBEDDED_STYLE: "Estilo integrado"
|
||||
STR_FOCUS_READING: "Lectura enfocada"
|
||||
STR_OPDS_SERVER_URL: "URL del servidor OPDS"
|
||||
STR_FOOTNOTES: "Pie de página"
|
||||
STR_NO_FOOTNOTES: "No hay notas al pie de esta página"
|
||||
|
||||
@@ -296,6 +296,7 @@ STR_SECTION_PREFIX: "Sektion"
|
||||
STR_UPLOAD: "Uppladdning"
|
||||
STR_BOOK_S_STYLE: "Bokstil"
|
||||
STR_EMBEDDED_STYLE: "Inbäddad stil"
|
||||
STR_FOCUS_READING: "Fokusläsning"
|
||||
STR_OPDS_SERVER_URL: "OPDS-serveradress"
|
||||
STR_SET_SLEEP_COVER: "Ställ in omslag"
|
||||
STR_FOOTNOTES: "Fotnoter"
|
||||
|
||||
@@ -262,6 +262,7 @@ STR_SECTION_PREFIX: "Bölüm "
|
||||
STR_UPLOAD: "Yükle"
|
||||
STR_BOOK_S_STYLE: "Kitabın Stili"
|
||||
STR_EMBEDDED_STYLE: "Gömülü Stil"
|
||||
STR_FOCUS_READING: "Odaklanmış Okuma"
|
||||
STR_OPDS_SERVER_URL: "OPDS Sunucu Adresi"
|
||||
STR_AUTO_TURN_ENABLED: "Otomatik Çevirme Etkin: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Otomatik Çevirme (Dakikada Sayfa)"
|
||||
|
||||
@@ -292,6 +292,7 @@ STR_SECTION_PREFIX: "Розділ "
|
||||
STR_UPLOAD: "Завантажити"
|
||||
STR_BOOK_S_STYLE: "Стиль книги"
|
||||
STR_EMBEDDED_STYLE: "Вбудований стиль"
|
||||
STR_FOCUS_READING: "Фокусне читання"
|
||||
STR_OPDS_SERVER_URL: "URL сервера OPDS"
|
||||
STR_FOOTNOTES: "Примітки"
|
||||
STR_NO_FOOTNOTES: "На цій сторінці немає приміток"
|
||||
|
||||
+1
-1
Submodule open-x4-sdk updated: 7d86603ad2...a64a3c29be
@@ -213,6 +213,8 @@ class CrossPointSettings {
|
||||
uint8_t fadingFix = 0;
|
||||
// Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled)
|
||||
uint8_t embeddedStyle = 1;
|
||||
// Focus Reading - emphasizes the first part of words with bold
|
||||
uint8_t focusReadingEnabled = 0;
|
||||
// SD card font family name (empty = use built-in fontFamily)
|
||||
char sdFontFamilyName[32] = "";
|
||||
// Show hidden files/directories (starting with '.') in the file browser (0 = hidden, 1 = show)
|
||||
|
||||
@@ -145,6 +145,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
|
||||
"paragraphAlignment", StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_EMBEDDED_STYLE, &CrossPointSettings::embeddedStyle, "embeddedStyle",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_FOCUS_READING, &CrossPointSettings::focusReadingEnabled, "focusReadingEnabled",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Toggle(StrId::STR_HYPHENATION, &CrossPointSettings::hyphenationEnabled, "hyphenationEnabled",
|
||||
StrId::STR_CAT_READER),
|
||||
SettingInfo::Enum(StrId::STR_ORIENTATION, &CrossPointSettings::orientation,
|
||||
|
||||
@@ -609,7 +609,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_DBG("ERS", "Cache not found, building...");
|
||||
|
||||
GUI.drawPopup(renderer, tr(STR_INDEXING));
|
||||
@@ -619,7 +619,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, popupFn)) {
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) {
|
||||
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
@@ -750,7 +750,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
|
||||
if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
|
||||
if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering)) {
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
|
||||
LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user