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
|
## ef-1.0.3
|
||||||
|
|
||||||
**Maintenance Release**
|
**Maintenance Release**
|
||||||
|
|||||||
@ -68,7 +68,9 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
|||||||
// Apply fixed transforms before any per-line layout work.
|
// Apply fixed transforms before any per-line layout work.
|
||||||
applyParagraphIndent();
|
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);
|
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||||
auto wordWidths = calculateWordWidths(renderer, fontId);
|
auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||||
std::vector<size_t> lineBreakIndices;
|
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;
|
const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1;
|
||||||
|
|
||||||
for (size_t i = 0; i < lineCount; ++i) {
|
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;
|
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::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||||
const size_t lineBreak = lineBreakIndices[breakIndex];
|
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);
|
spacing = spareSpace / (lineWordCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate initial x position
|
// Calculate initial x position (offset by left margin for blockquotes, etc.)
|
||||||
uint16_t xpos = 0;
|
uint16_t xpos = static_cast<uint16_t>(leftMargin);
|
||||||
if (style == TextBlock::RIGHT_ALIGN) {
|
if (style == TextBlock::RIGHT_ALIGN) {
|
||||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
xpos += spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
xpos += (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-calculate X positions for words
|
// Pre-calculate X positions for words
|
||||||
|
|||||||
@ -28,8 +28,8 @@ class ParsedText {
|
|||||||
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
int spaceWidth, std::vector<uint16_t>& wordWidths);
|
||||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths,
|
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, int leftMargin,
|
||||||
const std::vector<size_t>& lineBreakIndices,
|
const std::vector<uint16_t>& wordWidths, const std::vector<size_t>& lineBreakIndices,
|
||||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,8 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Version 12: Added content offsets to LUT for position restoration after re-indexing
|
// Version 13: Added marginLeft and hasLeftBorder to BlockStyle serialization
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 12;
|
constexpr uint8_t SECTION_FILE_VERSION = 13;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
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(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) +
|
||||||
sizeof(uint32_t);
|
sizeof(uint32_t);
|
||||||
|
|||||||
@ -13,5 +13,7 @@ struct BlockStyle {
|
|||||||
int8_t marginBottom = 0; // 0-2 lines
|
int8_t marginBottom = 0; // 0-2 lines
|
||||||
int8_t paddingTop = 0; // 0-2 lines (treated same as margin)
|
int8_t paddingTop = 0; // 0-2 lines (treated same as margin)
|
||||||
int8_t paddingBottom = 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;
|
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 wordIt = words.begin();
|
||||||
auto wordStylesIt = wordStyles.begin();
|
auto wordStylesIt = wordStyles.begin();
|
||||||
auto wordXposIt = wordXpos.begin();
|
auto wordXposIt = wordXpos.begin();
|
||||||
@ -92,6 +103,8 @@ bool TextBlock::serialize(FsFile& file) const {
|
|||||||
serialization::writePod(file, blockStyle.paddingTop);
|
serialization::writePod(file, blockStyle.paddingTop);
|
||||||
serialization::writePod(file, blockStyle.paddingBottom);
|
serialization::writePod(file, blockStyle.paddingBottom);
|
||||||
serialization::writePod(file, blockStyle.textIndent);
|
serialization::writePod(file, blockStyle.textIndent);
|
||||||
|
serialization::writePod(file, blockStyle.marginLeft);
|
||||||
|
serialization::writePod(file, blockStyle.hasLeftBorder);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -144,6 +157,8 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
|||||||
serialization::readPod(file, blockStyle.paddingTop);
|
serialization::readPod(file, blockStyle.paddingTop);
|
||||||
serialization::readPod(file, blockStyle.paddingBottom);
|
serialization::readPod(file, blockStyle.paddingBottom);
|
||||||
serialization::readPod(file, blockStyle.textIndent);
|
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,
|
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
||||||
blockStyle, std::move(wordUnderlines)));
|
blockStyle, std::move(wordUnderlines)));
|
||||||
|
|||||||
@ -393,6 +393,32 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
|||||||
style.paddingBottom = spacing;
|
style.paddingBottom = spacing;
|
||||||
style.defined.paddingBottom = 1;
|
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 marginBottom : 1;
|
||||||
uint16_t paddingTop : 1;
|
uint16_t paddingTop : 1;
|
||||||
uint16_t paddingBottom : 1;
|
uint16_t paddingBottom : 1;
|
||||||
uint16_t reserved : 7;
|
uint16_t marginLeft : 1;
|
||||||
|
uint16_t reserved : 6;
|
||||||
|
|
||||||
CssPropertyFlags()
|
CssPropertyFlags()
|
||||||
: alignment(0),
|
: alignment(0),
|
||||||
@ -37,16 +38,17 @@ struct CssPropertyFlags {
|
|||||||
marginBottom(0),
|
marginBottom(0),
|
||||||
paddingTop(0),
|
paddingTop(0),
|
||||||
paddingBottom(0),
|
paddingBottom(0),
|
||||||
|
marginLeft(0),
|
||||||
reserved(0) {}
|
reserved(0) {}
|
||||||
|
|
||||||
[[nodiscard]] bool anySet() const {
|
[[nodiscard]] bool anySet() const {
|
||||||
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || paddingTop ||
|
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || paddingTop ||
|
||||||
paddingBottom;
|
paddingBottom || marginLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearAll() {
|
void clearAll() {
|
||||||
alignment = fontStyle = fontWeight = decoration = indent = 0;
|
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 marginBottom = 0; // Vertical spacing after block (in lines, 0-2)
|
||||||
int8_t paddingTop = 0; // Padding before (in lines, 0-2)
|
int8_t paddingTop = 0; // Padding before (in lines, 0-2)
|
||||||
int8_t paddingBottom = 0; // Padding after (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
|
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||||
|
|
||||||
@ -105,6 +108,10 @@ struct CssStyle {
|
|||||||
paddingBottom = base.paddingBottom;
|
paddingBottom = base.paddingBottom;
|
||||||
defined.paddingBottom = 1;
|
defined.paddingBottom = 1;
|
||||||
}
|
}
|
||||||
|
if (base.defined.marginLeft) {
|
||||||
|
marginLeft = base.marginLeft;
|
||||||
|
defined.marginLeft = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compatibility accessors for existing code that uses hasX pattern
|
// Compatibility accessors for existing code that uses hasX pattern
|
||||||
@ -117,6 +124,7 @@ struct CssStyle {
|
|||||||
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||||||
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||||
|
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||||||
|
|
||||||
// Merge another style (alias for applyOver for compatibility)
|
// Merge another style (alias for applyOver for compatibility)
|
||||||
void merge(const CssStyle& other) { applyOver(other); }
|
void merge(const CssStyle& other) { applyOver(other); }
|
||||||
@ -128,6 +136,7 @@ struct CssStyle {
|
|||||||
decoration = CssTextDecoration::None;
|
decoration = CssTextDecoration::None;
|
||||||
indentPixels = 0.0f;
|
indentPixels = 0.0f;
|
||||||
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
marginTop = marginBottom = paddingTop = paddingBottom = 0;
|
||||||
|
marginLeft = 0.0f;
|
||||||
defined.clearAll();
|
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"};
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
||||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
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"};
|
const char* BOLD_TAGS[] = {"b", "strong"};
|
||||||
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
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.paddingTop = cssStyle.paddingTop;
|
||||||
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
blockStyle.paddingBottom = cssStyle.paddingBottom;
|
||||||
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
blockStyle.textIndent = static_cast<int16_t>(cssStyle.indentPixels);
|
||||||
|
blockStyle.marginLeft = static_cast<int16_t>(cssStyle.marginLeft);
|
||||||
return blockStyle;
|
return blockStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -320,6 +324,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
|
|
||||||
// Determine if this is a block element
|
// Determine if this is a block element
|
||||||
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
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
|
// Compute CSS style for this element
|
||||||
CssStyle cssStyle;
|
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
|
// This fixes issue where <br/> incorrectly wrapped the preceding word to a new line
|
||||||
self->flushPartWordBuffer();
|
self->flushPartWordBuffer();
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
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 {
|
} else {
|
||||||
// Determine alignment from CSS or default
|
// Determine alignment from CSS or default
|
||||||
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
||||||
@ -387,13 +417,72 @@ 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->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();
|
self->updateEffectiveInlineStyle();
|
||||||
|
|
||||||
if (strcmp(name, "li") == 0) {
|
// 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);
|
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)) {
|
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
|
||||||
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth);
|
||||||
@ -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) ||
|
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, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||||
matches(name, ITALIC_TAGS, NUM_ITALIC_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) {
|
if (shouldFlush) {
|
||||||
// Use combined depth-based and CSS-based style
|
// 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;
|
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
|
// Leaving bold tag
|
||||||
if (self->boldUntilDepth == self->depth) {
|
if (self->boldUntilDepth == self->depth) {
|
||||||
self->boldUntilDepth = INT_MAX;
|
self->boldUntilDepth = INT_MAX;
|
||||||
|
|||||||
@ -59,6 +59,22 @@ class ChapterHtmlSlimParser {
|
|||||||
bool effectiveItalic = false;
|
bool effectiveItalic = false;
|
||||||
bool effectiveUnderline = 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
|
// Byte offset tracking for position restoration after re-indexing
|
||||||
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
XML_Parser xmlParser = nullptr; // Store parser for getting current byte index
|
||||||
size_t currentPageStartOffset = 0; // Byte offset when current page was started
|
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) {
|
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)));
|
const auto inflator = static_cast<tinfl_decompressor*>(malloc(sizeof(tinfl_decompressor)));
|
||||||
if (!inflator) {
|
if (!inflator) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for inflator\n", millis());
|
||||||
|
free(outputBuffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
@ -541,29 +555,18 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch
|
|||||||
memset(inflator, 0, sizeof(tinfl_decompressor));
|
memset(inflator, 0, sizeof(tinfl_decompressor));
|
||||||
tinfl_init(inflator);
|
tinfl_init(inflator);
|
||||||
|
|
||||||
// Setup file read buffer
|
// Setup file read buffer (smallest allocation last)
|
||||||
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
const auto fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||||
if (!fileReadBuffer) {
|
if (!fileReadBuffer) {
|
||||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
Serial.printf("[%lu] [ZIP] Failed to allocate memory for zip file read buffer\n", millis());
|
||||||
free(inflator);
|
free(inflator);
|
||||||
|
free(outputBuffer);
|
||||||
if (!wasOpen) {
|
if (!wasOpen) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
return false;
|
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 fileRemainingBytes = deflatedDataSize;
|
||||||
size_t processedOutputBytes = 0;
|
size_t processedOutputBytes = 0;
|
||||||
size_t fileReadBufferFilledBytes = 0;
|
size_t fileReadBufferFilledBytes = 0;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ default_envs = default
|
|||||||
|
|
||||||
[crosspoint]
|
[crosspoint]
|
||||||
# 0.15.0 CrossPoint base, ef-1.0.0 is the first release of the ef branch
|
# 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]
|
[base]
|
||||||
platform = espressif32 @ 6.12.0
|
platform = espressif32 @ 6.12.0
|
||||||
|
|||||||
@ -52,6 +52,13 @@ void RecentBooksStore::clearAll() {
|
|||||||
Serial.printf("[%lu] [RBS] Cleared all recent books\n", millis());
|
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 {
|
bool RecentBooksStore::saveToFile() const {
|
||||||
// Make sure the directory exists
|
// Make sure the directory exists
|
||||||
SdMan.mkdir("/.crosspoint");
|
SdMan.mkdir("/.crosspoint");
|
||||||
|
|||||||
@ -29,9 +29,14 @@ class RecentBooksStore {
|
|||||||
// Returns true if the book was found and removed
|
// Returns true if the book was found and removed
|
||||||
bool removeBook(const std::string& path);
|
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();
|
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)
|
// Get the list of recent books (most recent first)
|
||||||
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
||||||
|
|
||||||
|
|||||||
@ -236,6 +236,15 @@ void EpubReaderActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -988,7 +997,7 @@ void EpubReaderActivity::renderEndOfBookPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Button hints
|
// 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.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -171,6 +171,14 @@ void TxtReaderActivity::loop() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
|
// Start over from beginning
|
||||||
|
currentPage = 0;
|
||||||
|
showingEndOfBookPrompt = false;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -909,7 +917,7 @@ void TxtReaderActivity::renderEndOfBookPrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Button hints
|
// 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.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
27
src/main.cpp
27
src/main.cpp
@ -381,6 +381,15 @@ void onGoToListsOrPinned() {
|
|||||||
|
|
||||||
void onGoToFileTransfer() {
|
void onGoToFileTransfer() {
|
||||||
exitActivity();
|
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));
|
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -396,12 +405,24 @@ void onGoToClearCache() {
|
|||||||
|
|
||||||
void onGoToMyLibrary() {
|
void onGoToMyLibrary() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|
||||||
|
// Reload recent books if they were cleared (e.g., when exiting File Transfer mode)
|
||||||
|
if (RECENT_BOOKS.getCount() == 0) {
|
||||||
|
RECENT_BOOKS.loadFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
enterNewActivity(
|
enterNewActivity(
|
||||||
new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
|
new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView, onGoToBookmarkList));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
||||||
exitActivity();
|
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,
|
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, onGoToListView,
|
||||||
onGoToBookmarkList, tab, path));
|
onGoToBookmarkList, tab, path));
|
||||||
}
|
}
|
||||||
@ -413,6 +434,12 @@ void onGoToBrowser() {
|
|||||||
|
|
||||||
void onGoHome() {
|
void onGoHome() {
|
||||||
exitActivity();
|
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,
|
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToListsOrPinned,
|
||||||
onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser));
|
onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -441,17 +441,33 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
showHidden = server->arg("showHidden") == "true";
|
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->setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||||
server->send(200, "application/json", "");
|
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];
|
char output[512];
|
||||||
constexpr size_t outputSize = sizeof(output);
|
constexpr size_t outputSize = sizeof(output);
|
||||||
bool seenFirst = false;
|
bool seenFirst = false;
|
||||||
|
bool clientDisconnected = false;
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
|
|
||||||
scanFiles(
|
scanFiles(
|
||||||
currentPath.c_str(),
|
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.clear();
|
||||||
doc["name"] = info.name;
|
doc["name"] = info.name;
|
||||||
doc["size"] = info.size;
|
doc["size"] = info.size;
|
||||||
@ -475,18 +491,33 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send comma separator before all entries except the first
|
||||||
if (seenFirst) {
|
if (seenFirst) {
|
||||||
server->sendContent(",");
|
if (!sendContentSafe(",")) {
|
||||||
|
clientDisconnected = true;
|
||||||
|
Serial.printf("[%lu] [WEB] Client disconnected during file list\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
seenFirst = true;
|
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);
|
showHidden);
|
||||||
server->sendContent("]");
|
|
||||||
|
// Only send closing bracket if client is still connected
|
||||||
|
if (!clientDisconnected) {
|
||||||
|
sendContentSafe("]");
|
||||||
// End of streamed response, empty chunk to signal client
|
// End of streamed response, empty chunk to signal client
|
||||||
server->sendContent("");
|
server->sendContent("");
|
||||||
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static variables for upload handling
|
// 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 {
|
bool CrossPointWebServer::copyFile(const String& srcPath, const String& destPath) const {
|
||||||
FsFile srcFile;
|
FsFile srcFile;
|
||||||
FsFile destFile;
|
FsFile destFile;
|
||||||
|
|||||||
@ -108,6 +108,11 @@ class CrossPointWebServer {
|
|||||||
bool copyFile(const String& srcPath, const String& destPath) const;
|
bool copyFile(const String& srcPath, const String& destPath) const;
|
||||||
bool copyFolder(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
|
// List management handlers
|
||||||
void handleListGet() const;
|
void handleListGet() const;
|
||||||
void handleListPost() const;
|
void handleListPost() const;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user