Files
crosspoint-reader-mod/src/activities/boot_sleep/SleepActivity.cpp

281 lines
10 KiB
C++
Raw Normal View History

#include "SleepActivity.h"
2025-12-06 04:20:03 +11:00
#include <Epub.h>
#include <GfxRenderer.h>
#include <SDCardManager.h>
Add TXT file reader support (#240) ## Summary * **What is the goal of this PR?** Add support for reading plain text (.txt) files, enabling users to browse, read, and track progress in TXT documents alongside existing EPUB and XTC formats. * **What changes are included?** - New Txt library for loading and parsing plain text files - New TxtReaderActivity with streaming page rendering using 8KB chunks to handle large files without memory issues on ESP32-C3 - Page index caching system (index.bin) for instant re-open after sleep or app restart - Progress bar UI during initial file indexing (matching EPUB style) - Word wrapping with proper UTF-8 support - Cover image support for TXT files: - Primary: image with same filename as TXT (e.g., book.jpg for book.txt) - Fallback: cover.bmp/jpg/jpeg in the same folder - JPG to BMP conversion using existing converter - Sleep screen cover mode now works with TXT files - File browser now shows .txt files ## Additional Context * Add any other information that might be helpful for the reviewer * Memory constraints: The streaming approach was necessary because ESP32-C3 only has 320KB RAM. A 700KB TXT file cannot be loaded entirely into memory, so we read 8KB chunks and build a page offset index instead. * Cache invalidation: The page index cache automatically invalidates when file size, viewport width, or lines per page changes (e.g., font size or orientation change). * Performance: First open requires indexing (with progress bar), subsequent opens load from cache instantly. * Cover image format: PNG is detected but not supported for conversion (no PNG decoder available). Only BMP and JPG/JPEG work.
2026-01-14 19:36:40 +09:00
#include <Txt.h>
#include <Xtc.h>
2025-12-06 04:20:03 +11:00
2025-12-15 23:17:23 +11:00
#include "CrossPointSettings.h"
#include "CrossPointState.h"
feat: UI themes, Lyra (#528) ## Summary ### What is the goal of this PR? - Visual UI overhaul - UI theme selection ### What changes are included? - Added a setting "UI Theme": Classic, Lyra - The classic theme is the current Crosspoint theme - The Lyra theme implements these mockups: https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0 by Discord users yagofarias, ruby and gan_shu - New functions in GFXRenderer to render rounded rectangles, greyscale fills (using dithering) and thick lines - Basic UI components are factored into BaseTheme methods which can be overridden by each additional theme. Methods that are not overridden will fallback to BaseTheme behavior. This means any new features/components in CrossPoint only need to be developed for the "Classic" BaseTheme. - Additional themes can easily be developed by the community using this foundation ![IMG_7649 Medium](https://github.com/user-attachments/assets/b516f5a9-2636-4565-acff-91a25b93b39b) ![IMG_7746 Medium](https://github.com/user-attachments/assets/def41810-ab6e-4952-b40f-b9ce7d62bea8) ![IMG_7651 Medium](https://github.com/user-attachments/assets/518a9a6d-107a-4be3-9533-43a2b64b944b) ## Additional Context - Only the Home, Library and main Settings screens have been implemented so far, this will be extended to the transfer screens and chapter selection screen later on, but we need to get the ball rolling somehow :) - Loading extra covers on the home screen in the Lyra theme takes a little more time (about 2 seconds), I added a loading bar popup (reusing the Indexing progress bar from the reader view, factored into a neat UI component) but the popup adds ~400ms to the loading time. - ~~Home screen thumbnails will need to be generated separately for each theme, because they are displayed in different sizes. Because we're using dithering, displaying a thumb with the wrong size causes the picture to look janky or dark as it does on the screenshots above. No worries this will be fixed in a future PR.~~ Thumbs are now generated with a size parameter - UI Icons will need to be implemented in a future PR. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ This is not a vibe coded PR. Copilot was used for autocompletion to save time but I reviewed, understood and edited all generated code. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
#include "components/UITheme.h"
Aleo, Noto Sans, Open Dyslexic fonts (#163) ## Summary * Swap out Bookerly font due to licensing issues, replace default font with Aleo * I did a bunch of searching around for a nice replacement font, and this trumped several other like Literata, Merriwether, Vollkorn, etc * Add Noto Sans, and Open Dyslexic as font options * They can be selected in the settings screen * Add font size options (Small, Medium, Large, Extra Large) * Adjustable in settings * Swap out uses of reader font in headings and replaced with slightly larger Ubuntu font * Replaced PixelArial14 font as it was difficult to track down, replace with Space Grotesk * Remove auto formatting on generated font files * Massively speeds up formatting step now that there is a lot more CPP font source * Include fonts with their licenses in the repo ## Additional Context Line compression setting will follow | Font | Small | Medium | Large | X Large | | --- | --- | --- | --- | --- | | Aleo | ![IMG_5704](https://github.com/user-attachments/assets/7acb054f-ddef-4080-b3c8-590cfaf13115) | ![IMG_5705](https://github.com/user-attachments/assets/d4819036-5c89-486e-92c3-86094fa4d89a) | ![IMG_5706](https://github.com/user-attachments/assets/35caf622-d126-4396-9c3e-f927eba1e1f4) | ![IMG_5707](https://github.com/user-attachments/assets/af32370a-6244-400f-bea9-5c27db040b5b) | | Noto Sans | ![IMG_5708](https://github.com/user-attachments/assets/1f9264a5-c069-4e22-9099-a082bfcaabc5) | ![IMG_5709](https://github.com/user-attachments/assets/ef6b07fe-8d87-403a-b152-05f50b69b78e) | ![IMG_5710](https://github.com/user-attachments/assets/112a5d20-262c-4dc0-b67d-980b237e4607) | ![IMG_5711](https://github.com/user-attachments/assets/d25e0e1d-2ace-450d-96dd-618e4efd4805) | | Open Dyslexic | ![IMG_5712](https://github.com/user-attachments/assets/ead64690-f261-4fae-a4a2-0becd1162e2d) | ![IMG_5713](https://github.com/user-attachments/assets/59d60f7d-5142-4591-96b0-c04e0a4c6436) | ![IMG_5714](https://github.com/user-attachments/assets/bb6652cd-1790-46a3-93ea-2b8f70d0d36d) | ![IMG_5715](https://github.com/user-attachments/assets/496e7eb4-c81a-4232-83e9-9ba9148fdea4) |
2025-12-30 18:21:47 +10:00
#include "fontIds.h"
#include "images/CrossLarge.h"
#include "util/StringUtils.h"
void SleepActivity::onEnter() {
Activity::onEnter();
feat: UI themes, Lyra (#528) ## Summary ### What is the goal of this PR? - Visual UI overhaul - UI theme selection ### What changes are included? - Added a setting "UI Theme": Classic, Lyra - The classic theme is the current Crosspoint theme - The Lyra theme implements these mockups: https://www.figma.com/design/UhxoV4DgUnfrDQgMPPTXog/Lyra-Theme?node-id=2003-7596&t=4CSOZqf0n9uQMxDt-0 by Discord users yagofarias, ruby and gan_shu - New functions in GFXRenderer to render rounded rectangles, greyscale fills (using dithering) and thick lines - Basic UI components are factored into BaseTheme methods which can be overridden by each additional theme. Methods that are not overridden will fallback to BaseTheme behavior. This means any new features/components in CrossPoint only need to be developed for the "Classic" BaseTheme. - Additional themes can easily be developed by the community using this foundation ![IMG_7649 Medium](https://github.com/user-attachments/assets/b516f5a9-2636-4565-acff-91a25b93b39b) ![IMG_7746 Medium](https://github.com/user-attachments/assets/def41810-ab6e-4952-b40f-b9ce7d62bea8) ![IMG_7651 Medium](https://github.com/user-attachments/assets/518a9a6d-107a-4be3-9533-43a2b64b944b) ## Additional Context - Only the Home, Library and main Settings screens have been implemented so far, this will be extended to the transfer screens and chapter selection screen later on, but we need to get the ball rolling somehow :) - Loading extra covers on the home screen in the Lyra theme takes a little more time (about 2 seconds), I added a loading bar popup (reusing the Indexing progress bar from the reader view, factored into a neat UI component) but the popup adds ~400ms to the loading time. - ~~Home screen thumbnails will need to be generated separately for each theme, because they are displayed in different sizes. Because we're using dithering, displaying a thumb with the wrong size causes the picture to look janky or dark as it does on the screenshots above. No worries this will be fixed in a future PR.~~ Thumbs are now generated with a size parameter - UI Icons will need to be implemented in a future PR. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ This is not a vibe coded PR. Copilot was used for autocompletion to save time but I reviewed, understood and edited all generated code. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2026-02-05 17:50:11 +07:00
GUI.drawPopup(renderer, "Entering Sleep...");
switch (SETTINGS.sleepScreen) {
case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK):
return renderBlankSleepScreen();
case (CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM):
return renderCustomSleepScreen();
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER):
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM):
return renderCoverSleepScreen();
default:
return renderDefaultSleepScreen();
}
}
void SleepActivity::renderCustomSleepScreen() const {
// Check if we have a /sleep directory
auto dir = SdMan.open("/sleep");
if (dir && dir.isDirectory()) {
std::vector<std::string> files;
char name[500];
// collect all valid BMP files
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
if (file.isDirectory()) {
file.close();
continue;
}
file.getName(name, sizeof(name));
auto filename = std::string(name);
if (filename[0] == '.') {
file.close();
continue;
}
if (filename.substr(filename.length() - 4) != ".bmp") {
Serial.printf("[%lu] [SLP] Skipping non-.bmp file name: %s\n", millis(), name);
file.close();
continue;
}
Bitmap bitmap(file);
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
Serial.printf("[%lu] [SLP] Skipping invalid BMP file: %s\n", millis(), name);
file.close();
continue;
}
files.emplace_back(filename);
file.close();
}
const auto numFiles = files.size();
if (numFiles > 0) {
// Generate a random number between 1 and numFiles
auto randomFileIndex = random(numFiles);
// If we picked the same image as last time, reroll
while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) {
randomFileIndex = random(numFiles);
}
APP_STATE.lastSleepImage = randomFileIndex;
APP_STATE.saveToFile();
const auto filename = "/sleep/" + files[randomFileIndex];
FsFile file;
if (SdMan.openFileForRead("SLP", filename, file)) {
Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str());
delay(100);
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
renderBitmapSleepScreen(bitmap);
dir.close();
return;
}
}
}
}
if (dir) dir.close();
// Look for sleep.bmp on the root of the sd card to determine if we should
// render a custom sleep screen instead of the default.
FsFile file;
if (SdMan.openFileForRead("SLP", "/sleep.bmp", file)) {
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis());
renderBitmapSleepScreen(bitmap);
return;
}
}
renderDefaultSleepScreen();
}
void SleepActivity::renderDefaultSleepScreen() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
fix: rotate origin in drawImage (#557) ## Summary This was originally a comment in #499, but I'm making it its own PR, because it doesn't depend on anything there and then I can base that PR on this one. Currently, `drawBitmap` is used for covers and sleep wallpaper, and `drawImage` is used for the boot logo. `drawBitmap` goes row by row and pixel by pixel, so it respects the renderer orientation. `drawImage` just calls the `EInkDisplay`'s `drawImage`, which works in the eink panel's native display orientation. `drawImage` rotates the x,y coordinates where it's going to draw the image, but doesn't account for the fact that the northwest corner in portrait orientation becomes, the southwest corner of the image rectangle in the native orientation. The boot and sleep activities currently work around this by calculating the north*east* corner of where the image should go, which becomes the northwest corner after `rotateCoordinates`. I think this wasn't really apparent because the CrossPoint logo is rotationally symmetrical. The `EInkDisplay` `drawImage` always draws the image in native orientation, but that looks the same for the "X" image. If we rotate the origin coordinate in `GfxRenderer`'s `drawImage`, we can use a much clearer northwest corner coordinate in the boot and sleep activities. (And then, in #499, we can actually rotate the boot screen to the user's preferred orientation). This does *not* yet rotate the actual bits in the image; it's still displayed in native orientation. This doesn't affect the rotationally-symmetric logo, but if it's ever changed, we will probably want to allocate a new `u8int[]` and transpose rows and columns if necessary. ## Additional Context I've created an additional branch on top of this to demonstrate by replacing the logo with a non-rotationally-symmetrical image: <img width="128" height="128" alt="Cat-in-a-pan-128-bw" src="https://github.com/user-attachments/assets/d0b239bc-fe75-4ec8-bc02-9cf9436ca65f" /> https://github.com/crosspoint-reader/crosspoint-reader/compare/master...maeveynot:rotated-cat (many thanks to https://notisrac.github.io/FileToCArray/) As you can see, it is always drawn in native orientation, which makes it sideways (turned clockwise) in portrait. --- ### AI Usage No Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-27 05:59:41 -06:00
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
2025-12-08 22:52:19 +11:00
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
// Make sleep screen dark unless light is selected in settings
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) {
renderer.invertScreen();
}
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const {
int x, y;
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
float cropX = 0, cropY = 0;
Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
pageWidth, pageHeight);
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
// image will scale, make sure placement is right
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), ratio, screenRatio);
if (ratio > screenRatio) {
// image wider than viewport ratio, scaled down image needs to be centered vertically
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
cropX = 1.0f - (screenRatio / ratio);
Serial.printf("[%lu] [SLP] Cropping bitmap x: %f\n", millis(), cropX);
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
}
x = 0;
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
Serial.printf("[%lu] [SLP] Centering with ratio %f to y=%d\n", millis(), ratio, y);
} else {
// image taller than viewport ratio, scaled down image needs to be centered horizontally
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
cropY = 1.0f - (ratio / screenRatio);
Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY);
ratio = static_cast<float>(bitmap.getWidth()) / ((1.0f - cropY) * static_cast<float>(bitmap.getHeight()));
}
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
y = 0;
Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x);
}
} else {
// center the image
x = (pageWidth - bitmap.getWidth()) / 2;
y = (pageHeight - bitmap.getHeight()) / 2;
}
Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y);
renderer.clearScreen();
feat: Add support to B&W filters to image covers (#476) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implementation of a new feature in Display options as Image Filter * **What changes are included?** Black & White and Inverted Black & White options are added. ## Additional Context Here are some examples: | None | Contrast | Inverted | | --- | --- | --- | | <img alt="image" src="https://github.com/user-attachments/assets/fe02dd9b-f647-41bd-8495-c262f73177c4" /> | <img alt="image" src="https://github.com/user-attachments/assets/2d17747d-3ff6-48a9-b9b9-eb17cccf19cf" /> | <img alt="image" src="https://github.com/user-attachments/assets/792dea50-f003-4634-83fe-77849ca49095" /> | | <img alt="image" src="https://github.com/user-attachments/assets/28395b63-14f8-41e2-886b-8ddf3faeafc4" /> | <img alt="image" src="https://github.com/user-attachments/assets/71a569c8-fc54-4647-ad4c-ec96e220cddb" /> | <img alt="image" src="https://github.com/user-attachments/assets/9139e32c-9175-433e-8372-45fa042d3dc9" /> | * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). I have also tried adding Color inversion, but could not see much difference with that. It might be because my implementation was wrong. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** PARTIALLY **_ --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 13:21:59 +00:00
const bool hasGreyscale = bitmap.hasGreyscale() &&
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
feat: Add support to B&W filters to image covers (#476) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implementation of a new feature in Display options as Image Filter * **What changes are included?** Black & White and Inverted Black & White options are added. ## Additional Context Here are some examples: | None | Contrast | Inverted | | --- | --- | --- | | <img alt="image" src="https://github.com/user-attachments/assets/fe02dd9b-f647-41bd-8495-c262f73177c4" /> | <img alt="image" src="https://github.com/user-attachments/assets/2d17747d-3ff6-48a9-b9b9-eb17cccf19cf" /> | <img alt="image" src="https://github.com/user-attachments/assets/792dea50-f003-4634-83fe-77849ca49095" /> | | <img alt="image" src="https://github.com/user-attachments/assets/28395b63-14f8-41e2-886b-8ddf3faeafc4" /> | <img alt="image" src="https://github.com/user-attachments/assets/71a569c8-fc54-4647-ad4c-ec96e220cddb" /> | <img alt="image" src="https://github.com/user-attachments/assets/9139e32c-9175-433e-8372-45fa042d3dc9" /> | * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). I have also tried adding Color inversion, but could not see much difference with that. It might be because my implementation was wrong. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** PARTIALLY **_ --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 13:21:59 +00:00
if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) {
renderer.invertScreen();
}
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
feat: Add support to B&W filters to image covers (#476) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implementation of a new feature in Display options as Image Filter * **What changes are included?** Black & White and Inverted Black & White options are added. ## Additional Context Here are some examples: | None | Contrast | Inverted | | --- | --- | --- | | <img alt="image" src="https://github.com/user-attachments/assets/fe02dd9b-f647-41bd-8495-c262f73177c4" /> | <img alt="image" src="https://github.com/user-attachments/assets/2d17747d-3ff6-48a9-b9b9-eb17cccf19cf" /> | <img alt="image" src="https://github.com/user-attachments/assets/792dea50-f003-4634-83fe-77849ca49095" /> | | <img alt="image" src="https://github.com/user-attachments/assets/28395b63-14f8-41e2-886b-8ddf3faeafc4" /> | <img alt="image" src="https://github.com/user-attachments/assets/71a569c8-fc54-4647-ad4c-ec96e220cddb" /> | <img alt="image" src="https://github.com/user-attachments/assets/9139e32c-9175-433e-8372-45fa042d3dc9" /> | * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). I have also tried adding Color inversion, but could not see much difference with that. It might be because my implementation was wrong. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** PARTIALLY **_ --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 13:21:59 +00:00
if (hasGreyscale) {
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.copyGrayscaleLsbBuffers();
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer();
renderer.setRenderMode(GfxRenderer::BW);
}
}
void SleepActivity::renderCoverSleepScreen() const {
void (SleepActivity::*renderNoCoverSleepScreen)() const;
switch (SETTINGS.sleepScreen) {
case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM):
renderNoCoverSleepScreen = &SleepActivity::renderCustomSleepScreen;
break;
default:
renderNoCoverSleepScreen = &SleepActivity::renderDefaultSleepScreen;
break;
}
if (APP_STATE.openEpubPath.empty()) {
return (this->*renderNoCoverSleepScreen)();
}
std::string coverBmpPath;
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
Add TXT file reader support (#240) ## Summary * **What is the goal of this PR?** Add support for reading plain text (.txt) files, enabling users to browse, read, and track progress in TXT documents alongside existing EPUB and XTC formats. * **What changes are included?** - New Txt library for loading and parsing plain text files - New TxtReaderActivity with streaming page rendering using 8KB chunks to handle large files without memory issues on ESP32-C3 - Page index caching system (index.bin) for instant re-open after sleep or app restart - Progress bar UI during initial file indexing (matching EPUB style) - Word wrapping with proper UTF-8 support - Cover image support for TXT files: - Primary: image with same filename as TXT (e.g., book.jpg for book.txt) - Fallback: cover.bmp/jpg/jpeg in the same folder - JPG to BMP conversion using existing converter - Sleep screen cover mode now works with TXT files - File browser now shows .txt files ## Additional Context * Add any other information that might be helpful for the reviewer * Memory constraints: The streaming approach was necessary because ESP32-C3 only has 320KB RAM. A 700KB TXT file cannot be loaded entirely into memory, so we read 8KB chunks and build a page offset index instead. * Cache invalidation: The page index cache automatically invalidates when file size, viewport width, or lines per page changes (e.g., font size or orientation change). * Performance: First open requires indexing (with progress bar), subsequent opens load from cache instantly. * Cover image format: PNG is detected but not supported for conversion (no PNG decoder available). Only BMP and JPG/JPEG work.
2026-01-14 19:36:40 +09:00
// Check if the current book is XTC, TXT, or EPUB
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
// Handle XTC file
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastXtc.load()) {
Serial.println("[SLP] Failed to load last XTC");
return (this->*renderNoCoverSleepScreen)();
}
if (!lastXtc.generateCoverBmp()) {
Serial.println("[SLP] Failed to generate XTC cover bmp");
return (this->*renderNoCoverSleepScreen)();
}
coverBmpPath = lastXtc.getCoverBmpPath();
Add TXT file reader support (#240) ## Summary * **What is the goal of this PR?** Add support for reading plain text (.txt) files, enabling users to browse, read, and track progress in TXT documents alongside existing EPUB and XTC formats. * **What changes are included?** - New Txt library for loading and parsing plain text files - New TxtReaderActivity with streaming page rendering using 8KB chunks to handle large files without memory issues on ESP32-C3 - Page index caching system (index.bin) for instant re-open after sleep or app restart - Progress bar UI during initial file indexing (matching EPUB style) - Word wrapping with proper UTF-8 support - Cover image support for TXT files: - Primary: image with same filename as TXT (e.g., book.jpg for book.txt) - Fallback: cover.bmp/jpg/jpeg in the same folder - JPG to BMP conversion using existing converter - Sleep screen cover mode now works with TXT files - File browser now shows .txt files ## Additional Context * Add any other information that might be helpful for the reviewer * Memory constraints: The streaming approach was necessary because ESP32-C3 only has 320KB RAM. A 700KB TXT file cannot be loaded entirely into memory, so we read 8KB chunks and build a page offset index instead. * Cache invalidation: The page index cache automatically invalidates when file size, viewport width, or lines per page changes (e.g., font size or orientation change). * Performance: First open requires indexing (with progress bar), subsequent opens load from cache instantly. * Cover image format: PNG is detected but not supported for conversion (no PNG decoder available). Only BMP and JPG/JPEG work.
2026-01-14 19:36:40 +09:00
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) {
// Handle TXT file - looks for cover image in the same folder
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastTxt.load()) {
Serial.println("[SLP] Failed to load last TXT");
return (this->*renderNoCoverSleepScreen)();
Add TXT file reader support (#240) ## Summary * **What is the goal of this PR?** Add support for reading plain text (.txt) files, enabling users to browse, read, and track progress in TXT documents alongside existing EPUB and XTC formats. * **What changes are included?** - New Txt library for loading and parsing plain text files - New TxtReaderActivity with streaming page rendering using 8KB chunks to handle large files without memory issues on ESP32-C3 - Page index caching system (index.bin) for instant re-open after sleep or app restart - Progress bar UI during initial file indexing (matching EPUB style) - Word wrapping with proper UTF-8 support - Cover image support for TXT files: - Primary: image with same filename as TXT (e.g., book.jpg for book.txt) - Fallback: cover.bmp/jpg/jpeg in the same folder - JPG to BMP conversion using existing converter - Sleep screen cover mode now works with TXT files - File browser now shows .txt files ## Additional Context * Add any other information that might be helpful for the reviewer * Memory constraints: The streaming approach was necessary because ESP32-C3 only has 320KB RAM. A 700KB TXT file cannot be loaded entirely into memory, so we read 8KB chunks and build a page offset index instead. * Cache invalidation: The page index cache automatically invalidates when file size, viewport width, or lines per page changes (e.g., font size or orientation change). * Performance: First open requires indexing (with progress bar), subsequent opens load from cache instantly. * Cover image format: PNG is detected but not supported for conversion (no PNG decoder available). Only BMP and JPG/JPEG work.
2026-01-14 19:36:40 +09:00
}
if (!lastTxt.generateCoverBmp()) {
Serial.println("[SLP] No cover image found for TXT file");
return (this->*renderNoCoverSleepScreen)();
Add TXT file reader support (#240) ## Summary * **What is the goal of this PR?** Add support for reading plain text (.txt) files, enabling users to browse, read, and track progress in TXT documents alongside existing EPUB and XTC formats. * **What changes are included?** - New Txt library for loading and parsing plain text files - New TxtReaderActivity with streaming page rendering using 8KB chunks to handle large files without memory issues on ESP32-C3 - Page index caching system (index.bin) for instant re-open after sleep or app restart - Progress bar UI during initial file indexing (matching EPUB style) - Word wrapping with proper UTF-8 support - Cover image support for TXT files: - Primary: image with same filename as TXT (e.g., book.jpg for book.txt) - Fallback: cover.bmp/jpg/jpeg in the same folder - JPG to BMP conversion using existing converter - Sleep screen cover mode now works with TXT files - File browser now shows .txt files ## Additional Context * Add any other information that might be helpful for the reviewer * Memory constraints: The streaming approach was necessary because ESP32-C3 only has 320KB RAM. A 700KB TXT file cannot be loaded entirely into memory, so we read 8KB chunks and build a page offset index instead. * Cache invalidation: The page index cache automatically invalidates when file size, viewport width, or lines per page changes (e.g., font size or orientation change). * Performance: First open requires indexing (with progress bar), subsequent opens load from cache instantly. * Cover image format: PNG is detected but not supported for conversion (no PNG decoder available). Only BMP and JPG/JPEG work.
2026-01-14 19:36:40 +09:00
}
coverBmpPath = lastTxt.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
// Handle EPUB file
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
feat: Add CSS parsing and CSS support in EPUBs (#411) ## Summary * **What is the goal of this PR?** - Adds basic CSS parsing to EPUBs and determine the CSS rules when rendering to the screen so that text is styled correctly. Currently supports bold, underline, italics, margin, padding, and text alignment ## Additional Context - My main reason for wanting this is that the book I'm currently reading, Carl's Doomsday Scenario (2nd in the Dungeon Crawler Carl series), relies _a lot_ on styled text for telling parts of the story. When text is bolded, it's supposed to be a message that's rendered "on-screen" in the story. When characters are "chatting" with each other, the text is bolded and their names are underlined. Plus, normal emphasis is provided with italicizing words here and there. So, this greatly improves my experience reading this book on the Xteink, and I figured it was useful enough for others too. - For transparency: I'm a software engineer, but I'm mostly frontend and TypeScript/JavaScript. It's been _years_ since I did any C/C++, so I would not be surprised if I'm doing something dumb along the way in this code. Please don't hesitate to ask for changes if something looks off. I heavily relied on Claude Code for help, and I had a lot of inspiration from how [microreader](https://github.com/CidVonHighwind/microreader) achieves their CSS parsing and styling. I did give this as good of a code review as I could and went through everything, and _it works on my machine_ 😄 ### Before ![IMG_6271](https://github.com/user-attachments/assets/dba7554d-efb6-4d13-88bc-8b83cd1fc615) ![IMG_6272](https://github.com/user-attachments/assets/61ba2de0-87c9-4f39-956f-013da4fe20a4) ### After ![IMG_6268](https://github.com/user-attachments/assets/ebe11796-cca9-4a46-b9c7-0709c7932818) ![IMG_6269](https://github.com/user-attachments/assets/e89c33dc-ff47-4bb7-855e-863fe44b3202) --- ### AI Usage Did you use AI tools to help write this code? **YES**, Claude Code
2026-02-05 05:28:10 -05:00
// Skip loading css since we only need metadata here
if (!lastEpub.load(true, true)) {
Serial.println("[SLP] Failed to load last epub");
return (this->*renderNoCoverSleepScreen)();
}
if (!lastEpub.generateCoverBmp(cropped)) {
Serial.println("[SLP] Failed to generate cover bmp");
return (this->*renderNoCoverSleepScreen)();
}
coverBmpPath = lastEpub.getCoverBmpPath(cropped);
} else {
return (this->*renderNoCoverSleepScreen)();
}
FsFile file;
if (SdMan.openFileForRead("SLP", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath);
renderBitmapSleepScreen(bitmap);
return;
}
}
return (this->*renderNoCoverSleepScreen)();
}
void SleepActivity::renderBlankSleepScreen() const {
renderer.clearScreen();
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}