mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 08:07:40 +03:00
Merge branch 'daveallie:master' into improve/black-entering-sleep
This commit is contained in:
commit
60eb8e27aa
10
README.md
10
README.md
@ -33,11 +33,13 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
|||||||
- [x] Support nested folders
|
- [x] Support nested folders
|
||||||
- [ ] EPUB picker with cover art
|
- [ ] EPUB picker with cover art
|
||||||
- [x] Custom sleep screen
|
- [x] Custom sleep screen
|
||||||
- [ ] Cover sleep screen
|
- [x] Cover sleep screen
|
||||||
- [x] Wifi book upload
|
- [x] Wifi book upload
|
||||||
- [ ] Wifi OTA updates
|
- [x] Wifi OTA updates
|
||||||
- [ ] Configurable font, layout, and display options
|
- [x] Configurable font, layout, and display options
|
||||||
- [ ] Screen rotation
|
- [ ] User provided fonts
|
||||||
|
- [ ] Full UTF support
|
||||||
|
- [x] Screen rotation
|
||||||
|
|
||||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ the device.
|
|||||||
|
|
||||||
## 1. Hardware Overview
|
## 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
|
### Button Layout
|
||||||
| Location | Buttons |
|
| 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** |
|
| **Bottom Edge** | **Back**, **Confirm**, **Left**, **Right** |
|
||||||
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
|
| **Right Side** | **Power**, **Volume Up**, **Volume Down** |
|
||||||
|
|
||||||
|
Button layout can be customized in **[Settings](#35-settings)**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Power & Startup
|
## 2. Power & Startup
|
||||||
|
|
||||||
### Power On / Off
|
### 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.
|
the power button to trigger on a short press instead of a long one.
|
||||||
|
|
||||||
### First Launch
|
### 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
|
### 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,
|
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** screen, or **File Upload** screen.
|
**[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.
|
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.
|
and down through folders and books.
|
||||||
* **Open Selection:** Press **Confirm** to open a folder or read a selected book.
|
* **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
|
### 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.
|
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.
|
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:
|
- **Sleep Screen**: Which sleep screen to display when the device sleeps, options are:
|
||||||
- "Dark" (default) - The default dark sleep screen
|
- "Dark" (default) - The default dark sleep screen
|
||||||
- "Light" - The same default sleep screen, on a white background
|
- "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)
|
- "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,
|
- **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.
|
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.
|
- **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
|
### 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.
|
- **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
|
- **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.
|
randomly selected each time the device sleeps.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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.
|
* **Previous Chapter:** Press and **hold** the **Left** (or **Volume Up**) button briefly, then release.
|
||||||
|
|
||||||
### System Navigation
|
### System Navigation
|
||||||
* **Return to Home:** Press **Back** to close the book and return to the Book Selection screen.
|
* **Return to Book Selection:** Press **Back** to close the book and return to the **[Book Selection](#32-book-selection)** screen.
|
||||||
* **Chapter Menu:** Press **Confirm** to open the Table of Contents/Chapter 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:
|
are planned for future updates:
|
||||||
|
|
||||||
* **Images:** Embedded images in e-books will not render.
|
* **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.
|
|
||||||
|
|||||||
@ -2,8 +2,220 @@
|
|||||||
|
|
||||||
## `book.bin`
|
## `book.bin`
|
||||||
|
|
||||||

|
### 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`
|
## `section.bin`
|
||||||
|
|
||||||

|
### 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));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 539 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 296 KiB |
@ -1,6 +1,6 @@
|
|||||||
#include "EpdFontFamily.h"
|
#include "EpdFontFamily.h"
|
||||||
|
|
||||||
const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
|
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||||
if (style == BOLD && bold) {
|
if (style == BOLD && bold) {
|
||||||
return bold;
|
return bold;
|
||||||
}
|
}
|
||||||
@ -22,16 +22,16 @@ const EpdFont* EpdFontFamily::getFont(const EpdFontStyle style) const {
|
|||||||
return regular;
|
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);
|
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);
|
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);
|
return getFont(style)->getGlyph(cp);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,24 +1,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "EpdFont.h"
|
#include "EpdFont.h"
|
||||||
|
|
||||||
enum EpdFontStyle { REGULAR, BOLD, ITALIC, BOLD_ITALIC };
|
|
||||||
|
|
||||||
class EpdFontFamily {
|
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* regular;
|
||||||
const EpdFont* bold;
|
const EpdFont* bold;
|
||||||
const EpdFont* italic;
|
const EpdFont* italic;
|
||||||
const EpdFont* boldItalic;
|
const EpdFont* boldItalic;
|
||||||
|
|
||||||
const EpdFont* getFont(EpdFontStyle style) const;
|
const EpdFont* getFont(Style 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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,7 @@ void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Page::serialize(FsFile& file) const {
|
bool Page::serialize(FsFile& file) const {
|
||||||
const uint32_t count = elements.size();
|
const uint16_t count = elements.size();
|
||||||
serialization::writePod(file, count);
|
serialization::writePod(file, count);
|
||||||
|
|
||||||
for (const auto& el : elements) {
|
for (const auto& el : elements) {
|
||||||
@ -49,10 +49,10 @@ bool Page::serialize(FsFile& file) const {
|
|||||||
std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
std::unique_ptr<Page> Page::deserialize(FsFile& file) {
|
||||||
auto page = std::unique_ptr<Page>(new Page());
|
auto page = std::unique_ptr<Page>(new Page());
|
||||||
|
|
||||||
uint32_t count;
|
uint16_t count;
|
||||||
serialization::readPod(file, count);
|
serialization::readPod(file, count);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < count; i++) {
|
for (uint16_t i = 0; i < count; i++) {
|
||||||
uint8_t tag;
|
uint8_t tag;
|
||||||
serialization::readPod(file, tag);
|
serialization::readPod(file, tag);
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
constexpr int MAX_COST = std::numeric_limits<int>::max();
|
||||||
|
|
||||||
void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) {
|
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle) {
|
||||||
if (word.empty()) return;
|
if (word.empty()) return;
|
||||||
|
|
||||||
words.push_back(std::move(word));
|
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
|
// 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<void(std::shared_ptr<TextBlock>)>& processLine,
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||||
const bool includeLastLine) {
|
const bool includeLastLine) {
|
||||||
if (words.empty()) {
|
if (words.empty()) {
|
||||||
@ -188,7 +188,7 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
|||||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||||
std::list<std::string> lineWords;
|
std::list<std::string> lineWords;
|
||||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||||
std::list<EpdFontStyle> lineWordStyles;
|
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||||
|
|
||||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style));
|
||||||
|
|||||||
@ -14,8 +14,8 @@ class GfxRenderer;
|
|||||||
|
|
||||||
class ParsedText {
|
class ParsedText {
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<EpdFontStyle> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
TextBlock::BLOCK_STYLE style;
|
TextBlock::Style style;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
|
|
||||||
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
|
std::vector<size_t> computeLineBreaks(int pageWidth, int spaceWidth, const std::vector<uint16_t>& wordWidths) const;
|
||||||
@ -25,16 +25,16 @@ class ParsedText {
|
|||||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ParsedText(const TextBlock::BLOCK_STYLE style, const bool extraParagraphSpacing)
|
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing)
|
||||||
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
|
: style(style), extraParagraphSpacing(extraParagraphSpacing) {}
|
||||||
~ParsedText() = default;
|
~ParsedText() = default;
|
||||||
|
|
||||||
void addWord(std::string word, EpdFontStyle fontStyle);
|
void addWord(std::string word, EpdFontFamily::Style fontStyle);
|
||||||
void setStyle(const TextBlock::BLOCK_STYLE style) { this->style = style; }
|
void setStyle(const TextBlock::Style style) { this->style = style; }
|
||||||
TextBlock::BLOCK_STYLE getStyle() const { return style; }
|
TextBlock::Style getStyle() const { return style; }
|
||||||
size_t size() const { return words.size(); }
|
size_t size() const { return words.size(); }
|
||||||
bool isEmpty() const { return words.empty(); }
|
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<void(std::shared_ptr<TextBlock>)>& processLine,
|
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||||
bool includeLastLine = true);
|
bool includeLastLine = true);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
#include "parsers/ChapterHtmlSlimParser.h"
|
#include "parsers/ChapterHtmlSlimParser.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 7;
|
constexpr uint8_t SECTION_FILE_VERSION = 8;
|
||||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(int) +
|
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint16_t) +
|
||||||
sizeof(int) + sizeof(int) + sizeof(uint32_t);
|
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t);
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||||
@ -30,7 +30,7 @@ uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
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) {
|
if (!file) {
|
||||||
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
Serial.printf("[%lu] [SCT] File not open for writing header\n", millis());
|
||||||
return;
|
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,
|
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)) {
|
if (!SdMan.openFileForRead("SCT", filePath, file)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -66,7 +66,8 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int fileFontId, fileViewportWidth, fileViewportHeight;
|
int fileFontId;
|
||||||
|
uint16_t fileViewportWidth, fileViewportHeight;
|
||||||
float fileLineCompression;
|
float fileLineCompression;
|
||||||
bool fileExtraParagraphSpacing;
|
bool fileExtraParagraphSpacing;
|
||||||
serialization::readPod(file, fileFontId);
|
serialization::readPod(file, fileFontId);
|
||||||
@ -108,7 +109,7 @@ bool Section::clearCache() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
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<void()>& progressSetupFn,
|
const std::function<void()>& progressSetupFn,
|
||||||
const std::function<void(int)>& progressFn) {
|
const std::function<void(int)>& progressFn) {
|
||||||
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
||||||
|
|||||||
@ -14,12 +14,12 @@ class Section {
|
|||||||
std::string filePath;
|
std::string filePath;
|
||||||
FsFile file;
|
FsFile file;
|
||||||
|
|
||||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth,
|
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth,
|
||||||
int viewportHeight);
|
uint16_t viewportHeight);
|
||||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
int pageCount = 0;
|
uint16_t pageCount = 0;
|
||||||
int currentPage = 0;
|
int currentPage = 0;
|
||||||
|
|
||||||
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
explicit Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||||
@ -28,11 +28,11 @@ class Section {
|
|||||||
renderer(renderer),
|
renderer(renderer),
|
||||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||||
~Section() = default;
|
~Section() = default;
|
||||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth,
|
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth,
|
||||||
int viewportHeight);
|
uint16_t viewportHeight);
|
||||||
bool clearCache() const;
|
bool clearCache() const;
|
||||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth,
|
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth,
|
||||||
int viewportHeight, const std::function<void()>& progressSetupFn = nullptr,
|
uint16_t viewportHeight, const std::function<void()>& progressSetupFn = nullptr,
|
||||||
const std::function<void(int)>& progressFn = nullptr);
|
const std::function<void(int)>& progressFn = nullptr);
|
||||||
std::unique_ptr<Page> loadPageFromSectionFile();
|
std::unique_ptr<Page> loadPageFromSectionFile();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,7 +32,7 @@ bool TextBlock::serialize(FsFile& file) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Word data
|
// Word data
|
||||||
serialization::writePod(file, static_cast<uint32_t>(words.size()));
|
serialization::writePod(file, static_cast<uint16_t>(words.size()));
|
||||||
for (const auto& w : words) serialization::writeString(file, w);
|
for (const auto& w : words) serialization::writeString(file, w);
|
||||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||||
@ -44,11 +44,11 @@ bool TextBlock::serialize(FsFile& file) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||||
uint32_t wc;
|
uint16_t wc;
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontStyle> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
BLOCK_STYLE style;
|
Style style;
|
||||||
|
|
||||||
// Word count
|
// Word count
|
||||||
serialization::readPod(file, wc);
|
serialization::readPod(file, wc);
|
||||||
|
|||||||
@ -8,10 +8,10 @@
|
|||||||
|
|
||||||
#include "Block.h"
|
#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 {
|
class TextBlock final : public Block {
|
||||||
public:
|
public:
|
||||||
enum BLOCK_STYLE : uint8_t {
|
enum Style : uint8_t {
|
||||||
JUSTIFIED = 0,
|
JUSTIFIED = 0,
|
||||||
LEFT_ALIGN = 1,
|
LEFT_ALIGN = 1,
|
||||||
CENTER_ALIGN = 2,
|
CENTER_ALIGN = 2,
|
||||||
@ -21,16 +21,16 @@ class TextBlock final : public Block {
|
|||||||
private:
|
private:
|
||||||
std::list<std::string> words;
|
std::list<std::string> words;
|
||||||
std::list<uint16_t> wordXpos;
|
std::list<uint16_t> wordXpos;
|
||||||
std::list<EpdFontStyle> wordStyles;
|
std::list<EpdFontFamily::Style> wordStyles;
|
||||||
BLOCK_STYLE style;
|
Style style;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos, std::list<EpdFontStyle> word_styles,
|
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||||
const BLOCK_STYLE style)
|
std::list<EpdFontFamily::Style> word_styles, const Style style)
|
||||||
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
: words(std::move(words)), wordXpos(std::move(word_xpos)), wordStyles(std::move(word_styles)), style(style) {}
|
||||||
~TextBlock() override = default;
|
~TextBlock() override = default;
|
||||||
void setStyle(const BLOCK_STYLE style) { this->style = style; }
|
void setStyle(const Style style) { this->style = style; }
|
||||||
BLOCK_STYLE getStyle() const { return style; }
|
Style getStyle() const { return style; }
|
||||||
bool isEmpty() override { return words.empty(); }
|
bool isEmpty() override { return words.empty(); }
|
||||||
void layout(GfxRenderer& renderer) override {};
|
void layout(GfxRenderer& renderer) override {};
|
||||||
// given a renderer works out where to break the words into lines
|
// given a renderer works out where to break the words into lines
|
||||||
|
|||||||
@ -42,7 +42,7 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start a new text block if needed
|
// start a new text block if needed
|
||||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::BLOCK_STYLE style) {
|
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
||||||
if (currentTextBlock) {
|
if (currentTextBlock) {
|
||||||
// already have a text block running and it is empty - just reuse it
|
// already have a text block running and it is empty - just reuse it
|
||||||
if (currentTextBlock->isEmpty()) {
|
if (currentTextBlock->isEmpty()) {
|
||||||
@ -116,13 +116,13 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EpdFontStyle fontStyle = REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||||
fontStyle = BOLD_ITALIC;
|
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||||
} else if (self->boldUntilDepth < self->depth) {
|
} else if (self->boldUntilDepth < self->depth) {
|
||||||
fontStyle = BOLD;
|
fontStyle = EpdFontFamily::BOLD;
|
||||||
} else if (self->italicUntilDepth < self->depth) {
|
} else if (self->italicUntilDepth < self->depth) {
|
||||||
fontStyle = ITALIC;
|
fontStyle = EpdFontFamily::ITALIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < len; i++) {
|
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;
|
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
|
||||||
|
|
||||||
if (shouldBreakText) {
|
if (shouldBreakText) {
|
||||||
EpdFontStyle fontStyle = REGULAR;
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||||
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) {
|
||||||
fontStyle = BOLD_ITALIC;
|
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||||
} else if (self->boldUntilDepth < self->depth) {
|
} else if (self->boldUntilDepth < self->depth) {
|
||||||
fontStyle = BOLD;
|
fontStyle = EpdFontFamily::BOLD;
|
||||||
} else if (self->italicUntilDepth < self->depth) {
|
} else if (self->italicUntilDepth < self->depth) {
|
||||||
fontStyle = ITALIC;
|
fontStyle = EpdFontFamily::ITALIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
self->partWordBuffer[self->partWordBufferIndex] = '\0';
|
||||||
|
|||||||
@ -33,10 +33,10 @@ class ChapterHtmlSlimParser {
|
|||||||
int fontId;
|
int fontId;
|
||||||
float lineCompression;
|
float lineCompression;
|
||||||
bool extraParagraphSpacing;
|
bool extraParagraphSpacing;
|
||||||
int viewportWidth;
|
uint16_t viewportWidth;
|
||||||
int viewportHeight;
|
uint16_t viewportHeight;
|
||||||
|
|
||||||
void startNewTextBlock(TextBlock::BLOCK_STYLE style);
|
void startNewTextBlock(TextBlock::Style style);
|
||||||
void makePages();
|
void makePages();
|
||||||
// XML callbacks
|
// XML callbacks
|
||||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
@ -45,8 +45,8 @@ class ChapterHtmlSlimParser {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId,
|
||||||
const float lineCompression, const bool extraParagraphSpacing, const int viewportWidth,
|
const float lineCompression, const bool extraParagraphSpacing,
|
||||||
const int viewportHeight,
|
const uint16_t viewportWidth, const uint16_t viewportHeight,
|
||||||
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
const std::function<void(std::unique_ptr<Page>)>& completePageFn,
|
||||||
const std::function<void(int)>& progressFn = nullptr)
|
const std::function<void(int)>& progressFn = nullptr)
|
||||||
: filepath(filepath),
|
: filepath(filepath),
|
||||||
|
|||||||
@ -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) {
|
if (fontMap.count(fontId) == 0) {
|
||||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||||
return 0;
|
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,
|
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;
|
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
|
||||||
drawText(fontId, x, y, text, black, style);
|
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,
|
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);
|
const int yPos = y + getFontAscenderSize(fontId);
|
||||||
int xpos = x;
|
int xpos = x;
|
||||||
|
|
||||||
@ -238,6 +238,17 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons
|
|||||||
einkDisplay.displayBuffer(refreshMode);
|
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
|
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
||||||
int GfxRenderer::getScreenWidth() const {
|
int GfxRenderer::getScreenWidth() const {
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
@ -273,7 +284,7 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getGlyph(' ', REGULAR)->advanceX;
|
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||||
@ -282,7 +293,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fontMap.at(fontId).getData(REGULAR)->ascender;
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||||
@ -291,7 +302,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
|||||||
return 0;
|
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,
|
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,
|
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);
|
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
||||||
if (!glyph) {
|
if (!glyph) {
|
||||||
// TODO: Replace with fallback glyph property?
|
// TODO: Replace with fallback glyph property?
|
||||||
|
|||||||
@ -31,7 +31,7 @@ class GfxRenderer {
|
|||||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||||
std::map<int, EpdFontFamily> fontMap;
|
std::map<int, EpdFontFamily> fontMap;
|
||||||
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
|
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 freeBwBufferChunks();
|
||||||
void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
|
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;
|
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||||
|
|
||||||
// Text
|
// Text
|
||||||
int getTextWidth(int fontId, const char* text, 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, EpdFontStyle style = REGULAR) const;
|
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
|
||||||
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
|
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 getSpaceWidth(int fontId) const;
|
||||||
int getFontAscenderSize(int fontId) const;
|
int getFontAscenderSize(int fontId) const;
|
||||||
int getLineHeight(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
|
// UI Components
|
||||||
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
|
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[platformio]
|
[platformio]
|
||||||
crosspoint_version = 0.11.1
|
crosspoint_version = 0.11.2
|
||||||
default_envs = default
|
default_envs = default
|
||||||
|
|
||||||
[base]
|
[base]
|
||||||
@ -26,6 +26,8 @@ build_flags =
|
|||||||
-DXML_GE=0
|
-DXML_GE=0
|
||||||
-DXML_CONTEXT_BYTES=1024
|
-DXML_CONTEXT_BYTES=1024
|
||||||
-std=c++2a
|
-std=c++2a
|
||||||
|
# Enable UTF-8 long file names in SdFat
|
||||||
|
-DUSE_UTF8_LONG_NAMES=1
|
||||||
|
|
||||||
; Board configuration
|
; Board configuration
|
||||||
board_build.flash_mode = dio
|
board_build.flash_mode = dio
|
||||||
|
|||||||
@ -13,7 +13,7 @@ void BootActivity::onEnter() {
|
|||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
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 / 2 + 95, "BOOTING");
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
|
|||||||
@ -127,7 +127,7 @@ void SleepActivity::renderDefaultSleepScreen() const {
|
|||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128);
|
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");
|
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
|
||||||
|
|
||||||
// Make sleep screen dark unless light is selected in settings
|
// Make sleep screen dark unless light is selected in settings
|
||||||
|
|||||||
@ -334,7 +334,7 @@ void CrossPointWebServerActivity::render() const {
|
|||||||
} else if (state == WebServerActivityState::AP_STARTING) {
|
} else if (state == WebServerActivityState::AP_STARTING) {
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
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();
|
renderer.displayBuffer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -365,13 +365,13 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
|||||||
// Use consistent line spacing
|
// Use consistent line spacing
|
||||||
constexpr int LINE_SPACING = 28; // Space between lines
|
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) {
|
if (isApMode) {
|
||||||
// AP mode display - center the content block
|
// AP mode display - center the content block
|
||||||
int startY = 55;
|
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;
|
std::string ssidInfo = "Network: " + connectedSSID;
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str());
|
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;
|
startY += 6 * 29 + 3 * LINE_SPACING;
|
||||||
// Show primary URL (hostname)
|
// Show primary URL (hostname)
|
||||||
std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/";
|
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
|
// Show IP address as fallback
|
||||||
std::string ipUrl = "or http://" + connectedIP + "/";
|
std::string ipUrl = "or http://" + connectedIP + "/";
|
||||||
@ -412,7 +412,7 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
|||||||
|
|
||||||
// Show web server URL prominently
|
// Show web server URL prominently
|
||||||
std::string webInfo = "http://" + connectedIP + "/";
|
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
|
// Also show hostname URL
|
||||||
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/";
|
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/";
|
||||||
|
|||||||
@ -97,7 +97,7 @@ void NetworkModeSelectionActivity::render() const {
|
|||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Draw header
|
// 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
|
// Draw subtitle
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?");
|
renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?");
|
||||||
|
|||||||
@ -496,7 +496,7 @@ void WifiSelectionActivity::renderNetworkList() const {
|
|||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Draw header
|
// 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()) {
|
if (networks.empty()) {
|
||||||
// No networks found or scan failed
|
// No networks found or scan failed
|
||||||
@ -577,7 +577,7 @@ void WifiSelectionActivity::renderConnecting() const {
|
|||||||
if (state == WifiSelectionState::SCANNING) {
|
if (state == WifiSelectionState::SCANNING) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, top, "Scanning...");
|
renderer.drawCenteredText(UI_10_FONT_ID, top, "Scanning...");
|
||||||
} else {
|
} 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;
|
std::string ssidInfo = "to " + selectedSSID;
|
||||||
if (ssidInfo.length() > 25) {
|
if (ssidInfo.length() > 25) {
|
||||||
@ -592,7 +592,7 @@ void WifiSelectionActivity::renderConnected() const {
|
|||||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const auto top = (pageHeight - height * 4) / 2;
|
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;
|
std::string ssidInfo = "Network: " + selectedSSID;
|
||||||
if (ssidInfo.length() > 28) {
|
if (ssidInfo.length() > 28) {
|
||||||
@ -612,7 +612,7 @@ void WifiSelectionActivity::renderSavePrompt() const {
|
|||||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const auto top = (pageHeight - height * 3) / 2;
|
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;
|
std::string ssidInfo = "Network: " + selectedSSID;
|
||||||
if (ssidInfo.length() > 28) {
|
if (ssidInfo.length() > 28) {
|
||||||
@ -651,7 +651,7 @@ void WifiSelectionActivity::renderConnectionFailed() const {
|
|||||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const auto top = (pageHeight - height * 2) / 2;
|
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(UI_10_FONT_ID, top + 20, connectionError.c_str());
|
||||||
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue");
|
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 height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const auto top = (pageHeight - height * 3) / 2;
|
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;
|
std::string ssidInfo = "Network: " + selectedSSID;
|
||||||
if (ssidInfo.length() > 28) {
|
if (ssidInfo.length() > 28) {
|
||||||
|
|||||||
@ -244,7 +244,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
// Show end of book screen
|
// Show end of book screen
|
||||||
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
||||||
renderer.clearScreen();
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -263,8 +263,8 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
||||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||||
|
|
||||||
const auto viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
||||||
const auto viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
|
||||||
|
|
||||||
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
|
||||||
SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight)) {
|
SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight)) {
|
||||||
@ -332,7 +332,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
|
|
||||||
if (section->pageCount == 0) {
|
if (section->pageCount == 0) {
|
||||||
Serial.printf("[%lu] [ERS] No pages to render\n", millis());
|
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);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
@ -340,7 +340,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
|
|
||||||
if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
|
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);
|
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);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -120,7 +120,10 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
|
|||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int pageItems = getPageItems();
|
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;
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
|
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
|
||||||
|
|||||||
@ -30,11 +30,19 @@ void FileSelectionActivity::taskTrampoline(void* param) {
|
|||||||
void FileSelectionActivity::loadFiles() {
|
void FileSelectionActivity::loadFiles() {
|
||||||
files.clear();
|
files.clear();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
|
||||||
auto root = SdMan.open(basepath.c_str());
|
auto root = SdMan.open(basepath.c_str());
|
||||||
|
if (!root || !root.isDirectory()) {
|
||||||
|
if (root) root.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.rewindDirectory();
|
||||||
|
|
||||||
char name[128];
|
char name[128];
|
||||||
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
||||||
file.getName(name, sizeof(name));
|
file.getName(name, sizeof(name));
|
||||||
if (name[0] == '.') {
|
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
|
||||||
file.close();
|
file.close();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -165,7 +173,7 @@ void FileSelectionActivity::render() const {
|
|||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
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
|
// Help text
|
||||||
const auto labels = mappedInput.mapLabels("« Home", "Open", "", "");
|
const auto labels = mappedInput.mapLabels("« Home", "Open", "", "");
|
||||||
@ -180,12 +188,7 @@ void FileSelectionActivity::render() const {
|
|||||||
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
|
||||||
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
|
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
|
||||||
for (int i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) {
|
for (int i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) {
|
||||||
auto item = files[i];
|
auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40);
|
||||||
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());
|
|
||||||
}
|
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
|
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -68,8 +68,8 @@ void ReaderActivity::onSelectBookFile(const std::string& path) {
|
|||||||
onGoToXtcReader(std::move(xtc));
|
onGoToXtcReader(std::move(xtc));
|
||||||
} else {
|
} else {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC", REGULAR,
|
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC",
|
||||||
EInkDisplay::HALF_REFRESH));
|
EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH));
|
||||||
delay(2000);
|
delay(2000);
|
||||||
onGoToFileSelection();
|
onGoToFileSelection();
|
||||||
}
|
}
|
||||||
@ -80,8 +80,8 @@ void ReaderActivity::onSelectBookFile(const std::string& path) {
|
|||||||
onGoToEpubReader(std::move(epub));
|
onGoToEpubReader(std::move(epub));
|
||||||
} else {
|
} else {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub", REGULAR,
|
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub",
|
||||||
EInkDisplay::HALF_REFRESH));
|
EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH));
|
||||||
delay(2000);
|
delay(2000);
|
||||||
onGoToFileSelection();
|
onGoToFileSelection();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,7 +165,7 @@ void XtcReaderActivity::renderScreen() {
|
|||||||
if (currentPage >= xtc->getPageCount()) {
|
if (currentPage >= xtc->getPageCount()) {
|
||||||
// Show end of book screen
|
// Show end of book screen
|
||||||
renderer.clearScreen();
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -194,7 +194,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
if (!pageBuffer) {
|
if (!pageBuffer) {
|
||||||
Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize);
|
Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize);
|
||||||
renderer.clearScreen();
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -205,7 +205,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage);
|
Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage);
|
||||||
free(pageBuffer);
|
free(pageBuffer);
|
||||||
renderer.clearScreen();
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,7 +130,7 @@ void XtcReaderChapterSelectionActivity::renderScreen() {
|
|||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int pageItems = getPageItems();
|
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();
|
const auto& chapters = xtc->getChapters();
|
||||||
if (chapters.empty()) {
|
if (chapters.empty()) {
|
||||||
|
|||||||
@ -124,36 +124,30 @@ void OtaUpdateActivity::render() {
|
|||||||
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
|
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
|
||||||
renderer.clearScreen();
|
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) {
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == WAITING_CONFIRMATION) {
|
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, 250, "Current Version: " CROSSPOINT_VERSION);
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str());
|
renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str());
|
||||||
|
|
||||||
renderer.drawRect(25, pageHeight - 40, 106, 40);
|
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", "");
|
||||||
renderer.drawText(UI_10_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_10_FONT_ID, "Cancel")) / 2, pageHeight - 35,
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
"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");
|
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == UPDATE_IN_PROGRESS) {
|
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.drawRect(20, 350, pageWidth - 40, 50);
|
||||||
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
|
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 420,
|
renderer.drawCenteredText(UI_10_FONT_ID, 420,
|
||||||
@ -166,19 +160,19 @@ void OtaUpdateActivity::render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state == NO_UPDATE) {
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == FAILED) {
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == FINISHED) {
|
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.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on");
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
state = SHUTTING_DOWN;
|
state = SHUTTING_DOWN;
|
||||||
|
|||||||
@ -163,7 +163,7 @@ void SettingsActivity::render() const {
|
|||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Draw header
|
// 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
|
// Draw selection
|
||||||
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
||||||
|
|||||||
@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
class FullScreenMessageActivity final : public Activity {
|
class FullScreenMessageActivity final : public Activity {
|
||||||
std::string text;
|
std::string text;
|
||||||
EpdFontStyle style;
|
EpdFontFamily::Style style;
|
||||||
EInkDisplay::RefreshMode refreshMode;
|
EInkDisplay::RefreshMode refreshMode;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text,
|
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)
|
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
|
||||||
: Activity("FullScreenMessage", renderer, mappedInput),
|
: Activity("FullScreenMessage", renderer, mappedInput),
|
||||||
text(std::move(text)),
|
text(std::move(text)),
|
||||||
|
|||||||
@ -151,8 +151,8 @@ void verifyWakeupLongPress() {
|
|||||||
const auto start = millis();
|
const auto start = millis();
|
||||||
bool abort = false;
|
bool abort = false;
|
||||||
// It takes us some time to wake up from deep sleep, so we need to subtract that from the duration
|
// It takes us some time to wake up from deep sleep, so we need to subtract that from the duration
|
||||||
uint16_t calibration = 29;
|
constexpr uint16_t calibration = 29;
|
||||||
uint16_t calibratedPressDuration =
|
const uint16_t calibratedPressDuration =
|
||||||
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
|
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
|
||||||
|
|
||||||
inputManager.update();
|
inputManager.update();
|
||||||
@ -271,7 +271,7 @@ void setup() {
|
|||||||
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
|
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
|
||||||
setupDisplayAndFonts();
|
setupDisplayAndFonts();
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", BOLD));
|
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user