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.
This commit is contained in:
Eunchurn Park
2026-01-14 19:36:40 +09:00
committed by GitHub
parent 14643d0225
commit 49f97b69ca
8 changed files with 1056 additions and 1 deletions

View File

@@ -3,6 +3,8 @@
#include "Epub.h"
#include "EpubReaderActivity.h"
#include "FileSelectionActivity.h"
#include "Txt.h"
#include "TxtReaderActivity.h"
#include "Xtc.h"
#include "XtcReaderActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
@@ -20,6 +22,12 @@ bool ReaderActivity::isXtcFile(const std::string& path) {
return StringUtils::checkFileExtension(path, ".xtc") || StringUtils::checkFileExtension(path, ".xtch");
}
bool ReaderActivity::isTxtFile(const std::string& path) {
if (path.length() < 4) return false;
std::string ext4 = path.substr(path.length() - 4);
return ext4 == ".txt" || ext4 == ".TXT";
}
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!SdMan.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
@@ -50,6 +58,21 @@ std::unique_ptr<Xtc> ReaderActivity::loadXtc(const std::string& path) {
return nullptr;
}
std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
if (!SdMan.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
auto txt = std::unique_ptr<Txt>(new Txt(path, "/.crosspoint"));
if (txt->load()) {
return txt;
}
Serial.printf("[%lu] [ ] Failed to load TXT\n", millis());
return nullptr;
}
void ReaderActivity::onSelectBookFile(const std::string& path) {
currentBookPath = path; // Track current book path
exitActivity();
@@ -67,6 +90,18 @@ void ReaderActivity::onSelectBookFile(const std::string& path) {
delay(2000);
onGoToFileSelection();
}
} else if (isTxtFile(path)) {
// Load TXT file
auto txt = loadTxt(path);
if (txt) {
onGoToTxtReader(std::move(txt));
} else {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load TXT",
EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH));
delay(2000);
onGoToFileSelection();
}
} else {
// Load EPUB file
auto epub = loadEpub(path);
@@ -108,6 +143,15 @@ void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
[this] { onGoBack(); }));
}
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
const auto txtPath = txt->getPath();
currentBookPath = txtPath;
exitActivity();
enterNewActivity(new TxtReaderActivity(
renderer, mappedInput, std::move(txt), [this, txtPath] { onGoToFileSelection(txtPath); },
[this] { onGoBack(); }));
}
void ReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
@@ -125,6 +169,13 @@ void ReaderActivity::onEnter() {
return;
}
onGoToXtcReader(std::move(xtc));
} else if (isTxtFile(initialBookPath)) {
auto txt = loadTxt(initialBookPath);
if (!txt) {
onGoBack();
return;
}
onGoToTxtReader(std::move(txt));
} else {
auto epub = loadEpub(initialBookPath);
if (!epub) {