Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f9f86b3dd | ||
|
|
47eb1157ef | ||
|
|
aee239a931 | ||
|
|
1ee8b728f9 | ||
|
|
2c80aca7b5 | ||
|
|
7704772ebe | ||
|
|
4186c7da9e | ||
|
|
802c9d0a30 |
16
README.md
16
README.md
@@ -31,7 +31,9 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
- [x] EPUB parsing and rendering
|
||||
- [x] Saved reading position
|
||||
- [ ] File explorer with file picker
|
||||
- Currently CrossPoint will just open the first EPUB it finds at the root of the SD card
|
||||
- [x] Basic EPUB picker from root directory
|
||||
- [ ] Support nested folders
|
||||
- [ ] EPUB picker with cover art
|
||||
- [ ] Image support within EPUB
|
||||
- [ ] Configurable font, layout, and display options
|
||||
- [ ] WiFi connectivity
|
||||
@@ -50,10 +52,22 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
||||
|
||||
#### Command line
|
||||
|
||||
Connect your Xteink X4 to your computer via USB-C and run the following command.
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
#### Web
|
||||
|
||||
1. Connect your Xteink X4 to your computer via USB-C
|
||||
2. Download the `firmware.bin` file from the latest release via the [releases page](https://github.com/daveallie/crosspoint-reader/releases)
|
||||
3. Go to https://xteink.dve.al/ and flash the firmware file using the "OTA fast flash controls" section
|
||||
4. Press the reset button on the Xteink X4 to restart the device
|
||||
|
||||
To revert back to the official firmware, you can flash the latest official firmware from https://xteink.dve.al/, or swap
|
||||
back to the other partition using the "Swap boot partition" button here https://xteink.dve.al/debug.
|
||||
|
||||
## Internals
|
||||
|
||||
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
|
||||
|
||||
@@ -57,10 +57,10 @@ void EpdRenderer::drawText(const int x, const int y, const char* text, const boo
|
||||
getFontRenderer(bold, italic)->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawSmallText(const int x, const int y, const char* text) const {
|
||||
void EpdRenderer::drawSmallText(const int x, const int y, const char* text, const uint16_t color) const {
|
||||
int ypos = y + smallFont->font->data->advanceY + marginTop;
|
||||
int xpos = x + marginLeft;
|
||||
smallFont->renderString(text, &xpos, &ypos, GxEPD_BLACK);
|
||||
smallFont->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
|
||||
|
||||
@@ -26,7 +26,7 @@ class EpdRenderer {
|
||||
int getTextWidth(const char* text, bool bold = false, bool italic = false) const;
|
||||
int getSmallTextWidth(const char* text) const;
|
||||
void drawText(int x, int y, const char* text, bool bold = false, bool italic = false, uint16_t color = 1) const;
|
||||
void drawSmallText(int x, int y, const char* text) const;
|
||||
void drawSmallText(int x, int y, const char* text, uint16_t color = 1) const;
|
||||
void drawTextBox(int x, int y, const std::string& text, int width, int height, bool bold = false,
|
||||
bool italic = false) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, uint16_t color) const;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <HardwareSerial.h>
|
||||
#include <SD.h>
|
||||
#include <ZipFile.h>
|
||||
#include <tinyxml2.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
@@ -162,14 +161,14 @@ bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNcxFile(ZipFile& zip) {
|
||||
bool Epub::parseTocNcxFile(const ZipFile& zip) {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
Serial.println("No ncx file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ncxData = zip.readTextFileToMemory(tocNcxItem.c_str());
|
||||
const auto ncxData = zip.readTextFileToMemory(tocNcxItem.c_str());
|
||||
if (!ncxData) {
|
||||
Serial.printf("Could not find %s\n", tocNcxItem.c_str());
|
||||
return false;
|
||||
@@ -177,7 +176,7 @@ bool Epub::parseTocNcxFile(ZipFile& zip) {
|
||||
|
||||
// Parse the Toc contents
|
||||
tinyxml2::XMLDocument doc;
|
||||
auto result = doc.Parse(ncxData);
|
||||
const auto result = doc.Parse(ncxData);
|
||||
free(ncxData);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
@@ -185,27 +184,30 @@ bool Epub::parseTocNcxFile(ZipFile& zip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ncx = doc.FirstChildElement("ncx");
|
||||
const auto ncx = doc.FirstChildElement("ncx");
|
||||
if (!ncx) {
|
||||
Serial.println("Could not find first child ncx in toc");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto navMap = ncx->FirstChildElement("navMap");
|
||||
const auto navMap = ncx->FirstChildElement("navMap");
|
||||
if (!navMap) {
|
||||
Serial.println("Could not find navMap child in ncx");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto navPoint = navMap->FirstChildElement("navPoint");
|
||||
recursivelyParseNavMap(navMap->FirstChildElement("navPoint"));
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::recursivelyParseNavMap(tinyxml2::XMLElement* element) {
|
||||
// Fills toc map
|
||||
while (navPoint) {
|
||||
std::string navTitle = navPoint->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
|
||||
auto content = navPoint->FirstChildElement("content");
|
||||
while (element) {
|
||||
std::string navTitle = element->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
|
||||
const auto content = element->FirstChildElement("content");
|
||||
std::string href = contentBasePath + content->Attribute("src");
|
||||
// split the href on the # to get the href and the anchor
|
||||
size_t pos = href.find('#');
|
||||
const size_t pos = href.find('#');
|
||||
std::string anchor;
|
||||
|
||||
if (pos != std::string::npos) {
|
||||
@@ -214,10 +216,13 @@ bool Epub::parseTocNcxFile(ZipFile& zip) {
|
||||
}
|
||||
|
||||
toc.emplace_back(navTitle, href, anchor, 0);
|
||||
navPoint = navPoint->NextSiblingElement("navPoint");
|
||||
}
|
||||
|
||||
return true;
|
||||
tinyxml2::XMLElement* nestedNavPoint = element->FirstChildElement("navPoint");
|
||||
if (nestedNavPoint) {
|
||||
recursivelyParseNavMap(nestedNavPoint);
|
||||
}
|
||||
element = element->NextSiblingElement("navPoint");
|
||||
}
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
@@ -369,9 +374,7 @@ int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
||||
// the toc entry should have an href that matches the spine item
|
||||
// so we can find the toc index by looking for the href
|
||||
Serial.printf("Looking for %s\n", spine[spineIndex].second.c_str());
|
||||
for (int i = 0; i < toc.size(); i++) {
|
||||
Serial.printf("Looking at %s\n", toc[i].href.c_str());
|
||||
if (toc[i].href == spine[spineIndex].second) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <HardwareSerial.h>
|
||||
#include <tinyxml2.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -38,36 +39,29 @@ class Epub {
|
||||
// find the path for the content.opf file
|
||||
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
|
||||
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
|
||||
bool parseTocNcxFile(ZipFile& zip);
|
||||
bool parseTocNcxFile(const ZipFile& zip);
|
||||
void recursivelyParseNavMap(tinyxml2::XMLElement* element);
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
// create a cache key based on the filepath
|
||||
|
||||
cachePath = cacheDir + "/epub_" + std::to_string(std::hash<std::string>{}(this->filepath));
|
||||
}
|
||||
~Epub() = default;
|
||||
std::string& getBasePath() { return contentBasePath; }
|
||||
bool load();
|
||||
|
||||
void clearCache() const;
|
||||
|
||||
void setupCacheDir() const;
|
||||
|
||||
const std::string& getCachePath() const;
|
||||
const std::string& getPath() const;
|
||||
const std::string& getTitle() const;
|
||||
const std::string& getCoverImageItem() const;
|
||||
uint8_t* getItemContents(const std::string& itemHref, size_t* size = nullptr) const;
|
||||
char* getTextItemContents(const std::string& itemHref, size_t* size = nullptr) const;
|
||||
|
||||
std::string& getSpineItem(int spineIndex);
|
||||
int getSpineItemsCount() const;
|
||||
|
||||
EpubTocEntry& getTocItem(int tocTndex);
|
||||
int getTocItemsCount() const;
|
||||
// work out the section index for a toc index
|
||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||
|
||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||
};
|
||||
|
||||
46
src/CrossPointState.cpp
Normal file
46
src/CrossPointState.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "CrossPointState.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SD.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
constexpr uint8_t STATE_VERSION = 1;
|
||||
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
|
||||
|
||||
void CrossPointState::serialize(std::ostream& os) const {
|
||||
serialization::writePod(os, STATE_VERSION);
|
||||
serialization::writeString(os, openEpubPath);
|
||||
}
|
||||
|
||||
CrossPointState* CrossPointState::deserialize(std::istream& is) {
|
||||
const auto state = new CrossPointState();
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(is, version);
|
||||
if (version != STATE_VERSION) {
|
||||
Serial.printf("CrossPointState: Unknown version %u\n", version);
|
||||
return state;
|
||||
}
|
||||
|
||||
serialization::readString(is, state->openEpubPath);
|
||||
return state;
|
||||
}
|
||||
|
||||
void CrossPointState::saveToFile() const {
|
||||
std::ofstream outputFile(STATE_FILE);
|
||||
serialize(outputFile);
|
||||
outputFile.close();
|
||||
}
|
||||
|
||||
CrossPointState* CrossPointState::loadFromFile() {
|
||||
if (!SD.exists(&STATE_FILE[3])) {
|
||||
return new CrossPointState();
|
||||
}
|
||||
|
||||
std::ifstream inputFile(STATE_FILE);
|
||||
CrossPointState* state = deserialize(inputFile);
|
||||
inputFile.close();
|
||||
return state;
|
||||
}
|
||||
14
src/CrossPointState.h
Normal file
14
src/CrossPointState.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
class CrossPointState {
|
||||
void serialize(std::ostream& os) const;
|
||||
static CrossPointState* deserialize(std::istream& is);
|
||||
|
||||
public:
|
||||
std::string openEpubPath;
|
||||
~CrossPointState() = default;
|
||||
void saveToFile() const;
|
||||
static CrossPointState* loadFromFile();
|
||||
};
|
||||
@@ -10,9 +10,9 @@
|
||||
// Button ADC thresholds
|
||||
#define BTN_THRESHOLD 100 // Threshold tolerance
|
||||
#define BTN_RIGHT_VAL 3
|
||||
#define BTN_LEFT_VAL 1470
|
||||
#define BTN_CONFIRM_VAL 2655
|
||||
#define BTN_BACK_VAL 3470
|
||||
#define BTN_LEFT_VAL 1500
|
||||
#define BTN_CONFIRM_VAL 2700
|
||||
#define BTN_BACK_VAL 3550
|
||||
#define BTN_VOLUME_DOWN_VAL 3
|
||||
#define BTN_VOLUME_UP_VAL 2305
|
||||
|
||||
|
||||
50
src/main.cpp
50
src/main.cpp
@@ -6,8 +6,10 @@
|
||||
#include <SPI.h>
|
||||
|
||||
#include "Battery.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "Input.h"
|
||||
#include "screens/EpubReaderScreen.h"
|
||||
#include "screens/FileSelectionScreen.h"
|
||||
#include "screens/FullScreenMessageScreen.h"
|
||||
|
||||
#define SPI_FQ 40000000
|
||||
@@ -28,6 +30,7 @@ GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2
|
||||
EPD_RST, EPD_BUSY));
|
||||
auto renderer = new EpdRenderer(&display);
|
||||
Screen* currentScreen;
|
||||
CrossPointState* appState;
|
||||
|
||||
// Power button timing
|
||||
// Time required to confirm boot from sleep
|
||||
@@ -102,6 +105,21 @@ void setupSerial() {
|
||||
}
|
||||
}
|
||||
|
||||
void onGoHome();
|
||||
void onSelectEpubFile(const std::string& path) {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading..."));
|
||||
|
||||
Epub* epub = loadEpub(path);
|
||||
if (epub) {
|
||||
appState->openEpubPath = path;
|
||||
appState->saveToFile();
|
||||
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome));
|
||||
} else {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub"));
|
||||
}
|
||||
}
|
||||
void onGoHome() { enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile)); }
|
||||
|
||||
void setup() {
|
||||
setupInputPinModes();
|
||||
|
||||
@@ -127,37 +145,21 @@ void setup() {
|
||||
display.setTextColor(GxEPD_BLACK);
|
||||
Serial.println("Display initialized");
|
||||
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...", true));
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Booting...", true));
|
||||
|
||||
// SD Card Initialization
|
||||
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
||||
|
||||
// TODO: Add a file selection screen, for now just load the first file
|
||||
File root = SD.open("/");
|
||||
String filename;
|
||||
while (true) {
|
||||
filename = root.getNextFileName();
|
||||
if (!filename) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (filename.substring(filename.length() - 5) == ".epub") {
|
||||
Serial.printf("Found epub: %s\n", filename.c_str());
|
||||
break;
|
||||
appState = CrossPointState::loadFromFile();
|
||||
if (!appState->openEpubPath.empty()) {
|
||||
Epub* epub = loadEpub(appState->openEpubPath);
|
||||
if (epub) {
|
||||
enterNewScreen(new EpubReaderScreen(renderer, epub, onGoHome));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Could not find epub"));
|
||||
return;
|
||||
}
|
||||
|
||||
Epub* epub = loadEpub(std::string(filename.c_str()));
|
||||
if (epub) {
|
||||
enterNewScreen(new EpubReaderScreen(renderer, epub));
|
||||
} else {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub"));
|
||||
}
|
||||
enterNewScreen(new FileSelectionScreen(renderer, onSelectEpubFile));
|
||||
}
|
||||
|
||||
void loop() {
|
||||
|
||||
@@ -41,13 +41,14 @@ void EpubReaderScreen::onEnter() {
|
||||
|
||||
void EpubReaderScreen::onExit() {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||
vSemaphoreDelete(sectionMutex);
|
||||
sectionMutex = nullptr;
|
||||
}
|
||||
|
||||
void EpubReaderScreen::handleInput(const Input input) {
|
||||
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN) {
|
||||
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN || input.button == LEFT || input.button == RIGHT) {
|
||||
const bool skipChapter = input.pressTime > SKIP_CHAPTER_MS;
|
||||
|
||||
// No current section, attempt to rerender the book
|
||||
@@ -56,17 +57,17 @@ void EpubReaderScreen::handleInput(const Input input) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.button == VOLUME_UP && skipChapter) {
|
||||
if ((input.button == VOLUME_UP || input.button == LEFT) && skipChapter) {
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex--;
|
||||
delete section;
|
||||
section = nullptr;
|
||||
} else if (input.button == VOLUME_DOWN && skipChapter) {
|
||||
} else if ((input.button == VOLUME_DOWN || input.button == RIGHT) && skipChapter) {
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex++;
|
||||
delete section;
|
||||
section = nullptr;
|
||||
} else if (input.button == VOLUME_UP) {
|
||||
} else if (input.button == VOLUME_UP || input.button == LEFT) {
|
||||
if (section->currentPage > 0) {
|
||||
section->currentPage--;
|
||||
} else {
|
||||
@@ -77,7 +78,7 @@ void EpubReaderScreen::handleInput(const Input input) {
|
||||
section = nullptr;
|
||||
xSemaphoreGive(sectionMutex);
|
||||
}
|
||||
} else if (input.button == VOLUME_DOWN) {
|
||||
} else if (input.button == VOLUME_DOWN || input.button == RIGHT) {
|
||||
if (section->currentPage < section->pageCount - 1) {
|
||||
section->currentPage++;
|
||||
} else {
|
||||
@@ -91,6 +92,8 @@ void EpubReaderScreen::handleInput(const Input input) {
|
||||
}
|
||||
|
||||
updateRequired = true;
|
||||
} else if (input.button == BACK) {
|
||||
onGoHome();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +162,7 @@ void EpubReaderScreen::renderPage() {
|
||||
void EpubReaderScreen::renderStatusBar() const {
|
||||
const auto pageWidth = renderer->getPageWidth();
|
||||
|
||||
std::string progress = std::to_string(currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||
std::string progress = std::to_string(section->currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||
const auto progressTextWidth = renderer->getSmallTextWidth(progress.c_str());
|
||||
renderer->drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ class EpubReaderScreen final : public Screen {
|
||||
SemaphoreHandle_t sectionMutex = nullptr;
|
||||
int currentSpineIndex = 0;
|
||||
int nextPageNumber = 0;
|
||||
int currentPage = 0;
|
||||
bool updateRequired = false;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
@@ -23,7 +23,8 @@ class EpubReaderScreen final : public Screen {
|
||||
void renderStatusBar() const;
|
||||
|
||||
public:
|
||||
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub) : Screen(renderer), epub(epub) {}
|
||||
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub, const std::function<void()>& onGoHome)
|
||||
: Screen(renderer), epub(epub), onGoHome(onGoHome) {}
|
||||
~EpubReaderScreen() override { free(section); }
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
|
||||
87
src/screens/FileSelectionScreen.cpp
Normal file
87
src/screens/FileSelectionScreen.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "FileSelectionScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
void FileSelectionScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<FileSelectionScreen*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void FileSelectionScreen::onEnter() {
|
||||
files.clear();
|
||||
auto root = SD.open("/");
|
||||
File file;
|
||||
while ((file = root.openNextFile())) {
|
||||
if (file.isDirectory()) {
|
||||
file.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
auto filename = std::string(file.name());
|
||||
if (filename.substr(filename.length() - 5) != ".epub" || filename[0] == '.') {
|
||||
file.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
files.emplace_back(filename);
|
||||
file.close();
|
||||
}
|
||||
root.close();
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
|
||||
1024, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void FileSelectionScreen::onExit() {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
|
||||
void FileSelectionScreen::handleInput(const Input input) {
|
||||
if (input.button == VOLUME_DOWN || input.button == RIGHT) {
|
||||
selectorIndex = (selectorIndex + 1) % files.size();
|
||||
updateRequired = true;
|
||||
} else if (input.button == VOLUME_UP || input.button == LEFT) {
|
||||
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
|
||||
updateRequired = true;
|
||||
} else if (input.button == CONFIRM) {
|
||||
Serial.printf("Selected file: %s\n", files[selectorIndex].c_str());
|
||||
onSelect("/" + files[selectorIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
void FileSelectionScreen::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
render();
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void FileSelectionScreen::render() const {
|
||||
renderer->clearScreen();
|
||||
|
||||
const auto pageWidth = renderer->getPageWidth();
|
||||
const auto titleWidth = renderer->getTextWidth("CrossPoint Reader", true);
|
||||
renderer->drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", true);
|
||||
|
||||
// Draw selection
|
||||
renderer->fillRect(0, 50 + selectorIndex * 20 + 2, pageWidth - 1, 20, 1);
|
||||
|
||||
for (size_t i = 0; i < files.size(); i++) {
|
||||
const auto file = files[i];
|
||||
renderer->drawSmallText(50, 50 + i * 20, file.c_str(), i == selectorIndex ? 0 : 1);
|
||||
}
|
||||
|
||||
renderer->flushDisplay();
|
||||
}
|
||||
28
src/screens/FileSelectionScreen.h
Normal file
28
src/screens/FileSelectionScreen.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Screen.h"
|
||||
|
||||
class FileSelectionScreen final : public Screen {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
std::vector<std::string> files;
|
||||
int selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
const std::function<void(const std::string&)> onSelect;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
public:
|
||||
explicit FileSelectionScreen(EpdRenderer* renderer, const std::function<void(const std::string&)>& onSelect)
|
||||
: Screen(renderer), onSelect(onSelect) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput(Input input) override;
|
||||
};
|
||||
Reference in New Issue
Block a user