diff --git a/README.md b/README.md index 6f484f41..f015f718 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,13 @@ This project is **not affiliated with Xteink**; it's built as a community projec - [x] Support nested folders - [ ] EPUB picker with cover art - [x] Custom sleep screen - - [ ] Cover sleep screen + - [x] Cover sleep screen - [x] Wifi book upload -- [ ] Wifi OTA updates -- [ ] Configurable font, layout, and display options -- [ ] Screen rotation +- [x] Wifi OTA updates +- [x] Configurable font, layout, and display options + - [ ] User provided fonts + - [ ] Full UTF support +- [x] Screen rotation See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. diff --git a/USER_GUIDE.md b/USER_GUIDE.md index c0eeded5..26ff1075 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -5,7 +5,7 @@ the device. ## 1. Hardware Overview -The device utilises the standard buttons on the Xtink X4 in the same layout: +The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default): ### Button Layout | Location | Buttons | @@ -13,20 +13,23 @@ The device utilises the standard buttons on the Xtink X4 in the same layout: | **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** | | **Right Side** | **Power**, **Volume Up**, **Volume Down** | +Button layout can be customized in **[Settings](#35-settings)**. + --- ## 2. Power & Startup ### Power On / Off -To turn the device on or off, **press and hold the Power button for half a second**. In **Settings** you can configure +To turn the device on or off, **press and hold the Power button for half a second**. In **[Settings](#35-settings)** you can configure the power button to trigger on a short press instead of a long one. ### First Launch -Upon turning the device on for the first time, you will be placed on the **Home** screen. +Upon turning the device on for the first time, you will be placed on the **[Home](#31-home-screen)** screen. -> **Note:** On subsequent restarts, the firmware will automatically reopen the last book you were reading. +> [!NOTE] +> On subsequent restarts, the firmware will automatically reopen the last book you were reading. --- @@ -34,10 +37,10 @@ Upon turning the device on for the first time, you will be placed on the **Home* ### 3.1 Home Screen -The Home Screen is the main entry point to the firmware. From here you can navigate to the **Book Selection** screen, -**Settings** screen, or **File Upload** screen. +The Home Screen is the main entry point to the firmware. From here you can navigate to **[Reading Mode](#4-reading-mode)** with the most recently read book, **[Book Selection](#32-book-selection)**, +**[Settings](#35-settings)**, or the **[File Upload](#34-file-upload-screen)** screen. -### 3.2 Book Selection (Read) +### 3.2 Book Selection The Book Selection acts as a folder and file browser. @@ -45,13 +48,13 @@ The Book Selection acts as a folder and file browser. and down through folders and books. * **Open Selection:** Press **Confirm** to open a folder or read a selected book. -### 3.3 Reading Screen +### 3.3 Reading Mode -See [4. Reading Mode](#4-reading-mode) below for more information. +See [Reading Mode](#4-reading-mode) below for more information. ### 3.4 File Upload Screen -The File Upload screen allows you to upload new e-books to the device. When you enter the screen you'll be prompted with +The File Upload screen allows you to upload new e-books to the device. When you enter the screen, you'll be prompted with a WiFi selection dialog and then your X4 will start hosting a web server. See the [webserver docs](./docs/webserver.md) for more information on how to connect to the web server and upload files. @@ -62,12 +65,32 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Sleep Screen**: Which sleep screen to display when the device sleeps, options are: - "Dark" (default) - The default dark sleep screen - "Light" - The same default sleep screen, on a white background - - "Custom" - Custom images from the SD card, see [3.6 Sleep Screen](#36-sleep-screen) below for more information + - "Custom" - Custom images from the SD card, see [Sleep Screen](#36-sleep-screen) below for more information - "Cover" - The book cover image (Note: this is experimental and may not work as expected) +- **Status Bar**: Configure the status bar displayed while reading, options are: + - "None" - No status bar + - "No Progress" - Show status bar without reading progress + - "Full" - Show status bar with reading progress - **Extra Paragraph Spacing**: If enabled, vertical space will be added between paragraphs in the book, if disabled, paragraphs will not have vertical space between them, but will have first word indentation. - **Short Power Button Click**: Whether to trigger the power button on a short press or a long press. -- **Front Button Layout**: Swap the order of the bottom edge buttons from Back/Confirm/Left/Right to Left/Right/Back/Confirm. +- **Reading Orientation**: Set the screen orientation for reading, options are: + - "Portrait" (default) - Standard portrait orientation + - "Landscape CW" - Landscape, rotated clockwise + - "Inverted" - Portrait, upside down + - "Landscape CCW" - Landscape, rotated counter-clockwise +- **Front Button Layout**: Configure the order of the bottom edge buttons, options are: + - "Bck, Cnfrm, Lft, Rght" (default) - Back, Confirm, Left, Right + - "Lft, Rght, Bck, Cnfrm" - Left, Right, Back, Confirm + - "Lft, Bck, Cnfrm, Rght" - Left, Back, Confirm, Right +- **Side Button Layout**: Swap the order of the volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading. +- **Reader Font Family**: Choose the font used for reading, options are: + - "Bookerly" (default) - Amazon's reading font + - "Noto Sans" - Google's sans-serif font + - "Open Dyslexic" - Font designed for readers with dyslexia +- **Reader Font Size**: Adjust the text size for reading, options are "Small", "Medium", "Large", or "X Large". +- **Reader Line Spacing**: Adjust the spacing between lines, options are "Tight", "Normal", or "Wide". +- **Check for updates**: Check for firmware updates over WiFi. ### 3.6 Sleep Screen @@ -75,7 +98,7 @@ You can customize the sleep screen by placing custom images in specific location - **Single Image:** Place a file named `sleep.bmp` in the root directory. - **Multiple Images:** Create a `sleep` directory in the root of the SD card and place any number of `.bmp` images - inside. If images are found in this directory, they will take priority over the `sleep.png` file, and one will be + inside. If images are found in this directory, they will take priority over the `sleep.bmp` file, and one will be randomly selected each time the device sleeps. > [!NOTE] @@ -103,8 +126,9 @@ Once you have opened a book, the button layout changes to facilitate reading. * **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release. ### System Navigation -* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen. -* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter Selection screen. +* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen. +* **Return to Home:** Press and hold **Back** to close the book and return to the **[Home](#31-home-screen)** screen. +* **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**. --- @@ -124,5 +148,3 @@ Please note that this firmware is currently in active development. The following are planned for future updates: * **Images:** Embedded images in e-books will not render. -* **Text Formatting:** There are currently no settings to adjust font type, size, line spacing, or margins. -* **Rotation**: Different rotation options are not supported. diff --git a/docs/file-formats.md b/docs/file-formats.md index fb096c88..2fa0c60b 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -2,8 +2,220 @@ ## `book.bin` -![](./images/file-formats/book-bin.png) +### Version 3 + +ImHex Pattern: + +```c++ +import std.mem; +import std.string; +import std.core; + +// === Configuration === +#define EXPECTED_VERSION 3 +#define MAX_STRING_LENGTH 65535 + +// === String Structure === + +struct String { + u32 length [[hidden, comment("String byte length")]]; + if (length > MAX_STRING_LENGTH) { + std::warning(std::format("Unusually large string length: {} bytes", length)); + } + char data[length] [[comment("UTF-8 string data")]]; +} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]]; + +fn format_string(String s) { + return s.data; +}; + +// === Metadata Structure === + +struct Metadata { + String title [[comment("Book title")]]; + String author [[comment("Book author")]]; + String coverItemHref [[comment("Path to cover image")]]; + String textReferenceHref [[comment("Path to guided first text reference")]]; +} [[comment("Book metadata information")]]; + +// === Spine Entry Structure === + +struct SpineEntry { + String href [[comment("Resource path")]]; + u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]]; + s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]]; +} [[comment("Spine entry defining reading order")]]; + +// === TOC Entry Structure === + +struct TocEntry { + String title [[comment("Chapter/section title")]]; + String href [[comment("Resource path")]]; + String anchor [[comment("Fragment identifier")]]; + u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]]; + s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]]; +} [[comment("Table of contents entry")]]; + +// === Book Bin Structure === + +struct BookBin { + // Header + u8 version [[comment("Format version"), color("FFD93D")]]; + + // Version validation + if (version != EXPECTED_VERSION) { + std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION)); + } + + u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]]; + u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]]; + u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]]; + + // Metadata section + Metadata metadata [[comment("Book metadata")]]; + + // Validate LUT offset alignment + u32 currentOffset = $; + if (currentOffset != lutOffset) { + std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset)); + } + + // Lookup Tables + u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]]; + u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]]; + + // Data Entries + SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]]; + TocEntry toc[tocCount] [[comment("Table of contents entries")]]; +}; + +// === File Parsing === + +BookBin book @ 0x00; + +// Validate we've consumed the entire file +u32 fileSize = std::mem::size(); +u32 parsedSize = $; + +if (parsedSize != fileSize) { + std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize)); +} +``` ## `section.bin` -![](./images/file-formats/section-bin.png) +### Version 8 + +ImHex Pattern: + +```c++ +import std.mem; +import std.string; +import std.core; + +// === Configuration === +#define EXPECTED_VERSION 8 +#define MAX_STRING_LENGTH 65535 + +// === String Structure === + +struct String { + u32 length [[hidden, comment("String byte length")]]; + if (length > MAX_STRING_LENGTH) { + std::warning(std::format("Unusually large string length: {} bytes", length)); + } + char data[length] [[comment("UTF-8 string data")]]; +} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]]; + +fn format_string(String s) { + return s.data; +}; + +// === Page Structure === + +enum StorageType : u8 { + PageLine = 1 +}; + +enum WordStyle : u8 { + REGULAR = 0, + BOLD = 1, + ITALIC = 2, + BOLD_ITALIC = 3 +}; + +enum BlockStyle : u8 { + JUSTIFIED = 0, + LEFT_ALIGN = 1, + CENTER_ALIGN = 2, + RIGHT_ALIGN = 3, +}; + +struct PageLine { + s16 xPos; + s16 yPos; + u16 wordCount; + String words[wordCount]; + u16 wordXPos[wordCount]; + WordStyle wordStyle[wordCount]; + BlockStyle blockStyle; +}; + +struct PageElement { + u8 pageElementType; + if (pageElementType == 1) { + PageLine pageLine [[inline]]; + } else { + std::error(std::format("Unknown page element type: {}", pageElementType)); + } +}; + +struct Page { + u16 elementCount; + PageElement elements[elementCount] [[inline]]; +}; + +// === Section Bin Structure === + +struct SectionBin { + // Header + u8 version [[comment("Format version"), color("FFD93D")]]; + + // Version validation + if (version != EXPECTED_VERSION) { + std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION)); + } + + // Cache busting parameters + s32 fontId; + float lineCompression; + bool extraParagraphSpacing; + u16 viewportWidth; + u16 vieportHeight; + u16 pageCount; + u32 lutOffset; + + Page page[pageCount]; + + // Validate LUT offset alignment + u32 currentOffset = $; + if (currentOffset != lutOffset) { + std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset)); + } + + // Lookup Tables + u32 lut[pageCount]; +}; + +// === File Parsing === + +SectionBin book @ 0x00; + +// Validate we've consumed the entire file +u32 fileSize = std::mem::size(); +u32 parsedSize = $; + +if (parsedSize != fileSize) { + std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize)); +} +``` diff --git a/docs/images/file-formats/book-bin.png b/docs/images/file-formats/book-bin.png deleted file mode 100644 index 07d9c2eb..00000000 Binary files a/docs/images/file-formats/book-bin.png and /dev/null differ diff --git a/docs/images/file-formats/section-bin.png b/docs/images/file-formats/section-bin.png deleted file mode 100644 index 9a9691c6..00000000 Binary files a/docs/images/file-formats/section-bin.png and /dev/null differ diff --git a/lib/EpdFont/EpdFontFamily.cpp b/lib/EpdFont/EpdFontFamily.cpp index e70f8a6d..74a6677f 100644 --- a/lib/EpdFont/EpdFontFamily.cpp +++ b/lib/EpdFont/EpdFontFamily.cpp @@ -1,6 +1,6 @@ #include "EpdFontFamily.h" -const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const { +const EpdFont* EpdFontFamily::getFont(const Style style) const { if (style == BOLD && bold) { return bold; } @@ -22,16 +22,16 @@ const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const { return regular; } -void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const EpdFontStyle style) const { +void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const Style style) const { getFont(style)->getTextDimensions(string, w, h); } -bool EpdFontFamily::hasPrintableChars(const char* string, const EpdFontStyle style) const { +bool EpdFontFamily::hasPrintableChars(const char* string, const Style style) const { return getFont(style)->hasPrintableChars(string); } -const EpdFontData* EpdFontFamily::getData(const EpdFontStyle style) const { return getFont(style)->data; } +const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; } -const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const EpdFontStyle style) const { +const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const { return getFont(style)->getGlyph(cp); }; diff --git a/lib/EpdFont/EpdFontFamily.h b/lib/EpdFont/EpdFontFamily.h index dbf8ccd6..92043d1f 100644 --- a/lib/EpdFont/EpdFontFamily.h +++ b/lib/EpdFont/EpdFontFamily.h @@ -1,24 +1,24 @@ #pragma once #include "EpdFont.h" -enum EpdFontStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC }; - class EpdFontFamily { + public: + enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 }; + + explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr, + const EpdFont* boldItalic = nullptr) + : regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {} + ~EpdFontFamily() = default; + void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const; + bool hasPrintableChars(const char* string, Style style = REGULAR) const; + const EpdFontData* getData(Style style = REGULAR) const; + const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const; + + private: const EpdFont* regular; const EpdFont* bold; const EpdFont* italic; const EpdFont* boldItalic; - const EpdFont* getFont(EpdFontStyle style) const; - - public: - explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr, - const EpdFont* boldItalic = nullptr) - : regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {} - ~EpdFontFamily() = default; - void getTextDimensions(const char* string, int* w, int* h, EpdFontStyle style = REGULAR) const; - bool hasPrintableChars(const char* string, EpdFontStyle style = REGULAR) const; - - const EpdFontData* getData(EpdFontStyle style = REGULAR) const; - const EpdGlyph* getGlyph(uint32_t cp, EpdFontStyle style = REGULAR) const; + const EpdFont* getFont(Style style) const; }; diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 65ce5698..92839eb7 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -32,7 +32,7 @@ void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, co } bool Page::serialize(FsFile& file) const { - const uint32_t count = elements.size(); + const uint16_t count = elements.size(); serialization::writePod(file, count); for (const auto& el : elements) { @@ -49,10 +49,10 @@ bool Page::serialize(FsFile& file) const { std::unique_ptr Page::deserialize(FsFile& file) { auto page = std::unique_ptr(new Page()); - uint32_t count; + uint16_t count; serialization::readPod(file, count); - for (uint32_t i = 0; i < count; i++) { + for (uint16_t i = 0; i < count; i++) { uint8_t tag; serialization::readPod(file, tag); diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 0e850f31..f9c0326f 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -10,7 +10,7 @@ constexpr int MAX_COST = std::numeric_limits::max(); -void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) { +void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) { if (word.empty()) return; words.push_back(std::move(word)); @@ -18,7 +18,7 @@ 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 viewportWidth, +void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const uint16_t viewportWidth, const std::function)>& processLine, const bool includeLastLine) { if (words.empty()) { @@ -188,7 +188,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const // *** CRITICAL STEP: CONSUME DATA USING SPLICE *** std::list lineWords; lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt); - std::list lineWordStyles; + std::list lineWordStyles; lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt); processLine(std::make_shared(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style)); diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 2696407f..4b851a94 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -14,8 +14,8 @@ class GfxRenderer; class ParsedText { std::list words; - std::list wordStyles; - TextBlock::BLOCK_STYLE style; + std::list wordStyles; + TextBlock::Style style; bool extraParagraphSpacing; std::vector computeLineBreaks(int pageWidth, int spaceWidth, const std::vector& wordWidths) const; @@ -25,16 +25,16 @@ class ParsedText { std::vector calculateWordWidths(const GfxRenderer& renderer, int fontId); public: - explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing) + explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing) : style(style), extraParagraphSpacing(extraParagraphSpacing) {} ~ParsedText() = default; - void addWord(std::string word, EpdFontStyle fontStyle); - void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; } - TextBlock::BLOCK_STYLE getStyle() const { return style; } + void addWord(std::string word, EpdFontFamily::Style fontStyle); + void setStyle(const TextBlock::Style style) { this->style = style; } + TextBlock::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 viewportWidth, + void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth, const std::function)>& processLine, bool includeLastLine = true); }; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index b153f4f0..1f99f018 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -7,9 +7,9 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 7; -constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(int) + - sizeof(int) + sizeof(int) + sizeof(uint32_t); +constexpr uint8_t SECTION_FILE_VERSION = 8; +constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint16_t) + + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t); } // namespace uint32_t Section::onPageComplete(std::unique_ptr page) { @@ -30,7 +30,7 @@ uint32_t Section::onPageComplete(std::unique_ptr page) { } void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const int viewportWidth, const int viewportHeight) { + const uint16_t viewportWidth, const uint16_t viewportHeight) { if (!file) { Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); return; @@ -50,7 +50,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi } bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const int viewportWidth, const int viewportHeight) { + const uint16_t viewportWidth, const uint16_t viewportHeight) { if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -66,7 +66,8 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con return false; } - int fileFontId, fileViewportWidth, fileViewportHeight; + int fileFontId; + uint16_t fileViewportWidth, fileViewportHeight; float fileLineCompression; bool fileExtraParagraphSpacing; serialization::readPod(file, fileFontId); @@ -108,7 +109,7 @@ bool Section::clearCache() const { } bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const int viewportWidth, const int viewportHeight, + const uint16_t viewportWidth, const uint16_t viewportHeight, const std::function& progressSetupFn, const std::function& progressFn) { constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index bc2efabc..55244d0e 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -14,12 +14,12 @@ class Section { std::string filePath; FsFile file; - void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, - int viewportHeight); + void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth, + uint16_t viewportHeight); uint32_t onPageComplete(std::unique_ptr page); public: - int pageCount = 0; + uint16_t pageCount = 0; int currentPage = 0; explicit Section(const std::shared_ptr& epub, const int spineIndex, GfxRenderer& renderer) @@ -28,11 +28,11 @@ class Section { renderer(renderer), filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} ~Section() = default; - bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, - int viewportHeight); + bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth, + uint16_t viewportHeight); bool clearCache() const; - bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, - int viewportHeight, const std::function& progressSetupFn = nullptr, + bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth, + uint16_t viewportHeight, const std::function& progressSetupFn = nullptr, const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 31193059..2a15aef0 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -32,7 +32,7 @@ bool TextBlock::serialize(FsFile& file) const { } // Word data - serialization::writePod(file, static_cast(words.size())); + serialization::writePod(file, static_cast(words.size())); for (const auto& w : words) serialization::writeString(file, w); for (auto x : wordXpos) serialization::writePod(file, x); for (auto s : wordStyles) serialization::writePod(file, s); @@ -44,11 +44,11 @@ bool TextBlock::serialize(FsFile& file) const { } std::unique_ptr TextBlock::deserialize(FsFile& file) { - uint32_t wc; + uint16_t wc; std::list words; std::list wordXpos; - std::list wordStyles; - BLOCK_STYLE style; + std::list wordStyles; + Style style; // Word count serialization::readPod(file, wc); diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 95d88846..415a18f3 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -8,10 +8,10 @@ #include "Block.h" -// represents a block of words in the html document +// Represents a line of text on a page class TextBlock final : public Block { public: - enum BLOCK_STYLE : uint8_t { + enum Style : uint8_t { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, @@ -21,16 +21,16 @@ class TextBlock final : public Block { private: std::list words; std::list wordXpos; - std::list wordStyles; - BLOCK_STYLE style; + std::list wordStyles; + Style style; public: - explicit TextBlock(std::list words, std::list word_xpos, std::list word_styles, - const BLOCK_STYLE style) + explicit TextBlock(std::list words, std::list word_xpos, + std::list word_styles, const Style style) : words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {} ~TextBlock() override = default; - void setStyle(const BLOCK_STYLE style) { this->style = style; } - BLOCK_STYLE getStyle() const { return style; } + void setStyle(const Style style) { this->style = style; } + Style getStyle() const { return style; } bool isEmpty() override { return words.empty(); } void layout(GfxRenderer& renderer) override {}; // given a renderer works out where to break the words into lines diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 9f7fed9f..5cd53293 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -42,7 +42,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib } // start a new text block if needed -void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style) { +void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { if (currentTextBlock) { // already have a text block running and it is empty - just reuse it if (currentTextBlock->isEmpty()) { @@ -116,13 +116,13 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char return; } - EpdFontStyle fontStyle = REGULAR; + EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) { - fontStyle = BOLD_ITALIC; + fontStyle = EpdFontFamily::BOLD_ITALIC; } else if (self->boldUntilDepth < self->depth) { - fontStyle = BOLD; + fontStyle = EpdFontFamily::BOLD; } else if (self->italicUntilDepth < self->depth) { - fontStyle = ITALIC; + fontStyle = EpdFontFamily::ITALIC; } for (int i = 0; i < len; i++) { @@ -172,13 +172,13 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1; if (shouldBreakText) { - EpdFontStyle fontStyle = REGULAR; + EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) { - fontStyle = BOLD_ITALIC; + fontStyle = EpdFontFamily::BOLD_ITALIC; } else if (self->boldUntilDepth < self->depth) { - fontStyle = BOLD; + fontStyle = EpdFontFamily::BOLD; } else if (self->italicUntilDepth < self->depth) { - fontStyle = ITALIC; + fontStyle = EpdFontFamily::ITALIC; } self->partWordBuffer[self->partWordBufferIndex] = '\0'; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 53bbbb4f..795c2c33 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -33,10 +33,10 @@ class ChapterHtmlSlimParser { int fontId; float lineCompression; bool extraParagraphSpacing; - int viewportWidth; - int viewportHeight; + uint16_t viewportWidth; + uint16_t viewportHeight; - void startNewTextBlock(TextBlock::BLOCK_STYLE style); + void startNewTextBlock(TextBlock::Style style); void makePages(); // XML callbacks static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); @@ -45,8 +45,8 @@ class ChapterHtmlSlimParser { public: explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, - const float lineCompression, const bool extraParagraphSpacing, const int viewportWidth, - const int viewportHeight, + const float lineCompression, const bool extraParagraphSpacing, + const uint16_t viewportWidth, const uint16_t viewportHeight, const std::function)>& completePageFn, const std::function& progressFn = nullptr) : filepath(filepath), diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 0fc4abf1..638fdf01 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -66,7 +66,7 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { } } -int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontStyle style) const { +int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const { if (fontMap.count(fontId) == 0) { Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId); return 0; @@ -78,13 +78,13 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontS } void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black, - const EpdFontStyle style) const { + const EpdFontFamily::Style style) const { const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2; drawText(fontId, x, y, text, black, style); } void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black, - const EpdFontStyle style) const { + const EpdFontFamily::Style style) const { const int yPos = y + getFontAscenderSize(fontId); int xpos = x; @@ -238,6 +238,17 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons einkDisplay.displayBuffer(refreshMode); } +std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, + const EpdFontFamily::Style style) const { + std::string item = text; + int itemWidth = getTextWidth(fontId, item.c_str(), style); + while (itemWidth > maxWidth && item.length() > 8) { + item.replace(item.length() - 5, 5, "..."); + itemWidth = getTextWidth(fontId, item.c_str(), style); + } + return item; +} + // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation int GfxRenderer::getScreenWidth() const { switch (orientation) { @@ -273,7 +284,7 @@ int GfxRenderer::getSpaceWidth(const int fontId) const { return 0; } - return fontMap.at(fontId).getGlyph(' ', REGULAR)->advanceX; + return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX; } int GfxRenderer::getFontAscenderSize(const int fontId) const { @@ -282,7 +293,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const { return 0; } - return fontMap.at(fontId).getData(REGULAR)->ascender; + return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender; } int GfxRenderer::getLineHeight(const int fontId) const { @@ -291,7 +302,7 @@ int GfxRenderer::getLineHeight(const int fontId) const { return 0; } - return fontMap.at(fontId).getData(REGULAR)->advanceY; + return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY; } void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3, @@ -436,7 +447,7 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { } void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y, - const bool pixelState, const EpdFontStyle style) const { + const bool pixelState, const EpdFontFamily::Style style) const { const EpdGlyph* glyph = fontFamily.getGlyph(cp, style); if (!glyph) { // TODO: Replace with fallback glyph property? diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 0d7bb885..7b0bcc00 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -31,7 +31,7 @@ class GfxRenderer { uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; std::map fontMap; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, - EpdFontStyle style) const; + EpdFontFamily::Style style) const; void freeBwBufferChunks(); void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; @@ -69,12 +69,16 @@ class GfxRenderer { void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const; // Text - int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const; - void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const; - void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const; + int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; + void drawCenteredText(int fontId, int y, const char* text, bool black = true, + EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; + void drawText(int fontId, int x, int y, const char* text, bool black = true, + EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; int getSpaceWidth(int fontId) const; int getFontAscenderSize(int fontId) const; int getLineHeight(int fontId) const; + std::string truncatedText(int fontId, const char* text, int maxWidth, + EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; // UI Components void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const; diff --git a/platformio.ini b/platformio.ini index dcec29a5..fad8c08c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,5 +1,5 @@ [platformio] -crosspoint_version = 0.11.1 +crosspoint_version = 0.11.2 default_envs = default [base] @@ -26,6 +26,8 @@ build_flags = -DXML_GE=0 -DXML_CONTEXT_BYTES=1024 -std=c++2a +# Enable UTF-8 long file names in SdFat + -DUSE_UTF8_LONG_NAMES=1 ; Board configuration board_build.flash_mode = dio diff --git a/src/activities/boot_sleep/BootActivity.cpp b/src/activities/boot_sleep/BootActivity.cpp index e777a7c4..65eb6a07 100644 --- a/src/activities/boot_sleep/BootActivity.cpp +++ b/src/activities/boot_sleep/BootActivity.cpp @@ -13,7 +13,7 @@ void BootActivity::onEnter() { renderer.clearScreen(); renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION); renderer.displayBuffer(); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index ee025959..11b8fee9 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -127,7 +127,7 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.clearScreen(); renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); // Make sleep screen dark unless light is selected in settings diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 875d6e48..a7160137 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -334,7 +334,7 @@ void CrossPointWebServerActivity::render() const { } else if (state == WebServerActivityState::AP_STARTING) { renderer.clearScreen(); const auto pageHeight = renderer.getScreenHeight(); - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD); renderer.displayBuffer(); } } @@ -365,13 +365,13 @@ void CrossPointWebServerActivity::renderServerRunning() const { // Use consistent line spacing constexpr int LINE_SPACING = 28; // Space between lines - renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); if (isApMode) { // AP mode display - center the content block int startY = 55; - renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + connectedSSID; renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); @@ -387,7 +387,7 @@ void CrossPointWebServerActivity::renderServerRunning() const { startY += 6 * 29 + 3 * LINE_SPACING; // Show primary URL (hostname) std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/"; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD); // Show IP address as fallback std::string ipUrl = "or http://" + connectedIP + "/"; @@ -412,7 +412,7 @@ void CrossPointWebServerActivity::renderServerRunning() const { // Show web server URL prominently std::string webInfo = "http://" + connectedIP + "/"; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD); // Also show hostname URL std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/"; diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 13da214a..ad05f5b8 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -97,7 +97,7 @@ void NetworkModeSelectionActivity::render() const { const auto pageHeight = renderer.getScreenHeight(); // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); // Draw subtitle renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?"); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index ab1e96b7..cbcec2d1 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -496,7 +496,7 @@ void WifiSelectionActivity::renderNetworkList() const { const auto pageHeight = renderer.getScreenHeight(); // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, EpdFontFamily::BOLD); if (networks.empty()) { // No networks found or scan failed @@ -577,7 +577,7 @@ void WifiSelectionActivity::renderConnecting() const { if (state == WifiSelectionState::SCANNING) { renderer.drawCenteredText(UI_10_FONT_ID, top, "Scanning..."); } else { - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connecting...", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connecting...", true, EpdFontFamily::BOLD); std::string ssidInfo = "to " + selectedSSID; if (ssidInfo.length() > 25) { @@ -592,7 +592,7 @@ void WifiSelectionActivity::renderConnected() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 4) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 30, "Connected!", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 30, "Connected!", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { @@ -612,7 +612,7 @@ void WifiSelectionActivity::renderSavePrompt() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connected!", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connected!", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { @@ -651,7 +651,7 @@ void WifiSelectionActivity::renderConnectionFailed() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 2) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 20, "Connection Failed", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 20, "Connection Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue"); } @@ -662,7 +662,7 @@ void WifiSelectionActivity::renderForgetPrompt() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network?", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network?", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index f9ef40c7..fae5d241 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -244,7 +244,7 @@ void EpubReaderActivity::renderScreen() { // Show end of book screen if (currentSpineIndex == epub->getSpineItemsCount()) { renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } @@ -263,8 +263,8 @@ void EpubReaderActivity::renderScreen() { Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); - const auto viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; - const auto viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; + const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; + const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight)) { @@ -332,7 +332,7 @@ void EpubReaderActivity::renderScreen() { if (section->pageCount == 0) { Serial.printf("[%lu] [ERS] No pages to render\n", millis()); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; @@ -340,7 +340,7 @@ void EpubReaderActivity::renderScreen() { if (section->currentPage < 0 || section->currentPage >= section->pageCount) { Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 25fa6167..63f1e5a7 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -120,7 +120,10 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const auto pageWidth = renderer.getScreenWidth(); const int pageItems = getPageItems(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, BOLD); + + const std::string title = + renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD); const auto pageStartIndex = selectorIndex / pageItems * pageItems; renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); diff --git a/src/activities/reader/FileSelectionActivity.cpp b/src/activities/reader/FileSelectionActivity.cpp index 4f4ed9dc..f87cc97c 100644 --- a/src/activities/reader/FileSelectionActivity.cpp +++ b/src/activities/reader/FileSelectionActivity.cpp @@ -30,11 +30,19 @@ void FileSelectionActivity::taskTrampoline(void* param) { void FileSelectionActivity::loadFiles() { files.clear(); selectorIndex = 0; + auto root = SdMan.open(basepath.c_str()); + if (!root || !root.isDirectory()) { + if (root) root.close(); + return; + } + + root.rewindDirectory(); + char name[128]; for (auto file = root.openNextFile(); file; file = root.openNextFile()) { file.getName(name, sizeof(name)); - if (name[0] == '.') { + if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) { file.close(); continue; } @@ -165,7 +173,7 @@ void FileSelectionActivity::render() const { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Books", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Books", true, EpdFontFamily::BOLD); // Help text const auto labels = mappedInput.mapLabels("« Home", "Open", "", ""); @@ -180,12 +188,7 @@ void FileSelectionActivity::render() const { const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); for (int i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { - auto item = files[i]; - int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, item.c_str()); - while (itemWidth > renderer.getScreenWidth() - 40 && item.length() > 8) { - item.replace(item.length() - 5, 5, "..."); - itemWidth = renderer.getTextWidth(UI_10_FONT_ID, item.c_str()); - } + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); } diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index d6a3aa6e..1829218a 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -68,8 +68,8 @@ void ReaderActivity::onSelectBookFile(const std::string& path) { onGoToXtcReader(std::move(xtc)); } else { exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC", REGULAR, - EInkDisplay::HALF_REFRESH)); + enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC", + EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); delay(2000); onGoToFileSelection(); } @@ -80,8 +80,8 @@ void ReaderActivity::onSelectBookFile(const std::string& path) { onGoToEpubReader(std::move(epub)); } else { exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub", REGULAR, - EInkDisplay::HALF_REFRESH)); + enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub", + EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); delay(2000); onGoToFileSelection(); } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 5f8a74c9..efdd18a2 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -165,7 +165,7 @@ void XtcReaderActivity::renderScreen() { if (currentPage >= xtc->getPageCount()) { // Show end of book screen renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } @@ -194,7 +194,7 @@ void XtcReaderActivity::renderPage() { if (!pageBuffer) { Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize); renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } @@ -205,7 +205,7 @@ void XtcReaderActivity::renderPage() { Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage); free(pageBuffer); renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index 1170377d..fd732924 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -130,7 +130,7 @@ void XtcReaderChapterSelectionActivity::renderScreen() { const auto pageWidth = renderer.getScreenWidth(); const int pageItems = getPageItems(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD); const auto& chapters = xtc->getChapters(); if (chapters.empty()) { diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index e11b90cb..0393847d 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -124,36 +124,30 @@ void OtaUpdateActivity::render() { lastUpdaterPercentage = static_cast(updaterProgress * 100); } - const auto pageHeight = renderer.getScreenHeight(); const auto pageWidth = renderer.getScreenWidth(); renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, EpdFontFamily::BOLD); if (state == CHECKING_FOR_UPDATE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } if (state == WAITING_CONFIRMATION) { - renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, EpdFontFamily::BOLD); renderer.drawText(UI_10_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION); renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str()); - renderer.drawRect(25, pageHeight - 40, 106, 40); - renderer.drawText(UI_10_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_10_FONT_ID, "Cancel")) / 2, pageHeight - 35, - "Cancel"); - - renderer.drawRect(130, pageHeight - 40, 106, 40); - renderer.drawText(UI_10_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_10_FONT_ID, "Update")) / 2, pageHeight - 35, - "Update"); + const auto labels = mappedInput.mapLabels("Cancel", "Update", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; } if (state == UPDATE_IN_PROGRESS) { - renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, EpdFontFamily::BOLD); renderer.drawRect(20, 350, pageWidth - 40, 50); renderer.fillRect(24, 354, static_cast(updaterProgress * static_cast(pageWidth - 44)), 42); renderer.drawCenteredText(UI_10_FONT_ID, 420, @@ -166,19 +160,19 @@ void OtaUpdateActivity::render() { } if (state == NO_UPDATE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } if (state == FINISHED) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on"); renderer.displayBuffer(); state = SHUTTING_DOWN; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7218eaec..fa0cc084 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -163,7 +163,7 @@ void SettingsActivity::render() const { const auto pageHeight = renderer.getScreenHeight(); // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); // Draw selection renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); diff --git a/src/activities/util/FullScreenMessageActivity.h b/src/activities/util/FullScreenMessageActivity.h index c7985056..3e975c91 100644 --- a/src/activities/util/FullScreenMessageActivity.h +++ b/src/activities/util/FullScreenMessageActivity.h @@ -9,12 +9,12 @@ class FullScreenMessageActivity final : public Activity { std::string text; - EpdFontStyle style; + EpdFontFamily::Style style; EInkDisplay::RefreshMode refreshMode; public: explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text, - const EpdFontStyle style = REGULAR, + const EpdFontFamily::Style style = EpdFontFamily::REGULAR, const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) : Activity("FullScreenMessage", renderer, mappedInput), text(std::move(text)), diff --git a/src/main.cpp b/src/main.cpp index 090e5191..6859f41c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -151,8 +151,8 @@ void verifyWakeupLongPress() { const auto start = millis(); bool abort = false; // It takes us some time to wake up from deep sleep, so we need to subtract that from the duration - uint16_t calibration = 29; - uint16_t calibratedPressDuration = + constexpr uint16_t calibration = 29; + const uint16_t calibratedPressDuration = (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; inputManager.update(); @@ -271,7 +271,7 @@ void setup() { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); setupDisplayAndFonts(); exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", BOLD)); + enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); return; }