Compare commits
4 Commits
c171813045
...
48267ad848
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48267ad848 | ||
|
|
dd630dcf72 | ||
|
|
ef705d3ac6 | ||
|
|
bab374a675 |
@ -6,6 +6,30 @@ Base: CrossPoint Reader 0.15.0
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.4
|
||||
|
||||
**EPUB Rendering & Stability**
|
||||
|
||||
### New Features
|
||||
|
||||
- **End-of-Book "Start Over"**: Press next at end of book to wrap to first page
|
||||
|
||||
### EPUB Rendering Improvements
|
||||
|
||||
- CSS `margin-left`/`padding-left` parsing for block indentation
|
||||
- Vertical bar and italic styling for blockquotes
|
||||
- Left margin indentation for list items (`<ol>`/`<ul>`)
|
||||
- Fixed ordered lists showing bullets instead of numbers
|
||||
- Fixed nested `<p>` inside `<li>` causing marker on separate line
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Webserver**: Fixed file listing disconnection issues with flow control
|
||||
- **Webserver**: Memory optimization for File Transfer mode (frees heap before starting)
|
||||
- **Dictionary**: Fixed zip dictionary allocation order for better memory allocation success
|
||||
|
||||
---
|
||||
|
||||
## ef-1.0.3
|
||||
|
||||
**Maintenance Release**
|
||||
|
||||
@ -68,7 +68,9 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
// Apply fixed transforms before any per-line layout work.
|
||||
applyParagraphIndent();
|
||||
|
||||
const int pageWidth = viewportWidth;
|
||||
// Apply horizontal margin (for blockquotes, nested content, etc.)
|
||||
const int leftMargin = blockStyle.marginLeft;
|
||||
const int pageWidth = viewportWidth - leftMargin;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
std::vector<size_t> lineBreakIndices;
|
||||
@ -81,7 +83,7 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||
|
||||
for (size_t i = 0; i < lineCount; ++i) {
|
||||
extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine);
|
||||
extractLine(i, pageWidth, spaceWidth, leftMargin, wordWidths, lineBreakIndices, processLine);
|
||||
}
|
||||
}
|
||||
|
||||
@ -336,7 +338,7 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl
|
||||
return true;
|
||||
}
|
||||
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, const int leftMargin,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
||||
@ -359,12 +361,12 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
}
|
||||
|
||||
// Calculate initial x position
|
||||
uint16_t xpos = 0;
|
||||
// Calculate initial x position (offset by left margin for blockquotes, etc.)
|
||||
uint16_t xpos = static_cast<uint16_t>(leftMargin);
|
||||
if (style == TextBlock::RIGHT_ALIGN) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
xpos += spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
xpos += (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
|
||||
@ -28,8 +28,8 @@ class ParsedText {
|
||||
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, int leftMargin,
|
||||
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
// Version 12: Added content offsets to LUT for position restoration after re-indexing
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
||||
// Version 13: Added marginLeft and hasLeftBorder to BlockStyle serialization
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||
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(uint32_t);
|
||||
|
||||
@ -13,5 +13,7 @@ struct BlockStyle {
|
||||
int8_t marginBottom = 0; // 0-2 lines
|
||||
int8_t paddingTop = 0; // 0-2 lines (treated same as margin)
|
||||
int8_t paddingBottom = 0; // 0-2 lines (treated same as margin)
|
||||
int16_t textIndent = 0; // pixels
|
||||
int16_t textIndent = 0; // pixels (first line indent)
|
||||
int16_t marginLeft = 0; // pixels (horizontal indent for entire block)
|
||||
bool hasLeftBorder = false; // draw vertical bar in left margin (for blockquotes)
|
||||
};
|
||||
|
||||
@ -11,6 +11,17 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw left border (vertical bar) for blockquotes
|
||||
if (blockStyle.hasLeftBorder && blockStyle.marginLeft > 0) {
|
||||
const int lineHeight = renderer.getLineHeight(fontId);
|
||||
const int barX = x + 4; // Small offset from left edge
|
||||
const int barTop = y;
|
||||
const int barBottom = y + lineHeight;
|
||||
// Draw a 2-pixel wide vertical bar
|
||||
renderer.drawLine(barX, barTop, barX, barBottom, true);
|
||||
renderer.drawLine(barX + 1, barTop, barX + 1, barBottom, true);
|
||||
}
|
||||
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
@ -92,6 +103,8 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
serialization::writePod(file, blockStyle.paddingTop);
|
||||
serialization::writePod(file, blockStyle.paddingBottom);
|
||||
serialization::writePod(file, blockStyle.textIndent);
|
||||
serialization::writePod(file, blockStyle.marginLeft);
|
||||
serialization::writePod(file, blockStyle.hasLeftBorder);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -144,6 +157,8 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
serialization::readPod(file, blockStyle.paddingTop);
|
||||
serialization::readPod(file, blockStyle.paddingBottom);
|
||||
serialization::readPod(file, blockStyle.textIndent);
|
||||
serialization::readPod(file, blockStyle.marginLeft);
|
||||
serialization::readPod(file, blockStyle.hasLeftBorder);
|
||||
|
||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
||||
blockStyle, std::move(wordUnderlines)));
|
||||
|
||||
@ -393,6 +393,32 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
style.paddingBottom = spacing;
|
||||
style.defined.paddingBottom = 1;
|
||||
}
|
||||
} else if (propName == "margin-left" || propName == "padding-left") {
|
||||
// Horizontal indentation for blockquotes and nested content
|
||||
const float pixels = interpretLength(propValue);
|
||||
if (pixels > 0) {
|
||||
style.marginLeft += pixels; // Accumulate margin-left and padding-left
|
||||
style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (propName == "margin") {
|
||||
// Shorthand: margin: top right bottom left OR margin: vertical horizontal
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (values.size() >= 2) {
|
||||
// At least 2 values: first is vertical (top/bottom), second is horizontal (left/right)
|
||||
const float horizontal = interpretLength(values[1]);
|
||||
if (horizontal > 0) {
|
||||
style.marginLeft = horizontal;
|
||||
style.defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
if (values.size() == 4) {
|
||||
// 4 values: top right bottom left - use the 4th value for left
|
||||
const float left = interpretLength(values[3]);
|
||||
if (left > 0) {
|
||||
style.marginLeft = left;
|
||||
style.defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,8 @@ struct CssPropertyFlags {
|
||||
uint16_t marginBottom : 1;
|
||||
uint16_t paddingTop : 1;
|
||||
uint16_t paddingBottom : 1;
|
||||
uint16_t reserved : 7;
|
||||
uint16_t marginLeft : 1;
|
||||
uint16_t reserved : 6;
|
||||
|
||||
CssPropertyFlags()
|
||||
: alignment(0),
|
||||
@ -37,16 +38,17 @@ struct CssPropertyFlags {
|
||||
marginBottom(0),
|
||||
paddingTop(0),
|
||||
paddingBottom(0),
|
||||
marginLeft(0),
|
||||
reserved(0) {}
|
||||
|
||||
[[nodiscard]] bool anySet() const {
|
||||
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || paddingTop ||
|
||||
paddingBottom;
|
||||
paddingBottom || marginLeft;
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
alignment = fontStyle = fontWeight = decoration = indent = 0;
|
||||
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
||||
marginTop = marginBottom = paddingTop = paddingBottom = marginLeft = 0;
|
||||
}
|
||||
};
|
||||
|
||||
@ -63,6 +65,7 @@ struct CssStyle {
|
||||
int8_t marginBottom = 0; // Vertical spacing after block (in lines, 0-2)
|
||||
int8_t paddingTop = 0; // Padding before (in lines, 0-2)
|
||||
int8_t paddingBottom = 0; // Padding after (in lines, 0-2)
|
||||
float marginLeft = 0.0f; // Horizontal indent in pixels (for blockquotes, etc.)
|
||||
|
||||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||
|
||||
@ -105,6 +108,10 @@ struct CssStyle {
|
||||
paddingBottom = base.paddingBottom;
|
||||
defined.paddingBottom = 1;
|
||||
}
|
||||
if (base.defined.marginLeft) {
|
||||
marginLeft = base.marginLeft;
|
||||
defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatibility accessors for existing code that uses hasX pattern
|
||||
@ -117,6 +124,7 @@ struct CssStyle {
|
||||
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||||
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||||
|
||||
// Merge another style (alias for applyOver for compatibility)
|
||||
void merge(const CssStyle& other) { applyOver(other); }
|
||||
@ -128,6 +136,7 @@ struct CssStyle {
|
||||
decoration = CssTextDecoration::None;
|
||||
indentPixels = 0.0f;
|
||||
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
||||
marginLeft = 0.0f;
|
||||
defined.clearAll();
|
||||
}
|
||||
};
|
||||
|
||||
@ -19,6 +19,9 @@ constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
|
||||
const char* LIST_TAGS[] = {"ol", "ul"};
|
||||
constexpr int NUM_LIST_TAGS = sizeof(LIST_TAGS) / sizeof(LIST_TAGS[0]);
|
||||
|
||||
const char* BOLD_TAGS[] = {"b", "strong"};
|
||||
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
||||
|
||||
@ -55,6 +58,7 @@ BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle) {
|
||||
blockStyle.paddingTop = cssStyle.paddingTop;
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
||||
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
||||
blockStyle.marginLeft = static_cast<int16_t>(cssStyle.marginLeft);
|
||||
return blockStyle;
|
||||
}
|
||||
|
||||
@ -320,6 +324,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
|
||||
// Determine if this is a block element
|
||||
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||
bool isListTag = matches(name, LIST_TAGS, NUM_LIST_TAGS);
|
||||
|
||||
// Handle list container tags (ol, ul)
|
||||
if (isListTag) {
|
||||
ListContext ctx;
|
||||
ctx.isOrdered = strcmp(name, "ol") == 0;
|
||||
ctx.counter = 0;
|
||||
ctx.depth = self->depth;
|
||||
self->listStack.push_back(ctx);
|
||||
self->depth += 1;
|
||||
return; // Lists themselves don't create text blocks
|
||||
}
|
||||
|
||||
// Compute CSS style for this element
|
||||
CssStyle cssStyle;
|
||||
@ -365,6 +381,20 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
// This fixes issue where <br/> incorrectly wrapped the preceding word to a new line
|
||||
self->flushPartWordBuffer();
|
||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||
} else if (strcmp(name, "li") == 0) {
|
||||
// For list items, DON'T create a text block yet - wait for the first content element
|
||||
// This prevents the marker from being on its own line when <li><p>content</p></li>
|
||||
self->insideListItem = true;
|
||||
self->listItemDepth = self->depth;
|
||||
self->listItemHasContent = false;
|
||||
|
||||
// Increment counter now (so nested lists work correctly)
|
||||
if (!self->listStack.empty()) {
|
||||
self->listStack.back().counter++;
|
||||
}
|
||||
// Don't create text block or add marker yet - will be done when first content arrives
|
||||
self->depth += 1;
|
||||
return;
|
||||
} else {
|
||||
// Determine alignment from CSS or default
|
||||
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
||||
@ -387,12 +417,71 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
|
||||
// Apply default styling for blockquote if no CSS margin is specified
|
||||
const bool isBlockquote = strcmp(name, "blockquote") == 0;
|
||||
if (isBlockquote) {
|
||||
if (!cssStyle.hasMarginLeft()) {
|
||||
// Default left indent for blockquotes (~1.5em at 16px base = 24px)
|
||||
cssStyle.marginLeft = 24.0f;
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
}
|
||||
// Also make blockquotes italic by default if not specified
|
||||
if (!cssStyle.hasFontStyle()) {
|
||||
cssStyle.fontStyle = CssFontStyle::Italic;
|
||||
cssStyle.defined.fontStyle = 1;
|
||||
}
|
||||
// Track blockquote context for child elements
|
||||
self->insideBlockquote = true;
|
||||
self->blockquoteDepth = self->depth;
|
||||
self->blockquoteMarginLeft = cssStyle.marginLeft;
|
||||
}
|
||||
|
||||
// Apply blockquote styling to child block elements
|
||||
if (self->insideBlockquote && !isBlockquote) {
|
||||
// Inherit margin and border from parent blockquote
|
||||
if (!cssStyle.hasMarginLeft()) {
|
||||
cssStyle.marginLeft = self->blockquoteMarginLeft;
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply left margin to list items (indent the whole block)
|
||||
if (self->insideListItem && !cssStyle.hasMarginLeft()) {
|
||||
// Default left indent for list items (~1.5em at 16px base = 24px)
|
||||
cssStyle.marginLeft = 24.0f;
|
||||
cssStyle.defined.marginLeft = 1;
|
||||
}
|
||||
|
||||
self->currentBlockStyle = cssStyle;
|
||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle));
|
||||
BlockStyle blockStyleForElement = createBlockStyleFromCss(cssStyle);
|
||||
if (isBlockquote || self->insideBlockquote) {
|
||||
blockStyleForElement.hasLeftBorder = true; // Draw vertical bar for blockquotes
|
||||
}
|
||||
self->startNewTextBlock(alignment, blockStyleForElement);
|
||||
self->updateEffectiveInlineStyle();
|
||||
|
||||
if (strcmp(name, "li") == 0) {
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
// If this is a blockquote, apply italic styling
|
||||
if (isBlockquote && cssStyle.hasFontStyle() && cssStyle.fontStyle == CssFontStyle::Italic) {
|
||||
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||
}
|
||||
|
||||
// If this is the first block element inside a list item, add the marker
|
||||
if (self->insideListItem && !self->listItemHasContent) {
|
||||
if (!self->listStack.empty()) {
|
||||
const ListContext& ctx = self->listStack.back();
|
||||
if (ctx.isOrdered) {
|
||||
// Ordered list: use number (counter was already incremented)
|
||||
std::string marker = std::to_string(ctx.counter) + ". ";
|
||||
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
|
||||
} else {
|
||||
// Unordered list: use bullet
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
} else {
|
||||
// No list context (orphan li), use bullet as fallback
|
||||
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
||||
}
|
||||
self->listItemHasContent = true;
|
||||
}
|
||||
}
|
||||
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||
@ -566,7 +655,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
const bool shouldFlush = styleWillChange || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) ||
|
||||
matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || self->depth == 1;
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) ||
|
||||
matches(name, LIST_TAGS, NUM_LIST_TAGS) || self->depth == 1;
|
||||
|
||||
if (shouldFlush) {
|
||||
// Use combined depth-based and CSS-based style
|
||||
@ -596,6 +686,27 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
self->skipUntilDepth = INT_MAX;
|
||||
}
|
||||
|
||||
// Leaving list container (ol, ul)
|
||||
if (matches(name, LIST_TAGS, NUM_LIST_TAGS)) {
|
||||
if (!self->listStack.empty() && self->listStack.back().depth == self->depth) {
|
||||
self->listStack.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Leaving list item (li)
|
||||
if (strcmp(name, "li") == 0 && self->listItemDepth == self->depth) {
|
||||
self->insideListItem = false;
|
||||
self->listItemDepth = INT_MAX;
|
||||
self->listItemHasContent = false;
|
||||
}
|
||||
|
||||
// Leaving blockquote
|
||||
if (strcmp(name, "blockquote") == 0 && self->blockquoteDepth == self->depth) {
|
||||
self->insideBlockquote = false;
|
||||
self->blockquoteDepth = INT_MAX;
|
||||
self->blockquoteMarginLeft = 0.0f;
|
||||
}
|
||||
|
||||
// Leaving bold tag
|
||||
if (self->boldUntilDepth == self->depth) {
|
||||
self->boldUntilDepth = INT_MAX;
|
||||
|
||||
@ -59,6 +59,22 @@ class ChapterHtmlSlimParser {
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
|
||||
// List context tracking for ordered/unordered lists
|
||||
struct ListContext {
|
||||
bool isOrdered = false; // true for <ol>, false for <ul>
|
||||
int counter = 0; // Current item number (for ordered lists)
|
||||
int depth = 0; // Depth at which list was opened
|
||||
};
|
||||
std::vector<ListContext> listStack;
|
||||
bool insideListItem = false; // True when we're inside an <li> element
|
||||
int listItemDepth = INT_MAX; // Depth at which <li> was opened
|
||||
bool listItemHasContent = false; // True if we've added content to the current list item
|
||||
|
||||
// Blockquote context tracking (for left border on child elements)
|
||||
bool insideBlockquote = false;
|
||||
int blockquoteDepth = INT_MAX;
|
||||
float blockquoteMarginLeft = 0.0f; // Inherit margin from blockquote to child elements
|
||||
|
||||
// Byte offset tracking for position restoration after re-indexing
|
||||
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
||||
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
||||
|
||||
@ -529,10 +529,24 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
}
|
||||
|
||||
if (fileStat.method == MZ_DEFLATED) {
|
||||
// Setup inflator
|
||||
// Allocate largest buffer first to maximize chance of finding contiguous block
|
||||
// Dictionary buffer (32KB) - needed for DEFLATE sliding window
|
||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||
if (!outputBuffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary (need %d bytes)\n", millis(),
|
||||
TINFL_LZ_DICT_SIZE);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memset(outputBuffer, 0, TINFL_LZ_DICT_SIZE);
|
||||
|
||||
// Setup inflator (~11KB)
|
||||
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||
if (!inflator) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||
free(outputBuffer);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
@ -541,29 +555,18 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||
tinfl_init(inflator);
|
||||
|
||||
// Setup file read buffer
|
||||
// Setup file read buffer (smallest allocation last)
|
||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!fileReadBuffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
||||
free(inflator);
|
||||
free(outputBuffer);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto outputBuffer = static_cast<uint8_t*>(malloc(TINFL_LZ_DICT_SIZE));
|
||||
if (!outputBuffer) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for dictionary\n", millis());
|
||||
free(inflator);
|
||||
free(fileReadBuffer);
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memset(outputBuffer, 0, TINFL_LZ_DICT_SIZE);
|
||||
|
||||
size_t fileRemainingBytes = deflatedDataSize;
|
||||
size_t processedOutputBytes = 0;
|
||||
size_t fileReadBufferFilledBytes = 0;
|
||||
|
||||
@ -3,7 +3,7 @@ default_envs = default
|
||||
|
||||
[crosspoint]
|
||||
# 0.15.0 CrossPoint base, ef-1.0.0 is the first release of the ef branch
|
||||
version = 0.15.ef-1.0.3
|
||||
version = 0.15.ef-1.0.4
|
||||
|
||||
[base]
|
||||
platform = espressif32 @ 6.12.0
|
||||
|
||||
@ -52,6 +52,13 @@ void RecentBooksStore::clearAll() {
|
||||
Serial.printf("[%lu] [RBS] Cleared all recent books\n", millis());
|
||||
}
|
||||
|
||||
void RecentBooksStore::clearFromMemory() {
|
||||
const size_t count = recentBooks.size();
|
||||
recentBooks.clear();
|
||||
recentBooks.shrink_to_fit(); // Actually free the vector capacity
|
||||
Serial.printf("[%lu] [RBS] Cleared %d recent books from memory (not saved)\n", millis(), count);
|
||||
}
|
||||
|
||||
bool RecentBooksStore::saveToFile() const {
|
||||
// Make sure the directory exists
|
||||
SdMan.mkdir("/.crosspoint");
|
||||
|
||||
@ -29,9 +29,14 @@ class RecentBooksStore {
|
||||
// Returns true if the book was found and removed
|
||||
bool removeBook(const std::string& path);
|
||||
|
||||
// Clear all recent books from the list
|
||||
// Clear all recent books from the list (and save to file)
|
||||
void clearAll();
|
||||
|
||||
// Clear recent books from memory without saving to file
|
||||
// Used to free memory when entering modes that don't need this data (e.g., File Transfer)
|
||||
// Call loadFromFile() to restore the data when needed again
|
||||
void clearFromMemory();
|
||||
|
||||
// Get the list of recent books (most recent first)
|
||||
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
||||
|
||||
|
||||
@ -236,6 +236,15 @@ void EpubReaderActivity::loop() {
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
// Start over from beginning
|
||||
currentSpineIndex = 0;
|
||||
nextPageNumber = 0;
|
||||
showingEndOfBookPrompt = false;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -988,7 +997,7 @@ void EpubReaderActivity::renderEndOfBookPrompt() {
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "Start Over");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
|
||||
@ -171,6 +171,14 @@ void TxtReaderActivity::loop() {
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
// Start over from beginning
|
||||
currentPage = 0;
|
||||
showingEndOfBookPrompt = false;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -909,7 +917,7 @@ void TxtReaderActivity::renderEndOfBookPrompt() {
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "Start Over");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
|
||||
27
src/main.cpp
27
src/main.cpp
@ -381,6 +381,15 @@ void onGoToListsOrPinned() {
|
||||
|
||||
void onGoToFileTransfer() {
|
||||
exitActivity();
|
||||
|
||||
// Free memory not needed during file transfer to maximize heap for webserver
|
||||
RECENT_BOOKS.clearFromMemory();
|
||||
APP_STATE.openBookTitle.clear();
|
||||
APP_STATE.openBookTitle.shrink_to_fit();
|
||||
APP_STATE.openBookAuthor.clear();
|
||||
APP_STATE.openBookAuthor.shrink_to_fit();
|
||||
Serial.printf("[%lu] [FT] Cleared non-essential memory before File Transfer\n", millis());
|
||||
|
||||
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
||||
}
|
||||
|
||||
@ -396,12 +405,24 @@ void onGoToClearCache() {
|
||||
|
||||
void onGoToMyLibrary() {
|
||||
exitActivity();
|
||||
|
||||
// Reload recent books if they were cleared (e.g., when exiting File Transfer mode)
|
||||
if (RECENT_BOOKS.getCount() == 0) {
|
||||
RECENT_BOOKS.loadFromFile();
|
||||
}
|
||||
|
||||
enterNewActivity(
|
||||
new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
|
||||
}
|
||||
|
||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
||||
exitActivity();
|
||||
|
||||
// Reload recent books if they were cleared (e.g., when exiting File Transfer mode)
|
||||
if (RECENT_BOOKS.getCount() == 0) {
|
||||
RECENT_BOOKS.loadFromFile();
|
||||
}
|
||||
|
||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
|
||||
onGoToBookmarkList, tab, path));
|
||||
}
|
||||
@ -413,6 +434,12 @@ void onGoToBrowser() {
|
||||
|
||||
void onGoHome() {
|
||||
exitActivity();
|
||||
|
||||
// Reload recent books if they were cleared (e.g., when exiting File Transfer mode)
|
||||
if (RECENT_BOOKS.getCount() == 0) {
|
||||
RECENT_BOOKS.loadFromFile();
|
||||
}
|
||||
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToListsOrPinned,
|
||||
onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser));
|
||||
}
|
||||
|
||||
@ -441,17 +441,33 @@ void CrossPointWebServer::handleFileListData() const {
|
||||
showHidden = server->arg("showHidden") == "true";
|
||||
}
|
||||
|
||||
// Check client connection before starting
|
||||
if (!server->client().connected()) {
|
||||
Serial.printf("[%lu] [WEB] Client disconnected before file list could start\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
server->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
server->send(200, "application/json", "");
|
||||
server->sendContent("[");
|
||||
if (!sendContentSafe("[")) {
|
||||
Serial.printf("[%lu] [WEB] Client disconnected at start of file list\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
char output[512];
|
||||
constexpr size_t outputSize = sizeof(output);
|
||||
bool seenFirst = false;
|
||||
bool clientDisconnected = false;
|
||||
JsonDocument doc;
|
||||
|
||||
scanFiles(
|
||||
currentPath.c_str(),
|
||||
[this, &output, &doc, seenFirst](const FileInfo& info) mutable {
|
||||
[this, &output, &doc, &seenFirst, &clientDisconnected](const FileInfo& info) mutable {
|
||||
// Skip remaining files if client already disconnected
|
||||
if (clientDisconnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.clear();
|
||||
doc["name"] = info.name;
|
||||
doc["size"] = info.size;
|
||||
@ -475,18 +491,33 @@ void CrossPointWebServer::handleFileListData() const {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send comma separator before all entries except the first
|
||||
if (seenFirst) {
|
||||
server->sendContent(",");
|
||||
if (!sendContentSafe(",")) {
|
||||
clientDisconnected = true;
|
||||
Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
seenFirst = true;
|
||||
}
|
||||
server->sendContent(output);
|
||||
|
||||
// Send the JSON entry with flow control
|
||||
if (!sendContentSafe(output)) {
|
||||
clientDisconnected = true;
|
||||
Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis());
|
||||
return;
|
||||
}
|
||||
},
|
||||
showHidden);
|
||||
server->sendContent("]");
|
||||
// End of streamed response, empty chunk to signal client
|
||||
server->sendContent("");
|
||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||
|
||||
// Only send closing bracket if client is still connected
|
||||
if (!clientDisconnected) {
|
||||
sendContentSafe("]");
|
||||
// End of streamed response, empty chunk to signal client
|
||||
server->sendContent("");
|
||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Static variables for upload handling
|
||||
@ -1264,6 +1295,40 @@ void CrossPointWebServer::handleRename() const {
|
||||
}
|
||||
}
|
||||
|
||||
// Counter for flow control pacing
|
||||
static uint8_t sendContentCounter = 0;
|
||||
|
||||
bool CrossPointWebServer::sendContentSafe(const char* content) const {
|
||||
if (!server || !server->client().connected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the content
|
||||
server->sendContent(content);
|
||||
|
||||
// Flow control: give TCP stack time to transmit data and drain the send buffer
|
||||
// The ESP32 TCP buffer is limited and fills quickly when streaming many small chunks.
|
||||
// We use progressive delays:
|
||||
// - yield() after every send to allow WiFi processing
|
||||
// - delay(5ms) every send to allow buffer draining
|
||||
// - delay(50ms) every 10 sends to allow larger buffer flush
|
||||
yield();
|
||||
sendContentCounter++;
|
||||
|
||||
if (sendContentCounter >= 10) {
|
||||
sendContentCounter = 0;
|
||||
delay(50); // Longer pause every 10 sends for buffer catchup
|
||||
} else {
|
||||
delay(5); // Short pause each send
|
||||
}
|
||||
|
||||
return server->client().connected();
|
||||
}
|
||||
|
||||
bool CrossPointWebServer::sendContentSafe(const String& content) const {
|
||||
return sendContentSafe(content.c_str());
|
||||
}
|
||||
|
||||
bool CrossPointWebServer::copyFile(const String& srcPath, const String& destPath) const {
|
||||
FsFile srcFile;
|
||||
FsFile destFile;
|
||||
|
||||
@ -108,6 +108,11 @@ class CrossPointWebServer {
|
||||
bool copyFile(const String& srcPath, const String& destPath) const;
|
||||
bool copyFolder(const String& srcPath, const String& destPath) const;
|
||||
|
||||
// Helper for safe content sending with flow control
|
||||
// Returns false if client disconnected, true otherwise
|
||||
bool sendContentSafe(const char* content) const;
|
||||
bool sendContentSafe(const String& content) const;
|
||||
|
||||
// List management handlers
|
||||
void handleListGet() const;
|
||||
void handleListPost() const;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user