mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 15:17:37 +03:00
Merge branch 'daveallie:master' into feature/show-img-alt-text
This commit is contained in:
commit
61b2622206
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
|
|
||||||
inline int min(const int a, const int b) { return a < b ? a : b; }
|
#include <algorithm>
|
||||||
inline int max(const int a, const int b) { return a < b ? b : a; }
|
|
||||||
|
|
||||||
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
||||||
int* maxY) const {
|
int* maxY) const {
|
||||||
@ -32,10 +31,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
*minX = min(*minX, cursorX + glyph->left);
|
*minX = std::min(*minX, cursorX + glyph->left);
|
||||||
*maxX = max(*maxX, cursorX + glyph->left + glyph->width);
|
*maxX = std::max(*maxX, cursorX + glyph->left + glyph->width);
|
||||||
*minY = min(*minY, cursorY + glyph->top - glyph->height);
|
*minY = std::min(*minY, cursorY + glyph->top - glyph->height);
|
||||||
*maxY = max(*maxY, cursorY + glyph->top);
|
*maxY = std::max(*maxY, cursorY + glyph->top);
|
||||||
cursorX += glyph->advanceX;
|
cursorX += glyph->advanceX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,6 +74,7 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
|||||||
bookMetadata.title = opfParser.title;
|
bookMetadata.title = opfParser.title;
|
||||||
bookMetadata.author = opfParser.author;
|
bookMetadata.author = opfParser.author;
|
||||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
|
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||||
|
|
||||||
if (!opfParser.tocNcxPath.empty()) {
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
tocNcxItem = opfParser.tocNcxPath;
|
tocNcxItem = opfParser.tocNcxPath;
|
||||||
@ -108,17 +109,20 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
|
|
||||||
if (!ncxParser.setup()) {
|
if (!ncxParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||||
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||||
if (!ncxBuffer) {
|
if (!ncxBuffer) {
|
||||||
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not allocate memory for toc ncx parser\n", millis());
|
||||||
|
tempNcxFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (tempNcxFile.available()) {
|
while (tempNcxFile.available()) {
|
||||||
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
||||||
|
if (readSize == 0) break;
|
||||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||||
|
|
||||||
if (processedSize != readSize) {
|
if (processedSize != readSize) {
|
||||||
@ -138,7 +142,7 @@ bool Epub::parseTocNcxFile() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// load in the meta data for the epub file
|
// load in the meta data for the epub file
|
||||||
bool Epub::load() {
|
bool Epub::load(const bool buildIfMissing) {
|
||||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||||
|
|
||||||
// Initialize spine/TOC cache
|
// Initialize spine/TOC cache
|
||||||
@ -150,6 +154,11 @@ bool Epub::load() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we didn't load from cache above and we aren't allowed to build, fail now
|
||||||
|
if (!buildIfMissing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Cache doesn't exist or is invalid, build it
|
// Cache doesn't exist or is invalid, build it
|
||||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||||
setupCacheDir();
|
setupCacheDir();
|
||||||
@ -426,6 +435,35 @@ size_t Epub::getBookSize() const {
|
|||||||
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Epub::getSpineIndexForTextReference() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] getSpineIndexForTextReference called but cache not loaded\n", millis());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
Serial.printf("[%lu] [ERS] Core Metadata: cover(%d)=%s, textReference(%d)=%s\n", millis(),
|
||||||
|
bookMetadataCache->coreMetadata.coverItemHref.size(),
|
||||||
|
bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||||
|
|
||||||
|
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||||
|
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// loop through spine items to get the correct index matching the text href
|
||||||
|
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||||
|
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||||
|
Serial.printf("[%lu] [ERS] Text reference %s found at index %d\n", millis(),
|
||||||
|
bookMetadataCache->coreMetadata.textReferenceHref.c_str(), i);
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||||
|
Serial.printf("[%lu] [EBP] Section not found for text reference\n", millis());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate progress in book
|
// Calculate progress in book
|
||||||
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||||
const size_t bookSize = getBookSize();
|
const size_t bookSize = getBookSize();
|
||||||
|
|||||||
@ -34,7 +34,7 @@ class Epub {
|
|||||||
}
|
}
|
||||||
~Epub() = default;
|
~Epub() = default;
|
||||||
std::string& getBasePath() { return contentBasePath; }
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
bool load();
|
bool load(bool buildIfMissing = true);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
void setupCacheDir() const;
|
void setupCacheDir() const;
|
||||||
const std::string& getCachePath() const;
|
const std::string& getCachePath() const;
|
||||||
@ -54,6 +54,7 @@ class Epub {
|
|||||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||||
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||||
|
int getSpineIndexForTextReference() const;
|
||||||
|
|
||||||
size_t getBookSize() const;
|
size_t getBookSize() const;
|
||||||
uint8_t calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
uint8_t calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
#include "FsHelpers.h"
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t BOOK_CACHE_VERSION = 2;
|
constexpr uint8_t BOOK_CACHE_VERSION = 3;
|
||||||
constexpr char bookBinFile[] = "/book.bin";
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
@ -87,8 +87,8 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
|
|
||||||
constexpr uint32_t headerASize =
|
constexpr uint32_t headerASize =
|
||||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
const uint32_t metadataSize =
|
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() +
|
||||||
metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() + sizeof(uint32_t) * 3;
|
metadata.textReferenceHref.size() + sizeof(uint32_t) * 4;
|
||||||
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||||
const uint32_t lutOffset = headerASize + metadataSize;
|
const uint32_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
@ -101,6 +101,7 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
|||||||
serialization::writeString(bookFile, metadata.title);
|
serialization::writeString(bookFile, metadata.title);
|
||||||
serialization::writeString(bookFile, metadata.author);
|
serialization::writeString(bookFile, metadata.author);
|
||||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||||
|
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||||
|
|
||||||
// Loop through spine entries, writing LUT positions
|
// Loop through spine entries, writing LUT positions
|
||||||
spineFile.seek(0);
|
spineFile.seek(0);
|
||||||
@ -289,6 +290,7 @@ bool BookMetadataCache::load() {
|
|||||||
serialization::readString(bookFile, coreMetadata.title);
|
serialization::readString(bookFile, coreMetadata.title);
|
||||||
serialization::readString(bookFile, coreMetadata.author);
|
serialization::readString(bookFile, coreMetadata.author);
|
||||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||||
|
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class BookMetadataCache {
|
|||||||
std::string title;
|
std::string title;
|
||||||
std::string author;
|
std::string author;
|
||||||
std::string coverItemHref;
|
std::string coverItemHref;
|
||||||
|
std::string textReferenceHref;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SpineEntry {
|
struct SpineEntry {
|
||||||
|
|||||||
@ -57,7 +57,6 @@ void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style
|
|||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
(void)atts;
|
|
||||||
|
|
||||||
// Middle of skip
|
// Middle of skip
|
||||||
if (self->skipUntilDepth < self->depth) {
|
if (self->skipUntilDepth < self->depth) {
|
||||||
@ -111,7 +110,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
|
|
||||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||||
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
if (strcmp(name, "br") == 0) {
|
if (strcmp(name, "br") == 0) {
|
||||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||||
@ -119,9 +118,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->startNewTextBlock(TextBlock::JUSTIFIED);
|
self->startNewTextBlock(TextBlock::JUSTIFIED);
|
||||||
}
|
}
|
||||||
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||||
self->boldUntilDepth = min(self->boldUntilDepth, self->depth);
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||||
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
self->depth += 1;
|
self->depth += 1;
|
||||||
@ -180,7 +179,6 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
|
|
||||||
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
||||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||||
(void)name;
|
|
||||||
|
|
||||||
if (self->partWordBufferIndex > 0) {
|
if (self->partWordBufferIndex > 0) {
|
||||||
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
||||||
@ -263,9 +261,9 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t len = file.read(static_cast<uint8_t*>(buf), 1024);
|
const size_t len = file.read(buf, 1024);
|
||||||
|
|
||||||
if (len == 0) {
|
if (len == 0 && file.available() > 0) {
|
||||||
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
||||||
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
||||||
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
||||||
|
|||||||
@ -127,6 +127,18 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
|
self->state = IN_GUIDE;
|
||||||
|
// TODO Remove print
|
||||||
|
Serial.printf("[%lu] [COF] Entering guide state.\n", millis());
|
||||||
|
if (!SdMan.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
Serial.printf(
|
||||||
|
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
||||||
|
millis());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
|
if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
|
||||||
bool isCover = false;
|
bool isCover = false;
|
||||||
std::string coverItemId;
|
std::string coverItemId;
|
||||||
@ -205,6 +217,29 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// parse the guide
|
||||||
|
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
||||||
|
std::string type;
|
||||||
|
std::string textHref;
|
||||||
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "type") == 0) {
|
||||||
|
type = atts[i + 1];
|
||||||
|
if (type == "text" || type == "start") {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [COF] Skipping non-text reference in guide: %s\n", millis(), type.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (strcmp(atts[i], "href") == 0) {
|
||||||
|
textHref = self->baseContentPath + atts[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((type == "text" || (type == "start" && !self->textReferenceHref.empty())) && (textHref.length() > 0)) {
|
||||||
|
Serial.printf("[%lu] [COF] Found %s reference in guide: %s.\n", millis(), type.c_str(), textHref.c_str());
|
||||||
|
self->textReferenceHref = textHref;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
|
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||||
@ -231,6 +266,12 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self->state == IN_GUIDE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||||
|
self->state = IN_PACKAGE;
|
||||||
|
self->tempItemStore.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
self->state = IN_PACKAGE;
|
self->state = IN_PACKAGE;
|
||||||
self->tempItemStore.close();
|
self->tempItemStore.close();
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class ContentOpfParser final : public Print {
|
|||||||
IN_BOOK_AUTHOR,
|
IN_BOOK_AUTHOR,
|
||||||
IN_MANIFEST,
|
IN_MANIFEST,
|
||||||
IN_SPINE,
|
IN_SPINE,
|
||||||
|
IN_GUIDE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::string& cachePath;
|
const std::string& cachePath;
|
||||||
@ -35,6 +36,7 @@ class ContentOpfParser final : public Print {
|
|||||||
std::string author;
|
std::string author;
|
||||||
std::string tocNcxPath;
|
std::string tocNcxPath;
|
||||||
std::string coverItemHref;
|
std::string coverItemHref;
|
||||||
|
std::string textReferenceHref;
|
||||||
|
|
||||||
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||||
BookMetadataCache* cache)
|
BookMetadataCache* cache)
|
||||||
|
|||||||
@ -37,7 +37,7 @@ class GfxRenderer {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
|
||||||
~GfxRenderer() = default;
|
~GfxRenderer() { freeBwBufferChunks(); }
|
||||||
|
|
||||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
||||||
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
|
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[platformio]
|
[platformio]
|
||||||
crosspoint_version = 0.10.0
|
crosspoint_version = 0.11.0
|
||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
|
|||||||
41
src/ScreenComponents.cpp
Normal file
41
src/ScreenComponents.cpp
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#include "ScreenComponents.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Battery.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top) {
|
||||||
|
// Left aligned battery icon and percentage
|
||||||
|
const uint16_t percentage = battery.readPercentage();
|
||||||
|
const auto percentageText = std::to_string(percentage) + "%";
|
||||||
|
renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str());
|
||||||
|
|
||||||
|
// 1 column on left, 2 columns on right, 5 columns of battery body
|
||||||
|
constexpr int batteryWidth = 15;
|
||||||
|
constexpr int batteryHeight = 10;
|
||||||
|
const int x = left;
|
||||||
|
const int y = top + 7;
|
||||||
|
|
||||||
|
// Top line
|
||||||
|
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
||||||
|
// Bottom line
|
||||||
|
renderer.drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1);
|
||||||
|
// Left line
|
||||||
|
renderer.drawLine(x, y, x, y + batteryHeight - 1);
|
||||||
|
// Battery end
|
||||||
|
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
|
||||||
|
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
|
||||||
|
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
|
||||||
|
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
|
||||||
|
|
||||||
|
// The +1 is to round up, so that we always fill at least one pixel
|
||||||
|
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
|
||||||
|
if (filledWidth > batteryWidth - 5) {
|
||||||
|
filledWidth = batteryWidth - 5; // Ensure we don't overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
||||||
|
}
|
||||||
8
src/ScreenComponents.h
Normal file
8
src/ScreenComponents.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class GfxRenderer;
|
||||||
|
|
||||||
|
class ScreenComponents {
|
||||||
|
public:
|
||||||
|
static void drawBattery(const GfxRenderer& renderer, int left, int top);
|
||||||
|
};
|
||||||
@ -1,10 +1,12 @@
|
|||||||
#include "HomeActivity.h"
|
#include "HomeActivity.h"
|
||||||
|
|
||||||
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "ScreenComponents.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
void HomeActivity::taskTrampoline(void* param) {
|
void HomeActivity::taskTrampoline(void* param) {
|
||||||
@ -22,6 +24,33 @@ void HomeActivity::onEnter() {
|
|||||||
// Check if we have a book to continue reading
|
// Check if we have a book to continue reading
|
||||||
hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str());
|
hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str());
|
||||||
|
|
||||||
|
if (hasContinueReading) {
|
||||||
|
// Extract filename from path for display
|
||||||
|
lastBookTitle = APP_STATE.openEpubPath;
|
||||||
|
const size_t lastSlash = lastBookTitle.find_last_of('/');
|
||||||
|
if (lastSlash != std::string::npos) {
|
||||||
|
lastBookTitle = lastBookTitle.substr(lastSlash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string ext4 = lastBookTitle.length() >= 4 ? lastBookTitle.substr(lastBookTitle.length() - 4) : "";
|
||||||
|
const std::string ext5 = lastBookTitle.length() >= 5 ? lastBookTitle.substr(lastBookTitle.length() - 5) : "";
|
||||||
|
// If epub, try to load the metadata for title/author
|
||||||
|
if (ext5 == ".epub") {
|
||||||
|
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||||
|
epub.load(false);
|
||||||
|
if (!epub.getTitle().empty()) {
|
||||||
|
lastBookTitle = std::string(epub.getTitle());
|
||||||
|
}
|
||||||
|
if (!epub.getAuthor().empty()) {
|
||||||
|
lastBookAuthor = std::string(epub.getAuthor());
|
||||||
|
}
|
||||||
|
} else if (ext5 == ".xtch") {
|
||||||
|
lastBookTitle.resize(lastBookTitle.length() - 5);
|
||||||
|
} else if (ext4 == ".xtc") {
|
||||||
|
lastBookTitle.resize(lastBookTitle.length() - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
@ -103,52 +132,191 @@ void HomeActivity::render() const {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "CrossPoint Reader", true, BOLD);
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Draw selection
|
constexpr int margin = 20;
|
||||||
renderer.fillRect(0, 60 + selectorIndex * 30 - 2, pageWidth - 1, 30);
|
constexpr int bottomMargin = 60;
|
||||||
|
|
||||||
int menuY = 60;
|
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
||||||
int menuIndex = 0;
|
const int bookWidth = pageWidth / 2;
|
||||||
|
const int bookHeight = pageHeight / 2;
|
||||||
|
const int bookX = (pageWidth - bookWidth) / 2;
|
||||||
|
constexpr int bookY = 30;
|
||||||
|
const bool bookSelected = hasContinueReading && selectorIndex == 0;
|
||||||
|
|
||||||
if (hasContinueReading) {
|
// Draw book card regardless, fill with message based on `hasContinueReading`
|
||||||
// Extract filename from path for display
|
{
|
||||||
std::string bookName = APP_STATE.openEpubPath;
|
if (bookSelected) {
|
||||||
const size_t lastSlash = bookName.find_last_of('/');
|
renderer.fillRect(bookX, bookY, bookWidth, bookHeight);
|
||||||
if (lastSlash != std::string::npos) {
|
} else {
|
||||||
bookName = bookName.substr(lastSlash + 1);
|
renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
|
||||||
}
|
|
||||||
// Remove .epub extension
|
|
||||||
if (bookName.length() > 5 && bookName.substr(bookName.length() - 5) == ".epub") {
|
|
||||||
bookName.resize(bookName.length() - 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Truncate if too long
|
// Bookmark icon in the top-right corner of the card
|
||||||
std::string continueLabel = "Continue: " + bookName;
|
const int bookmarkWidth = bookWidth / 8;
|
||||||
int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, continueLabel.c_str());
|
const int bookmarkHeight = bookHeight / 5;
|
||||||
while (itemWidth > renderer.getScreenWidth() - 40 && continueLabel.length() > 8) {
|
const int bookmarkX = bookX + bookWidth - bookmarkWidth - 8;
|
||||||
continueLabel.replace(continueLabel.length() - 5, 5, "...");
|
constexpr int bookmarkY = bookY + 1;
|
||||||
itemWidth = renderer.getTextWidth(UI_10_FONT_ID, continueLabel.c_str());
|
|
||||||
Serial.printf("[%lu] [HOM] width: %lu, pageWidth: %lu\n", millis(), itemWidth, pageWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, menuY, continueLabel.c_str(), selectorIndex != menuIndex);
|
// Main bookmark body (solid)
|
||||||
menuY += 30;
|
renderer.fillRect(bookmarkX, bookmarkY, bookmarkWidth, bookmarkHeight, !bookSelected);
|
||||||
menuIndex++;
|
|
||||||
|
// Carve out an inverted triangle notch at the bottom center to create angled points
|
||||||
|
const int notchHeight = bookmarkHeight / 2; // depth of the notch
|
||||||
|
for (int i = 0; i < notchHeight; ++i) {
|
||||||
|
const int y = bookmarkY + bookmarkHeight - 1 - i;
|
||||||
|
const int xStart = bookmarkX + i;
|
||||||
|
const int width = bookmarkWidth - 2 * i;
|
||||||
|
if (width <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Draw a horizontal strip in the opposite color to "cut" the notch
|
||||||
|
renderer.fillRect(xStart, y, width, 1, bookSelected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, menuY, "Browse", selectorIndex != menuIndex);
|
if (hasContinueReading) {
|
||||||
menuY += 30;
|
// Split into words (avoid stringstream to keep this light on the MCU)
|
||||||
menuIndex++;
|
std::vector<std::string> words;
|
||||||
|
words.reserve(8);
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < lastBookTitle.size()) {
|
||||||
|
while (pos < lastBookTitle.size() && lastBookTitle[pos] == ' ') {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
if (pos >= lastBookTitle.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const size_t start = pos;
|
||||||
|
while (pos < lastBookTitle.size() && lastBookTitle[pos] != ' ') {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
words.emplace_back(lastBookTitle.substr(start, pos - start));
|
||||||
|
}
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, menuY, "File transfer", selectorIndex != menuIndex);
|
std::vector<std::string> lines;
|
||||||
menuY += 30;
|
std::string currentLine;
|
||||||
menuIndex++;
|
// Extra padding inside the card so text doesn't hug the border
|
||||||
|
const int maxLineWidth = bookWidth - 40;
|
||||||
|
const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID);
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, menuY, "Settings", selectorIndex != menuIndex);
|
for (auto& i : words) {
|
||||||
|
// If we just hit the line limit (3), stop processing words
|
||||||
|
if (lines.size() >= 3) {
|
||||||
|
// Limit to 3 lines
|
||||||
|
// Still have words left, so add ellipsis to last line
|
||||||
|
lines.back().append("...");
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("Back", "Confirm", "Left", "Right");
|
while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) {
|
||||||
|
lines.back().resize(lines.back().size() - 5);
|
||||||
|
lines.back().append("...");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
||||||
|
while (wordWidth > maxLineWidth && i.size() > 5) {
|
||||||
|
// Word itself is too long, trim it
|
||||||
|
i.resize(i.size() - 5);
|
||||||
|
i.append("...");
|
||||||
|
wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str());
|
||||||
|
if (newLineWidth > 0) {
|
||||||
|
newLineWidth += spaceWidth;
|
||||||
|
}
|
||||||
|
newLineWidth += wordWidth;
|
||||||
|
|
||||||
|
if (newLineWidth > maxLineWidth && !currentLine.empty()) {
|
||||||
|
// New line too long, push old line
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
currentLine = i;
|
||||||
|
} else {
|
||||||
|
currentLine.append(" ").append(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If lower than the line limit, push remaining words
|
||||||
|
if (!currentLine.empty() && lines.size() < 3) {
|
||||||
|
lines.push_back(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Book title text
|
||||||
|
int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast<int>(lines.size());
|
||||||
|
if (!lastBookAuthor.empty()) {
|
||||||
|
totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertically center the title block within the card
|
||||||
|
int titleYStart = bookY + (bookHeight - totalTextHeight) / 2;
|
||||||
|
|
||||||
|
for (const auto& line : lines) {
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected);
|
||||||
|
titleYStart += renderer.getLineHeight(UI_12_FONT_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastBookAuthor.empty()) {
|
||||||
|
titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2;
|
||||||
|
std::string trimmedAuthor = lastBookAuthor;
|
||||||
|
// Trim author if too long
|
||||||
|
while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) {
|
||||||
|
trimmedAuthor.resize(trimmedAuthor.size() - 5);
|
||||||
|
trimmedAuthor.append("...");
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2,
|
||||||
|
"Continue Reading", !bookSelected);
|
||||||
|
} else {
|
||||||
|
// No book to continue reading
|
||||||
|
const int y =
|
||||||
|
bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2;
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book");
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bottom menu tiles (indices 1-3) ---
|
||||||
|
const int menuTileWidth = pageWidth - 2 * margin;
|
||||||
|
constexpr int menuTileHeight = 50;
|
||||||
|
constexpr int menuSpacing = 10;
|
||||||
|
constexpr int totalMenuHeight = 3 * menuTileHeight + 2 * menuSpacing;
|
||||||
|
|
||||||
|
int menuStartY = bookY + bookHeight + 20;
|
||||||
|
// Ensure we don't collide with the bottom button legend
|
||||||
|
const int maxMenuStartY = pageHeight - bottomMargin - totalMenuHeight - margin;
|
||||||
|
if (menuStartY > maxMenuStartY) {
|
||||||
|
menuStartY = maxMenuStartY;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
constexpr const char* items[3] = {"Browse files", "File transfer", "Settings"};
|
||||||
|
const int overallIndex = i + (getMenuItemCount() - 3);
|
||||||
|
constexpr int tileX = margin;
|
||||||
|
const int tileY = menuStartY + i * (menuTileHeight + menuSpacing);
|
||||||
|
const bool selected = selectorIndex == overallIndex;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
renderer.fillRect(tileX, tileY, menuTileWidth, menuTileHeight);
|
||||||
|
} else {
|
||||||
|
renderer.drawRect(tileX, tileY, menuTileWidth, menuTileHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* label = items[i];
|
||||||
|
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
|
||||||
|
const int textX = tileX + (menuTileWidth - textWidth) / 2;
|
||||||
|
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
|
const int textY = tileY + (menuTileHeight - lineHeight) / 2; // vertically centered assuming y is top of text
|
||||||
|
|
||||||
|
// Invert text when the tile is selected, to contrast with the filled background
|
||||||
|
renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down");
|
||||||
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);
|
||||||
|
|
||||||
|
ScreenComponents::drawBattery(renderer, 20, pageHeight - 30);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ class HomeActivity final : public Activity {
|
|||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
bool hasContinueReading = false;
|
bool hasContinueReading = false;
|
||||||
|
std::string lastBookTitle;
|
||||||
|
std::string lastBookAuthor;
|
||||||
const std::function<void()> onContinueReading;
|
const std::function<void()> onContinueReading;
|
||||||
const std::function<void()> onReaderOpen;
|
const std::function<void()> onReaderOpen;
|
||||||
const std::function<void()> onSettingsOpen;
|
const std::function<void()> onSettingsOpen;
|
||||||
|
|||||||
@ -5,11 +5,11 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
#include "Battery.h"
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "ScreenComponents.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@ -65,6 +65,16 @@ void EpubReaderActivity::onEnter() {
|
|||||||
}
|
}
|
||||||
f.close();
|
f.close();
|
||||||
}
|
}
|
||||||
|
// We may want a better condition to detect if we are opening for the first time.
|
||||||
|
// This will trigger if the book is re-opened at Chapter 0.
|
||||||
|
if (currentSpineIndex == 0) {
|
||||||
|
int textSpineIndex = epub->getSpineIndexForTextReference();
|
||||||
|
if (textSpineIndex != 0) {
|
||||||
|
currentSpineIndex = textSpineIndex;
|
||||||
|
Serial.printf("[%lu] [ERS] Opened for first time, navigating to text reference at index %d\n", millis(),
|
||||||
|
textSpineIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save current epub as last opened epub
|
// Save current epub as last opened epub
|
||||||
APP_STATE.openEpubPath = epub->getPath();
|
APP_STATE.openEpubPath = epub->getPath();
|
||||||
@ -412,7 +422,6 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
|||||||
// Position status bar near the bottom of the logical screen, regardless of orientation
|
// Position status bar near the bottom of the logical screen, regardless of orientation
|
||||||
const auto screenHeight = renderer.getScreenHeight();
|
const auto screenHeight = renderer.getScreenHeight();
|
||||||
const auto textY = screenHeight - orientedMarginBottom - 2;
|
const auto textY = screenHeight - orientedMarginBottom - 2;
|
||||||
int percentageTextWidth = 0;
|
|
||||||
int progressTextWidth = 0;
|
int progressTextWidth = 0;
|
||||||
|
|
||||||
if (showProgress) {
|
if (showProgress) {
|
||||||
@ -429,42 +438,13 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showBattery) {
|
if (showBattery) {
|
||||||
// Left aligned battery icon and percentage
|
ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY);
|
||||||
const uint16_t percentage = battery.readPercentage();
|
|
||||||
const auto percentageText = std::to_string(percentage) + "%";
|
|
||||||
percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
|
||||||
renderer.drawText(SMALL_FONT_ID, 20 + orientedMarginLeft, textY, percentageText.c_str());
|
|
||||||
|
|
||||||
// 1 column on left, 2 columns on right, 5 columns of battery body
|
|
||||||
constexpr int batteryWidth = 15;
|
|
||||||
constexpr int batteryHeight = 10;
|
|
||||||
const int x = orientedMarginLeft;
|
|
||||||
const int y = screenHeight - orientedMarginBottom + 5;
|
|
||||||
|
|
||||||
// Top line
|
|
||||||
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
|
||||||
// Bottom line
|
|
||||||
renderer.drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1);
|
|
||||||
// Left line
|
|
||||||
renderer.drawLine(x, y, x, y + batteryHeight - 1);
|
|
||||||
// Battery end
|
|
||||||
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
|
|
||||||
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
|
|
||||||
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
|
|
||||||
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
|
|
||||||
|
|
||||||
// The +1 is to round up, so that we always fill at least one pixel
|
|
||||||
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
|
|
||||||
if (filledWidth > batteryWidth - 5) {
|
|
||||||
filledWidth = batteryWidth - 5; // Ensure we don't overflow
|
|
||||||
}
|
|
||||||
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showChapterTitle) {
|
if (showChapterTitle) {
|
||||||
// Centered chatper title text
|
// Centered chatper title text
|
||||||
// Page width minus existing content with 30px padding on each side
|
// Page width minus existing content with 30px padding on each side
|
||||||
const int titleMarginLeft = 20 + percentageTextWidth + 30 + orientedMarginLeft;
|
const int titleMarginLeft = 50 + 30 + orientedMarginLeft; // 50px for battery
|
||||||
const int titleMarginRight = progressTextWidth + 30 + orientedMarginRight;
|
const int titleMarginRight = progressTextWidth + 30 + orientedMarginRight;
|
||||||
const int availableTextWidth = renderer.getScreenWidth() - titleMarginLeft - titleMarginRight;
|
const int availableTextWidth = renderer.getScreenWidth() - titleMarginLeft - titleMarginRight;
|
||||||
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
|
|||||||
@ -50,6 +50,14 @@ void CrossPointWebServer::begin() {
|
|||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port);
|
||||||
server.reset(new WebServer(port));
|
server.reset(new WebServer(port));
|
||||||
|
|
||||||
|
// Disable WiFi sleep to improve responsiveness and prevent 'unreachable' errors.
|
||||||
|
// This is critical for reliable web server operation on ESP32.
|
||||||
|
WiFi.setSleep(false);
|
||||||
|
|
||||||
|
// Note: WebServer class doesn't have setNoDelay() in the standard ESP32 library.
|
||||||
|
// We rely on disabling WiFi sleep for responsiveness.
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after WebServer allocation: %d bytes\n", millis(), ESP.getFreeHeap());
|
Serial.printf("[%lu] [WEB] [MEM] Free heap after WebServer allocation: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||||
|
|
||||||
if (!server) {
|
if (!server) {
|
||||||
@ -157,15 +165,16 @@ void CrossPointWebServer::handleStatus() const {
|
|||||||
// Get correct IP based on AP vs STA mode
|
// Get correct IP based on AP vs STA mode
|
||||||
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
const String ipAddr = apMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString();
|
||||||
|
|
||||||
String json = "{";
|
JsonDocument doc;
|
||||||
json += "\"version\":\"" + String(CROSSPOINT_VERSION) + "\",";
|
doc["version"] = CROSSPOINT_VERSION;
|
||||||
json += "\"ip\":\"" + ipAddr + "\",";
|
doc["ip"] = ipAddr;
|
||||||
json += "\"mode\":\"" + String(apMode ? "AP" : "STA") + "\",";
|
doc["mode"] = apMode ? "AP" : "STA";
|
||||||
json += "\"rssi\":" + String(apMode ? 0 : WiFi.RSSI()) + ","; // RSSI not applicable in AP mode
|
doc["rssi"] = apMode ? 0 : WiFi.RSSI();
|
||||||
json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
|
doc["freeHeap"] = ESP.getFreeHeap();
|
||||||
json += "\"uptime\":" + String(millis() / 1000);
|
doc["uptime"] = millis() / 1000;
|
||||||
json += "}";
|
|
||||||
|
|
||||||
|
String json;
|
||||||
|
serializeJson(doc, json);
|
||||||
server->send(200, "application/json", json);
|
server->send(200, "application/json", json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +229,7 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function<void(F
|
|||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
yield(); // Yield to allow WiFi and other tasks to process during long scans
|
||||||
file = root.openNextFile();
|
file = root.openNextFile();
|
||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
@ -254,12 +264,15 @@ void CrossPointWebServer::handleFileListData() const {
|
|||||||
char output[512];
|
char output[512];
|
||||||
constexpr size_t outputSize = sizeof(output);
|
constexpr size_t outputSize = sizeof(output);
|
||||||
bool seenFirst = false;
|
bool seenFirst = false;
|
||||||
scanFiles(currentPath.c_str(), [this, &output, seenFirst](const FileInfo& info) mutable {
|
JsonDocument doc;
|
||||||
JsonDocument doc;
|
|
||||||
|
scanFiles(currentPath.c_str(), [this, &output, &doc, seenFirst](const FileInfo& info) mutable {
|
||||||
|
doc.clear();
|
||||||
doc["name"] = info.name;
|
doc["name"] = info.name;
|
||||||
doc["size"] = info.size;
|
doc["size"] = info.size;
|
||||||
doc["isDirectory"] = info.isDirectory;
|
doc["isDirectory"] = info.isDirectory;
|
||||||
doc["isEpub"] = info.isEpub;
|
doc["isEpub"] = info.isEpub;
|
||||||
|
|
||||||
const size_t written = serializeJson(doc, output, outputSize);
|
const size_t written = serializeJson(doc, output, outputSize);
|
||||||
if (written >= outputSize) {
|
if (written >= outputSize) {
|
||||||
// JSON output truncated; skip this entry to avoid sending malformed JSON
|
// JSON output truncated; skip this entry to avoid sending malformed JSON
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user