mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Compare commits
8 Commits
cfe838e03b
...
0d32d21d75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d32d21d75 | ||
|
|
9b4dfbd180 | ||
|
|
926c786705 | ||
|
|
299623927e | ||
|
|
9a3bb81337 | ||
|
|
73d1839ddd | ||
|
|
cc86533e86 | ||
|
|
bf3f270067 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
sudo apt-get install -y clang-format-21
|
||||
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect medium --fail-on-defect high
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
- name: Run clang-format
|
||||
run: PATH="/usr/lib/llvm-21/bin:$PATH" ./bin/clang-format-fix && git diff --exit-code || (echo "Please run 'bin/clang-format-fix' to fix formatting issues" && exit 1)
|
||||
|
||||
@ -30,24 +30,22 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
// Stream read (reusing your existing stream logic)
|
||||
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read META-INF/container.xml\n", millis());
|
||||
containerParser.teardown();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the result
|
||||
if (containerParser.fullPath.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Could not find valid rootfile in container.xml\n", millis());
|
||||
containerParser.teardown();
|
||||
return false;
|
||||
}
|
||||
|
||||
*contentOpfFile = std::move(containerParser.fullPath);
|
||||
|
||||
containerParser.teardown();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
||||
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
||||
|
||||
size_t contentOpfSize;
|
||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
||||
@ -63,7 +61,6 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
||||
|
||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
||||
opfParser.teardown();
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -84,8 +81,6 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
|
||||
opfParser.teardown();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -96,6 +91,8 @@ bool Epub::parseTocNcxFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsing toc ncx file: %s\n", millis(), tocNcxItem.c_str());
|
||||
|
||||
size_t tocSize;
|
||||
if (!getItemSize(tocNcxItem, &tocSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not get size of toc ncx\n", millis());
|
||||
@ -111,22 +108,18 @@ bool Epub::parseTocNcxFile() {
|
||||
|
||||
if (!readItemContentsToStream(tocNcxItem, ncxParser, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read toc ncx stream\n", millis());
|
||||
ncxParser.teardown();
|
||||
return false;
|
||||
}
|
||||
|
||||
this->toc = std::move(ncxParser.toc);
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
|
||||
|
||||
ncxParser.teardown();
|
||||
return true;
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load() {
|
||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||
ZipFile zip("/sd" + filepath);
|
||||
|
||||
std::string contentOpfFilePath;
|
||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||
@ -155,44 +148,20 @@ bool Epub::load() {
|
||||
}
|
||||
|
||||
void Epub::initializeSpineItemSizes() {
|
||||
setupCacheDir();
|
||||
Serial.printf("[%lu] [EBP] Calculating book size\n", millis());
|
||||
|
||||
size_t spineItemsCount = getSpineItemsCount();
|
||||
const size_t spineItemsCount = getSpineItemsCount();
|
||||
size_t cumSpineItemSize = 0;
|
||||
if (SD.exists((getCachePath() + "/spine_size.bin").c_str())) {
|
||||
File f = SD.open((getCachePath() + "/spine_size.bin").c_str());
|
||||
uint8_t data[4];
|
||||
for (size_t i = 0; i < spineItemsCount; i++) {
|
||||
f.read(data, 4);
|
||||
cumSpineItemSize = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
|
||||
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
|
||||
// Serial.printf("[%lu] [EBP] Loading item %d size %u to %u %u\n", millis(),
|
||||
// i, cumSpineItemSize, data[1], data[0]);
|
||||
}
|
||||
f.close();
|
||||
} else {
|
||||
File f = SD.open((getCachePath() + "/spine_size.bin").c_str(), FILE_WRITE);
|
||||
uint8_t data[4];
|
||||
// determine size of spine items
|
||||
for (size_t i = 0; i < spineItemsCount; i++) {
|
||||
std::string spineItem = getSpineItem(i);
|
||||
size_t s = 0;
|
||||
getItemSize(spineItem, &s);
|
||||
cumSpineItemSize += s;
|
||||
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
|
||||
const ZipFile zip("/sd" + filepath);
|
||||
|
||||
// and persist to cache
|
||||
data[0] = cumSpineItemSize & 0xFF;
|
||||
data[1] = (cumSpineItemSize >> 8) & 0xFF;
|
||||
data[2] = (cumSpineItemSize >> 16) & 0xFF;
|
||||
data[3] = (cumSpineItemSize >> 24) & 0xFF;
|
||||
// Serial.printf("[%lu] [EBP] Persisting item %d size %u to %u %u\n", millis(),
|
||||
// i, cumSpineItemSize, data[1], data[0]);
|
||||
f.write(data, 4);
|
||||
}
|
||||
|
||||
f.close();
|
||||
for (size_t i = 0; i < spineItemsCount; i++) {
|
||||
std::string spineItem = getSpineItem(i);
|
||||
size_t s = 0;
|
||||
getItemSize(zip, spineItem, &s);
|
||||
cumSpineItemSize += s;
|
||||
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize);
|
||||
}
|
||||
|
||||
@ -291,17 +260,31 @@ bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, con
|
||||
|
||||
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
|
||||
const ZipFile zip("/sd" + filepath);
|
||||
const std::string path = normalisePath(itemHref);
|
||||
return getItemSize(zip, itemHref, size);
|
||||
}
|
||||
|
||||
bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size) {
|
||||
const std::string path = normalisePath(itemHref);
|
||||
return zip.getInflatedFileSize(path.c_str(), size);
|
||||
}
|
||||
|
||||
int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); }
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
|
||||
if (spineIndex < 0 || spineIndex >= static_cast<int>(cumulativeSpineItemSize.size())) {
|
||||
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex);
|
||||
return 0;
|
||||
}
|
||||
return cumulativeSpineItemSize.at(spineIndex);
|
||||
}
|
||||
|
||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||
static std::string emptyString;
|
||||
if (spine.empty()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem called but spine is empty\n", millis());
|
||||
return emptyString;
|
||||
}
|
||||
if (spineIndex < 0 || spineIndex >= static_cast<int>(spine.size())) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||
return spine.at(0).second;
|
||||
}
|
||||
@ -310,7 +293,12 @@ std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
}
|
||||
|
||||
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
||||
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
||||
static EpubTocEntry emptyEntry("", "", "", 0);
|
||||
if (toc.empty()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis());
|
||||
return emptyEntry;
|
||||
}
|
||||
if (tocTndex < 0 || tocTndex >= static_cast<int>(toc.size())) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
|
||||
return toc.at(0);
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ class Epub {
|
||||
bool parseContentOpf(const std::string& contentOpfFilePath);
|
||||
bool parseTocNcxFile();
|
||||
void initializeSpineItemSizes();
|
||||
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
|
||||
@ -19,14 +19,25 @@ void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
|
||||
|
||||
// Consumes data to minimize memory usage
|
||||
void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine) {
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
const bool includeLastLine) {
|
||||
if (words.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t totalWordCount = words.size();
|
||||
const int pageWidth = renderer.getScreenWidth() - horizontalMargin;
|
||||
const int spaceWidth = renderer.getSpaceWidth(fontId);
|
||||
const auto wordWidths = calculateWordWidths(renderer, fontId);
|
||||
const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& renderer, const int fontId) {
|
||||
const size_t totalWordCount = words.size();
|
||||
|
||||
std::vector<uint16_t> wordWidths;
|
||||
wordWidths.reserve(totalWordCount);
|
||||
@ -47,6 +58,13 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
std::advance(wordStylesIt, 1);
|
||||
}
|
||||
|
||||
return wordWidths;
|
||||
}
|
||||
|
||||
std::vector<size_t> ParsedText::computeLineBreaks(const int pageWidth, const int spaceWidth,
|
||||
const std::vector<uint16_t>& wordWidths) const {
|
||||
const size_t totalWordCount = words.size();
|
||||
|
||||
// DP table to store the minimum badness (cost) of lines starting at index i
|
||||
std::vector<int> dp(totalWordCount);
|
||||
// 'ans[i]' stores the index 'j' of the *last word* in the optimal line starting at 'i'
|
||||
@ -106,66 +124,59 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo
|
||||
currentWordIndex = nextBreakIndex;
|
||||
}
|
||||
|
||||
// Initialize iterators for consumption
|
||||
auto wordStartIt = words.begin();
|
||||
auto wordStyleStartIt = wordStyles.begin();
|
||||
size_t wordWidthIndex = 0;
|
||||
|
||||
size_t lastBreakAt = 0;
|
||||
for (const size_t lineBreak : lineBreakIndices) {
|
||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||
|
||||
// Calculate end iterators for the range to splice
|
||||
auto wordEndIt = wordStartIt;
|
||||
auto wordStyleEndIt = wordStyleStartIt;
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
|
||||
// Calculate total word width for this line
|
||||
int lineWordWidthSum = 0;
|
||||
for (size_t i = 0; i < lineWordCount; ++i) {
|
||||
lineWordWidthSum += wordWidths[wordWidthIndex + i];
|
||||
}
|
||||
|
||||
// Calculate spacing
|
||||
int spareSpace = pageWidth - lineWordWidthSum;
|
||||
|
||||
int spacing = spaceWidth;
|
||||
const bool isLastLine = lineBreak == totalWordCount;
|
||||
|
||||
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
}
|
||||
|
||||
// Calculate initial x position
|
||||
uint16_t xpos = 0;
|
||||
if (style == TextBlock::RIGHT_ALIGN) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
std::list<uint16_t> lineXPos;
|
||||
for (size_t i = 0; i < lineWordCount; ++i) {
|
||||
const uint16_t currentWordWidth = wordWidths[wordWidthIndex + i];
|
||||
lineXPos.push_back(xpos);
|
||||
xpos += currentWordWidth + spacing;
|
||||
}
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
lineWords.splice(lineWords.begin(), words, wordStartIt, wordEndIt);
|
||||
std::list<EpdFontStyle> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyleStartIt, wordStyleEndIt);
|
||||
|
||||
processLine(
|
||||
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
||||
|
||||
// Update pointers/indices for the next line
|
||||
wordStartIt = wordEndIt;
|
||||
wordStyleStartIt = wordStyleEndIt;
|
||||
wordWidthIndex += lineWordCount;
|
||||
lastBreakAt = lineBreak;
|
||||
}
|
||||
return lineBreakIndices;
|
||||
}
|
||||
|
||||
void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth,
|
||||
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];
|
||||
const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0;
|
||||
const size_t lineWordCount = lineBreak - lastBreakAt;
|
||||
|
||||
// Calculate total word width for this line
|
||||
int lineWordWidthSum = 0;
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
lineWordWidthSum += wordWidths[i];
|
||||
}
|
||||
|
||||
// Calculate spacing
|
||||
const int spareSpace = pageWidth - lineWordWidthSum;
|
||||
|
||||
int spacing = spaceWidth;
|
||||
const bool isLastLine = lineBreak == words.size();
|
||||
|
||||
if (style == TextBlock::JUSTIFIED && !isLastLine && lineWordCount >= 2) {
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
}
|
||||
|
||||
// Calculate initial x position
|
||||
uint16_t xpos = 0;
|
||||
if (style == TextBlock::RIGHT_ALIGN) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
std::list<uint16_t> lineXPos;
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
const uint16_t currentWordWidth = wordWidths[i];
|
||||
lineXPos.push_back(xpos);
|
||||
xpos += currentWordWidth + spacing;
|
||||
}
|
||||
|
||||
// Iterators always start at the beginning as we are moving content with splice below
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||
std::list<EpdFontStyle> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
@ -18,6 +18,12 @@ class ParsedText {
|
||||
TextBlock::BLOCK_STYLE style;
|
||||
bool extraParagraphSpacing;
|
||||
|
||||
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
|
||||
void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, 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);
|
||||
|
||||
public:
|
||||
explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing)
|
||||
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
|
||||
@ -26,7 +32,9 @@ class ParsedText {
|
||||
void addWord(std::string word, EpdFontStyle fontStyle);
|
||||
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
|
||||
TextBlock::BLOCK_STYLE getStyle() const { return style; }
|
||||
size_t size() const { return words.size(); }
|
||||
bool isEmpty() const { return words.empty(); }
|
||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine);
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
bool includeLastLine = true);
|
||||
};
|
||||
|
||||
@ -21,9 +21,10 @@ class Section {
|
||||
int currentPage = 0;
|
||||
|
||||
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
|
||||
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
||||
}
|
||||
: epub(epub),
|
||||
spineIndex(spineIndex),
|
||||
renderer(renderer),
|
||||
cachePath(epub->getCachePath() + "/" + std::to_string(spineIndex)) {}
|
||||
~Section() = default;
|
||||
bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom,
|
||||
int marginLeft, bool extraParagraphSpacing);
|
||||
|
||||
@ -143,6 +143,17 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
||||
|
||||
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
||||
}
|
||||
|
||||
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
||||
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
||||
// memory.
|
||||
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
||||
if (self->currentTextBlock->size() > 750) {
|
||||
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
|
||||
self->currentTextBlock->layoutAndExtractLines(
|
||||
self->renderer, self->fontId, self->marginLeft + self->marginRight,
|
||||
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||
|
||||
@ -14,12 +14,11 @@ bool ContainerParser::setup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContainerParser::teardown() {
|
||||
ContainerParser::~ContainerParser() {
|
||||
if (parser) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
@ -23,9 +23,9 @@ class ContainerParser final : public Print {
|
||||
std::string fullPath;
|
||||
|
||||
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
|
||||
~ContainerParser() override;
|
||||
|
||||
bool setup();
|
||||
bool teardown();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
#include <ZipFile.h>
|
||||
|
||||
namespace {
|
||||
constexpr const char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||
}
|
||||
|
||||
bool ContentOpfParser::setup() {
|
||||
@ -20,12 +20,11 @@ bool ContentOpfParser::setup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ContentOpfParser::teardown() {
|
||||
ContentOpfParser::~ContentOpfParser() {
|
||||
if (parser) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
@ -34,9 +34,9 @@ class ContentOpfParser final : public Print {
|
||||
|
||||
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
||||
~ContentOpfParser() override;
|
||||
|
||||
bool setup();
|
||||
bool teardown();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#include "TocNcxParser.h"
|
||||
|
||||
#include <Esp.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
bool TocNcxParser::setup() {
|
||||
@ -15,12 +16,11 @@ bool TocNcxParser::setup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TocNcxParser::teardown() {
|
||||
TocNcxParser::~TocNcxParser() {
|
||||
if (parser) {
|
||||
XML_ParserFree(parser);
|
||||
parser = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
@ -28,9 +28,9 @@ class TocNcxParser final : public Print {
|
||||
|
||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
||||
~TocNcxParser() override;
|
||||
|
||||
bool setup();
|
||||
bool teardown();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
|
||||
@ -136,6 +136,13 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
||||
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
||||
|
||||
if (!outputRow || !rowBytes) {
|
||||
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
||||
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
||||
// Screen's (0, 0) is the top-left corner.
|
||||
@ -183,6 +190,10 @@ void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScre
|
||||
|
||||
void GfxRenderer::invertScreen() const {
|
||||
uint8_t* buffer = einkDisplay.getFrameBuffer();
|
||||
if (!buffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
||||
buffer[i] = ~buffer[i];
|
||||
}
|
||||
@ -256,6 +267,10 @@ void GfxRenderer::freeBwBufferChunks() {
|
||||
*/
|
||||
void GfxRenderer::storeBwBuffer() {
|
||||
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate and copy each chunk
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
@ -306,6 +321,12 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
}
|
||||
|
||||
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
||||
if (!frameBuffer) {
|
||||
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
|
||||
freeBwBufferChunks();
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
// Check if chunk is missing
|
||||
if (!bwBufferChunks[i]) {
|
||||
|
||||
@ -27,31 +27,28 @@ bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t*
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const {
|
||||
mz_zip_archive zipArchive = {};
|
||||
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
|
||||
ZipFile::ZipFile(std::string filePath) : filePath(std::move(filePath)) {
|
||||
const bool status = mz_zip_reader_init_file(&zipArchive, this->filePath.c_str(), 0);
|
||||
|
||||
if (!status) {
|
||||
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed! Error: %s\n", millis(),
|
||||
Serial.printf("[%lu] [ZIP] mz_zip_reader_init_file() failed for %s! Error: %s\n", millis(), this->filePath.c_str(),
|
||||
mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ZipFile::loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const {
|
||||
// find the file
|
||||
mz_uint32 fileIndex = 0;
|
||||
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
|
||||
Serial.printf("[%lu] [ZIP] Could not find file %s\n", millis(), filename);
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, fileStat)) {
|
||||
Serial.printf("[%lu] [ZIP] mz_zip_reader_file_stat() failed! Error: %s\n", millis(),
|
||||
mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return false;
|
||||
}
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -118,6 +115,11 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo
|
||||
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
|
||||
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
||||
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
||||
if (data == nullptr) {
|
||||
Serial.printf("[%lu] [ZIP] Failed to allocate memory for output buffer (%zu bytes)\n", millis(), dataSize);
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (fileStat.m_method == MZ_NO_COMPRESSION) {
|
||||
// no deflation, just read content
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "miniz.h"
|
||||
|
||||
class ZipFile {
|
||||
std::string filePath;
|
||||
mutable mz_zip_archive zipArchive = {};
|
||||
bool loadFileStat(const char* filename, mz_zip_archive_file_stat* fileStat) const;
|
||||
long getDataOffset(const mz_zip_archive_file_stat& fileStat) const;
|
||||
|
||||
public:
|
||||
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
|
||||
~ZipFile() = default;
|
||||
explicit ZipFile(std::string filePath);
|
||||
~ZipFile() { mz_zip_reader_end(&zipArchive); }
|
||||
bool getInflatedFileSize(const char* filename, size_t* size) const;
|
||||
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
|
||||
bool readFileToStream(const char* filename, Print& out, size_t chunkSize) const;
|
||||
|
||||
@ -9,8 +9,8 @@ framework = arduino
|
||||
monitor_speed = 115200
|
||||
upload_speed = 921600
|
||||
check_tool = cppcheck
|
||||
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --inline-suppr
|
||||
check_skip_packages = yes
|
||||
check_severity = medium, high
|
||||
|
||||
board_upload.flash_size = 16MB
|
||||
board_upload.maximum_size = 16777216
|
||||
|
||||
@ -111,12 +111,12 @@ bool WifiCredentialStore::loadFromFile() {
|
||||
|
||||
bool WifiCredentialStore::addCredential(const std::string& ssid, const std::string& password) {
|
||||
// Check if this SSID already exists and update it
|
||||
for (auto& cred : credentials) {
|
||||
if (cred.ssid == ssid) {
|
||||
cred.password = password;
|
||||
Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
const auto cred = find_if(credentials.begin(), credentials.end(),
|
||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||
if (cred != credentials.end()) {
|
||||
cred->password = password;
|
||||
Serial.printf("[%lu] [WCS] Updated credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
|
||||
// Check if we've reached the limit
|
||||
@ -132,22 +132,24 @@ bool WifiCredentialStore::addCredential(const std::string& ssid, const std::stri
|
||||
}
|
||||
|
||||
bool WifiCredentialStore::removeCredential(const std::string& ssid) {
|
||||
for (auto it = credentials.begin(); it != credentials.end(); ++it) {
|
||||
if (it->ssid == ssid) {
|
||||
credentials.erase(it);
|
||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
const auto cred = find_if(credentials.begin(), credentials.end(),
|
||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||
if (cred != credentials.end()) {
|
||||
credentials.erase(cred);
|
||||
Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str());
|
||||
return saveToFile();
|
||||
}
|
||||
return false; // Not found
|
||||
}
|
||||
|
||||
const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssid) const {
|
||||
for (const auto& cred : credentials) {
|
||||
if (cred.ssid == ssid) {
|
||||
return &cred;
|
||||
}
|
||||
const auto cred = find_if(credentials.begin(), credentials.end(),
|
||||
[&ssid](const WifiCredential& cred) { return cred.ssid == ssid; });
|
||||
|
||||
if (cred != credentials.end()) {
|
||||
return &*cred;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
#include "SleepActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "SD.h"
|
||||
#include "config.h"
|
||||
#include "images/CrossLarge.h"
|
||||
|
||||
|
||||
@ -217,8 +217,7 @@ void CrossPointWebServerActivity::render() const {
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (pageHeight - height * 5) / 2;
|
||||
|
||||
@ -226,7 +225,7 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
|
||||
std::string ssidInfo = "Network: " + connectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo = ssidInfo.substr(0, 25) + "...";
|
||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
|
||||
|
||||
|
||||
@ -138,6 +138,7 @@ void WifiSelectionActivity::processWifiScanResults() {
|
||||
// Convert map to vector
|
||||
networks.clear();
|
||||
for (const auto& pair : uniqueNetworks) {
|
||||
// cppcheck-suppress useStlAlgorithm
|
||||
networks.push_back(pair.second);
|
||||
}
|
||||
|
||||
@ -334,11 +335,10 @@ void WifiSelectionActivity::loop() {
|
||||
// User chose "Yes" - forget the network
|
||||
WIFI_STORE.removeCredential(selectedSSID);
|
||||
// Update the network list to reflect the change
|
||||
for (auto& network : networks) {
|
||||
if (network.ssid == selectedSSID) {
|
||||
network.hasSavedPassword = false;
|
||||
break;
|
||||
}
|
||||
const auto network = find_if(networks.begin(), networks.end(),
|
||||
[this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; });
|
||||
if (network != networks.end()) {
|
||||
network->hasSavedPassword = false;
|
||||
}
|
||||
}
|
||||
// Go back to network list
|
||||
@ -468,8 +468,8 @@ void WifiSelectionActivity::render() const {
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderNetworkList() const {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Draw header
|
||||
renderer.drawCenteredText(READER_FONT_ID, 10, "WiFi Networks", true, BOLD);
|
||||
@ -506,7 +506,7 @@ void WifiSelectionActivity::renderNetworkList() const {
|
||||
// Draw network name (truncate if too long)
|
||||
std::string displayName = network.ssid;
|
||||
if (displayName.length() > 16) {
|
||||
displayName = displayName.substr(0, 13) + "...";
|
||||
displayName.replace(13, displayName.length() - 13, "...");
|
||||
}
|
||||
renderer.drawText(UI_FONT_ID, 20, networkY, displayName.c_str());
|
||||
|
||||
@ -544,15 +544,13 @@ void WifiSelectionActivity::renderNetworkList() const {
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderPasswordEntry() const {
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
|
||||
// Draw header
|
||||
renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD);
|
||||
|
||||
// Draw network name with good spacing from header
|
||||
std::string networkInfo = "Network: " + selectedSSID;
|
||||
if (networkInfo.length() > 30) {
|
||||
networkInfo = networkInfo.substr(0, 27) + "...";
|
||||
networkInfo.replace(27, networkInfo.length() - 27, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR);
|
||||
|
||||
@ -563,7 +561,7 @@ void WifiSelectionActivity::renderPasswordEntry() const {
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderConnecting() const {
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (pageHeight - height) / 2;
|
||||
|
||||
@ -574,15 +572,14 @@ void WifiSelectionActivity::renderConnecting() const {
|
||||
|
||||
std::string ssidInfo = "to " + selectedSSID;
|
||||
if (ssidInfo.length() > 25) {
|
||||
ssidInfo = ssidInfo.substr(0, 22) + "...";
|
||||
ssidInfo.replace(22, ssidInfo.length() - 22, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
|
||||
}
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderConnected() const {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (pageHeight - height * 4) / 2;
|
||||
|
||||
@ -590,7 +587,7 @@ void WifiSelectionActivity::renderConnected() const {
|
||||
|
||||
std::string ssidInfo = "Network: " + selectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo = ssidInfo.substr(0, 25) + "...";
|
||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, top + 10, ssidInfo.c_str(), true, REGULAR);
|
||||
|
||||
@ -601,8 +598,8 @@ void WifiSelectionActivity::renderConnected() const {
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderSavePrompt() const {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (pageHeight - height * 3) / 2;
|
||||
|
||||
@ -610,7 +607,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
|
||||
|
||||
std::string ssidInfo = "Network: " + selectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo = ssidInfo.substr(0, 25) + "...";
|
||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
|
||||
|
||||
@ -641,7 +638,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderConnectionFailed() const {
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (pageHeight - height * 2) / 2;
|
||||
|
||||
@ -651,8 +648,8 @@ void WifiSelectionActivity::renderConnectionFailed() const {
|
||||
}
|
||||
|
||||
void WifiSelectionActivity::renderForgetPrompt() const {
|
||||
const auto pageWidth = GfxRenderer::getScreenWidth();
|
||||
const auto pageHeight = GfxRenderer::getScreenHeight();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const auto height = renderer.getLineHeight(UI_FONT_ID);
|
||||
const auto top = (pageHeight - height * 3) / 2;
|
||||
|
||||
@ -660,7 +657,7 @@ void WifiSelectionActivity::renderForgetPrompt() const {
|
||||
|
||||
std::string ssidInfo = "Network: " + selectedSSID;
|
||||
if (ssidInfo.length() > 28) {
|
||||
ssidInfo = ssidInfo.substr(0, 25) + "...";
|
||||
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
|
||||
}
|
||||
renderer.drawCenteredText(UI_FONT_ID, top, ssidInfo.c_str(), true, REGULAR);
|
||||
|
||||
|
||||
@ -383,9 +383,7 @@ void CrossPointWebServer::handleFileList() {
|
||||
// Folders come first
|
||||
if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory;
|
||||
// Then sort by epub status (epubs first among files)
|
||||
if (!a.isDirectory && !b.isDirectory) {
|
||||
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
|
||||
}
|
||||
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
|
||||
// Then alphabetically
|
||||
return a.name < b.name;
|
||||
});
|
||||
@ -513,13 +511,6 @@ void CrossPointWebServer::handleUpload() {
|
||||
Serial.printf("[%lu] [WEB] [UPLOAD] START: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str());
|
||||
Serial.printf("[%lu] [WEB] [UPLOAD] Free heap: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||
|
||||
// Validate file extension
|
||||
if (!isEpubFile(uploadFileName)) {
|
||||
uploadError = "Only .epub files are allowed";
|
||||
Serial.printf("[%lu] [WEB] [UPLOAD] REJECTED - not an epub file\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
// Create file path
|
||||
String filePath = uploadPath;
|
||||
if (!filePath.endsWith("/")) filePath += "/";
|
||||
|
||||
@ -383,7 +383,7 @@ void EpubReaderActivity::renderStatusBar() const {
|
||||
title = tocItem.title;
|
||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
while (titleWidth > availableTextWidth && title.length() > 11) {
|
||||
title = title.substr(0, title.length() - 8) + "...";
|
||||
title.replace(title.length() - 8, 8, "...");
|
||||
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ void FileSelectionActivity::loop() {
|
||||
}
|
||||
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||
if (basepath != "/") {
|
||||
basepath = basepath.substr(0, basepath.rfind('/'));
|
||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||
if (basepath.empty()) basepath = "/";
|
||||
loadFiles();
|
||||
updateRequired = true;
|
||||
|
||||
@ -20,7 +20,7 @@ KeyboardEntryActivity::KeyboardEntryActivity(GfxRenderer& renderer, InputManager
|
||||
void KeyboardEntryActivity::setText(const std::string& newText) {
|
||||
text = newText;
|
||||
if (maxLength > 0 && text.length() > maxLength) {
|
||||
text = text.substr(0, maxLength);
|
||||
text.resize(maxLength);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,15 +3,15 @@
|
||||
CrossPoint E-Reader • Open Source
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div class="modal-overlay" id="uploadModal">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeUploadModal()">×</button>
|
||||
<h3>📤 Upload eBook</h3>
|
||||
<h3>📤 Upload file</h3>
|
||||
<div class="upload-form">
|
||||
<p class="file-info">Select an .epub file to upload to <strong id="uploadPathDisplay"></strong></p>
|
||||
<input type="file" id="fileInput" accept=".epub" onchange="validateFile()">
|
||||
<p class="file-info">Select a file to upload to <strong id="uploadPathDisplay"></strong></p>
|
||||
<input type="file" id="fileInput" onchange="validateFile()">
|
||||
<button id="uploadBtn" class="upload-btn" onclick="uploadFile()" disabled>Upload</button>
|
||||
<div id="progress-container">
|
||||
<div id="progress-bar"><div id="progress-fill"></div></div>
|
||||
@ -20,7 +20,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- New Folder Modal -->
|
||||
<div class="modal-overlay" id="folderModal">
|
||||
<div class="modal">
|
||||
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal-overlay" id="deleteModal">
|
||||
<div class="modal">
|
||||
@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// Modal functions
|
||||
function openUploadModal() {
|
||||
@ -58,7 +58,7 @@
|
||||
document.getElementById('uploadPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath;
|
||||
document.getElementById('uploadModal').classList.add('open');
|
||||
}
|
||||
|
||||
|
||||
function closeUploadModal() {
|
||||
document.getElementById('uploadModal').classList.remove('open');
|
||||
document.getElementById('fileInput').value = '';
|
||||
@ -67,18 +67,18 @@
|
||||
document.getElementById('progress-fill').style.width = '0%';
|
||||
document.getElementById('progress-fill').style.backgroundColor = '#27ae60';
|
||||
}
|
||||
|
||||
|
||||
function openFolderModal() {
|
||||
const currentPath = document.getElementById('currentPath').value;
|
||||
document.getElementById('folderPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath;
|
||||
document.getElementById('folderModal').classList.add('open');
|
||||
document.getElementById('folderName').value = '';
|
||||
}
|
||||
|
||||
|
||||
function closeFolderModal() {
|
||||
document.getElementById('folderModal').classList.remove('open');
|
||||
}
|
||||
|
||||
|
||||
// Close modals when clicking overlay
|
||||
document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
@ -87,58 +87,40 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function validateFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (file) {
|
||||
const fileName = file.name.toLowerCase();
|
||||
if (!fileName.endsWith('.epub')) {
|
||||
alert('Only .epub files are allowed!');
|
||||
fileInput.value = '';
|
||||
uploadBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
uploadBtn.disabled = false;
|
||||
} else {
|
||||
uploadBtn.disabled = true;
|
||||
}
|
||||
uploadBtn.disabled = !file;
|
||||
}
|
||||
|
||||
|
||||
function uploadFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const file = fileInput.files[0];
|
||||
const currentPath = document.getElementById('currentPath').value;
|
||||
|
||||
|
||||
if (!file) {
|
||||
alert('Please select a file first!');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = file.name.toLowerCase();
|
||||
if (!fileName.endsWith('.epub')) {
|
||||
alert('Only .epub files are allowed!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
|
||||
const progressContainer = document.getElementById('progress-container');
|
||||
const progressFill = document.getElementById('progress-fill');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
|
||||
|
||||
progressContainer.style.display = 'block';
|
||||
uploadBtn.disabled = true;
|
||||
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
// Include path as query parameter since multipart form data doesn't make
|
||||
// form fields available until after file upload completes
|
||||
xhr.open('POST', '/upload?path=' + encodeURIComponent(currentPath), true);
|
||||
|
||||
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
@ -146,7 +128,7 @@
|
||||
progressText.textContent = 'Uploading: ' + percent + '%';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
progressText.textContent = 'Upload complete!';
|
||||
@ -159,39 +141,39 @@
|
||||
uploadBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
xhr.onerror = function() {
|
||||
progressText.textContent = 'Upload failed - network error';
|
||||
progressFill.style.backgroundColor = '#e74c3c';
|
||||
uploadBtn.disabled = false;
|
||||
};
|
||||
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
|
||||
function createFolder() {
|
||||
const folderName = document.getElementById('folderName').value.trim();
|
||||
const currentPath = document.getElementById('currentPath').value;
|
||||
|
||||
|
||||
if (!folderName) {
|
||||
alert('Please enter a folder name!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Validate folder name (no special characters except underscore and hyphen)
|
||||
const validName = /^[a-zA-Z0-9_\-]+$/.test(folderName);
|
||||
if (!validName) {
|
||||
alert('Folder name can only contain letters, numbers, underscores, and hyphens.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', folderName);
|
||||
formData.append('path', currentPath);
|
||||
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/mkdir', true);
|
||||
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
@ -199,14 +181,14 @@
|
||||
alert('Failed to create folder: ' + xhr.responseText);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert('Failed to create folder - network error');
|
||||
};
|
||||
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
|
||||
// Delete functions
|
||||
function openDeleteModal(name, path, isFolder) {
|
||||
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;
|
||||
@ -214,22 +196,22 @@
|
||||
document.getElementById('deleteItemType').value = isFolder ? 'folder' : 'file';
|
||||
document.getElementById('deleteModal').classList.add('open');
|
||||
}
|
||||
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').classList.remove('open');
|
||||
}
|
||||
|
||||
|
||||
function confirmDelete() {
|
||||
const path = document.getElementById('deleteItemPath').value;
|
||||
const itemType = document.getElementById('deleteItemType').value;
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('path', path);
|
||||
formData.append('type', itemType);
|
||||
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/delete', true);
|
||||
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
@ -238,12 +220,12 @@
|
||||
closeDeleteModal();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert('Failed to delete - network error');
|
||||
closeDeleteModal();
|
||||
};
|
||||
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
</script>
|
||||
|
||||
19
src/main.cpp
19
src/main.cpp
@ -5,7 +5,6 @@
|
||||
#include <InputManager.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
#include <WiFi.h>
|
||||
#include <builtinFonts/bookerly_2b.h>
|
||||
#include <builtinFonts/bookerly_bold_2b.h>
|
||||
#include <builtinFonts/bookerly_bold_italic_2b.h>
|
||||
@ -203,20 +202,18 @@ void setup() {
|
||||
}
|
||||
|
||||
void loop() {
|
||||
static unsigned long lastLoopTime = 0;
|
||||
static unsigned long maxLoopDuration = 0;
|
||||
|
||||
unsigned long loopStartTime = millis();
|
||||
|
||||
const unsigned long loopStartTime = millis();
|
||||
static unsigned long lastMemPrint = 0;
|
||||
|
||||
inputManager.update();
|
||||
|
||||
if (Serial && millis() - lastMemPrint >= 10000) {
|
||||
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
||||
lastMemPrint = millis();
|
||||
}
|
||||
|
||||
inputManager.update();
|
||||
|
||||
// Check for any user activity (button press or release)
|
||||
static unsigned long lastActivityTime = millis();
|
||||
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased()) {
|
||||
@ -237,13 +234,13 @@ void loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned long activityStartTime = millis();
|
||||
const unsigned long activityStartTime = millis();
|
||||
if (currentActivity) {
|
||||
currentActivity->loop();
|
||||
}
|
||||
unsigned long activityDuration = millis() - activityStartTime;
|
||||
const unsigned long activityDuration = millis() - activityStartTime;
|
||||
|
||||
unsigned long loopDuration = millis() - loopStartTime;
|
||||
const unsigned long loopDuration = millis() - loopStartTime;
|
||||
if (loopDuration > maxLoopDuration) {
|
||||
maxLoopDuration = loopDuration;
|
||||
if (maxLoopDuration > 50) {
|
||||
@ -252,8 +249,6 @@ void loop() {
|
||||
}
|
||||
}
|
||||
|
||||
lastLoopTime = loopStartTime;
|
||||
|
||||
// Add delay at the end of the loop to prevent tight spinning
|
||||
// When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response
|
||||
// Otherwise, use longer delay to save power
|
||||
|
||||
Loading…
Reference in New Issue
Block a user