Merge branch 'master' into feature/footnotes

This commit is contained in:
Jérôme Launay 2025-12-17 14:28:57 +01:00
commit a9176ff909
39 changed files with 560 additions and 429 deletions

View File

@ -152,6 +152,18 @@ bool Epub::load() {
return false;
}
// determine size of spine items
size_t spineItemsCount = getSpineItemsCount();
size_t spineItemsSize = 0;
for (size_t i = 0; i < spineItemsCount; i++) {
std::string spineItem = getSpineItem(i);
size_t s = 0;
getItemSize(spineItem, &s);
spineItemsSize += s;
cumulativeSpineItemSize.emplace_back(spineItemsSize);
}
Serial.printf("[%lu] [EBP] Book size: %u\n", millis(), spineItemsSize);
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true;
@ -262,14 +274,11 @@ int Epub::getSpineItemsCount() const {
return spine.size() + virtualCount;
}
std::string Epub::getSpineItem(const int spineIndex) const {
if (spineIndex < 0) {
Serial.printf("[%lu] [EBP] getSpineItem index:%d is negative\n", millis(), spineIndex);
return "";
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return cumulativeSpineItemSize.at(spineIndex); }
std::string Epub::getSpineItem(const int spineIndex) const {
// Normal spine item
if (spineIndex < static_cast<int>(spine.size())) {
if (spineIndex >= 0 && spineIndex < static_cast<int>(spine.size())) {
return contentBasePath + spine.at(spineIndex).second;
}
@ -282,7 +291,10 @@ std::string Epub::getSpineItem(const int spineIndex) const {
}
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
return "";
// Return empty string instead of reference to avoid issues
static std::string emptyString = "";
return emptyString;
}
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
@ -401,4 +413,14 @@ int Epub::findVirtualSpineIndex(const std::string& filename) const {
}
}
return -1;
}
}
size_t Epub::getBookSize() const { return getCumulativeSpineItemSize(getSpineItemsCount() - 1); }
// Calculate progress in book
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
size_t prevChapterSize = getCumulativeSpineItemSize(currentSpineIndex - 1);
size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
size_t bookSize = getBookSize();
size_t sectionProgSize = currentSpineRead * curChapterSize;
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
}

View File

@ -16,6 +16,9 @@ class Epub {
std::string tocNcxItem;
std::string filepath;
std::vector<std::pair<std::string, std::string>> spine;
// the file size of the spine items (proxy to book progress)
std::vector<size_t> cumulativeSpineItemSize;
// the toc of the EPUB file
std::vector<EpubTocEntry> toc;
std::string contentBasePath;
std::string cachePath;
@ -51,11 +54,10 @@ class Epub {
bool trailingNullByte = false) const;
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
bool getItemSize(const std::string& itemHref, size_t* size) const;
std::string getSpineItem(int index) const;
int getSpineItemsCount() const;
EpubTocEntry& getTocItem(int tocTndex);
size_t getCumulativeSpineItemSize(const int spineIndex) const;
EpubTocEntry& getTocItem(int tocIndex);
int getTocItemsCount() const;
int getSpineIndexForTocIndex(int tocIndex) const;
int getTocIndexForSpineIndex(int spineIndex) const;
@ -66,4 +68,7 @@ class Epub {
int addVirtualSpineItem(const std::string& path);
bool isVirtualSpineItem(int spineIndex) const;
int findVirtualSpineIndex(const std::string& filename) const;
};
size_t getBookSize() const;
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const;
};

View File

@ -3,7 +3,9 @@
#include <HardwareSerial.h>
#include <Serialization.h>
constexpr uint8_t PAGE_FILE_VERSION = 6; // Incremented
namespace {
constexpr uint8_t PAGE_FILE_VERSION = 3;
}
void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); }
@ -81,4 +83,4 @@ std::unique_ptr<Page> Page::deserialize(std::istream& is) {
}
return page;
}
}

View File

@ -10,7 +10,9 @@
#include "Page.h"
#include "parsers/ChapterHtmlSlimParser.h"
constexpr uint8_t SECTION_FILE_VERSION = 6;
namespace {
constexpr uint8_t SECTION_FILE_VERSION = 5;
}
// Helper function to write XML-escaped text directly to file
static bool writeEscapedXml(File& file, const char* text) {

View File

@ -10,9 +10,11 @@
// Initialize the static instance
CrossPointSettings CrossPointSettings::instance;
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
constexpr uint8_t SETTINGS_COUNT = 2;
constexpr char SETTINGS_FILE[] = "/sd/.crosspoint/settings.bin";
} // namespace
bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists

View File

@ -6,8 +6,12 @@
#include <fstream>
namespace {
constexpr uint8_t STATE_FILE_VERSION = 1;
constexpr char STATE_FILE[] = "/sd/.crosspoint/state.bin";
} // namespace
CrossPointState CrossPointState::instance;
bool CrossPointState::saveToFile() const {
std::ofstream outputFile(STATE_FILE);

View File

@ -3,11 +3,20 @@
#include <string>
class CrossPointState {
// Static instance
static CrossPointState instance;
public:
std::string openEpubPath;
~CrossPointState() = default;
// Get singleton instance
static CrossPointState& getInstance() { return instance; }
bool saveToFile() const;
bool loadFromFile();
};
// Helper macro to access settings
#define APP_STATE CrossPointState::getInstance()

18
src/activities/Activity.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <InputManager.h>
class GfxRenderer;
class Activity {
protected:
GfxRenderer& renderer;
InputManager& inputManager;
public:
explicit Activity(GfxRenderer& renderer, InputManager& inputManager)
: renderer(renderer), inputManager(inputManager) {}
virtual ~Activity() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void loop() {}
};

View File

@ -0,0 +1,21 @@
#include "ActivityWithSubactivity.h"
void ActivityWithSubactivity::exitActivity() {
if (subActivity) {
subActivity->onExit();
subActivity.reset();
}
}
void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
subActivity.reset(activity);
subActivity->onEnter();
}
void ActivityWithSubactivity::loop() {
if (subActivity) {
subActivity->loop();
}
}
void ActivityWithSubactivity::onExit() { exitActivity(); }

View File

@ -0,0 +1,17 @@
#pragma once
#include <memory>
#include "Activity.h"
class ActivityWithSubactivity : public Activity {
protected:
std::unique_ptr<Activity> subActivity = nullptr;
void exitActivity();
void enterNewActivity(Activity* activity);
public:
explicit ActivityWithSubactivity(GfxRenderer& renderer, InputManager& inputManager)
: Activity(renderer, inputManager) {}
void loop() override;
void onExit() override;
};

View File

@ -1,11 +1,11 @@
#include "BootLogoScreen.h"
#include "BootActivity.h"
#include <GfxRenderer.h>
#include "config.h"
#include "images/CrossLarge.h"
void BootLogoScreen::onEnter() {
void BootActivity::onEnter() {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();

View File

@ -0,0 +1,8 @@
#pragma once
#include "../Activity.h"
class BootActivity final : public Activity {
public:
explicit BootActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -1,4 +1,4 @@
#include "SleepScreen.h"
#include "SleepActivity.h"
#include <GfxRenderer.h>
@ -6,7 +6,7 @@
#include "config.h"
#include "images/CrossLarge.h"
void SleepScreen::onEnter() {
void SleepActivity::onEnter() {
const auto pageWidth = GfxRenderer::getScreenWidth();
const auto pageHeight = GfxRenderer::getScreenHeight();

View File

@ -0,0 +1,8 @@
#pragma once
#include "../Activity.h"
class SleepActivity final : public Activity {
public:
explicit SleepActivity(GfxRenderer& renderer, InputManager& inputManager) : Activity(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -1,16 +1,20 @@
#include "HomeScreen.h"
#include "HomeActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
#include "config.h"
void HomeScreen::taskTrampoline(void* param) {
auto* self = static_cast<HomeScreen*>(param);
namespace {
constexpr int menuItemCount = 2;
}
void HomeActivity::taskTrampoline(void* param) {
auto* self = static_cast<HomeActivity*>(param);
self->displayTaskLoop();
}
void HomeScreen::onEnter() {
void HomeActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = 0;
@ -18,7 +22,7 @@ void HomeScreen::onEnter() {
// Trigger first update
updateRequired = true;
xTaskCreate(&HomeScreen::taskTrampoline, "HomeScreenTask",
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
@ -26,7 +30,7 @@ void HomeScreen::onEnter() {
);
}
void HomeScreen::onExit() {
void HomeActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
@ -37,7 +41,7 @@ void HomeScreen::onExit() {
renderingMutex = nullptr;
}
void HomeScreen::handleInput() {
void HomeActivity::loop() {
const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed =
@ -45,7 +49,7 @@ void HomeScreen::handleInput() {
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (selectorIndex == 0) {
onFileSelectionOpen();
onReaderOpen();
} else if (selectorIndex == 1) {
onSettingsOpen();
}
@ -58,7 +62,7 @@ void HomeScreen::handleInput() {
}
}
void HomeScreen::displayTaskLoop() {
void HomeActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -70,7 +74,7 @@ void HomeScreen::displayTaskLoop() {
}
}
void HomeScreen::render() const {
void HomeActivity::render() const {
renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth();

View File

@ -0,0 +1,29 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "../Activity.h"
class HomeActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onReaderOpen;
const std::function<void()> onSettingsOpen;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public:
explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onReaderOpen,
const std::function<void()>& onSettingsOpen)
: Activity(renderer, inputManager), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@ -1,4 +1,4 @@
#include "EpubReaderScreen.h"
#include "EpubReaderActivity.h"
#include <Epub/Page.h>
#include <GfxRenderer.h>
@ -6,25 +6,31 @@
#include "Battery.h"
#include "CrossPointSettings.h"
#include "EpubReaderChapterSelectionScreen.h"
#include "EpubReaderFootnotesScreen.h"
#include "EpubReaderMenuScreen.h"
#include "EpubReaderChapterSelectionActivity.h"
#include "EpubReaderFootnotesActivity.h"
#include "EpubReaderMenuActivity.h"
// Note: Vous devrez créer ces nouveaux fichiers:
// - EpubReaderMenuActivity.h/cpp (basé sur EpubReaderMenuScreen)
// - EpubReaderFootnotesActivity.h/cpp (basé sur EpubReaderFootnotesScreen)
#include "config.h"
constexpr int PAGES_PER_REFRESH = 15;
constexpr unsigned long SKIP_CHAPTER_MS = 700;
namespace {
constexpr int pagesPerRefresh = 15;
constexpr unsigned long skipChapterMs = 700;
constexpr float lineCompression = 0.95f;
constexpr int marginTop = 8;
constexpr int marginRight = 10;
constexpr int marginBottom = 22;
constexpr int marginLeft = 10;
} // namespace
void EpubReaderScreen::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderScreen*>(param);
void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderScreen::onEnter() {
void EpubReaderActivity::onEnter() {
if (!epub) {
return;
}
@ -45,12 +51,15 @@ void EpubReaderScreen::onEnter() {
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask",
24576, // 32768
this, 1, &displayTaskHandle);
xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void EpubReaderScreen::onExit() {
void EpubReaderActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
@ -63,47 +72,36 @@ void EpubReaderScreen::onExit() {
epub.reset();
}
void EpubReaderScreen::handleInput() {
// Pass input responsibility to sub screen if exists
if (subScreen) {
subScreen->handleInput();
void EpubReaderActivity::loop() {
// Pass input responsibility to sub activity if exists
if (subAcitivity) {
subAcitivity->loop();
return;
}
// Enter Menu selection screen
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (isViewingFootnote) {
restoreSavedPosition();
updateRequired = true;
return;
} else {
onGoBack();
return;
}
}
// Enter menu activity
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
// Don't start screen transition while rendering
// Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY);
subScreen.reset(new EpubReaderMenuScreen(
subAcitivity.reset(new EpubReaderMenuActivity(
this->renderer, this->inputManager,
[this] {
// onGoBack - return to reading
subScreen->onExit();
subScreen.reset();
// onGoBack from menu
subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true;
},
[this](EpubReaderMenuScreen::MenuOption option) {
[this](EpubReaderMenuActivity::MenuOption option) {
// onSelectOption - handle menu choice
if (option == EpubReaderMenuScreen::CHAPTERS) {
if (option == EpubReaderMenuActivity::CHAPTERS) {
// Show chapter selection
subScreen->onExit();
subScreen.reset(new EpubReaderChapterSelectionScreen(
subAcitivity->onExit();
subAcitivity.reset(new EpubReaderChapterSelectionActivity(
this->renderer, this->inputManager, epub, currentSpineIndex,
[this] {
// onGoBack from chapter selection
subScreen->onExit();
subScreen.reset();
subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true;
},
[this](const int newSpineIndex) {
@ -113,42 +111,46 @@ void EpubReaderScreen::handleInput() {
nextPageNumber = 0;
section.reset();
}
subScreen->onExit();
subScreen.reset();
subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true;
}));
subScreen->onEnter();
} else if (option == EpubReaderMenuScreen::FOOTNOTES) {
subAcitivity->onEnter();
} else if (option == EpubReaderMenuActivity::FOOTNOTES) {
// Show footnotes page with current page notes
subScreen->onExit();
subScreen.reset(new EpubReaderFootnotesScreen(
subAcitivity->onExit();
subAcitivity.reset(new EpubReaderFootnotesActivity(
this->renderer, this->inputManager,
currentPageFootnotes, // Pass collected footnotes (reference)
[this] {
// onGoBack from footnotes
subScreen->onExit();
subScreen.reset();
subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true;
},
[this](const char* href) {
// onSelectFootnote - navigate to the footnote location
navigateToHref(href, true); // true = save current position
subScreen->onExit();
subScreen.reset();
subAcitivity->onExit();
subAcitivity.reset();
updateRequired = true;
}));
subScreen->onEnter();
subAcitivity->onEnter();
}
}));
subScreen->onEnter();
subAcitivity->onEnter();
xSemaphoreGive(renderingMutex);
}
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
return;
if (isViewingFootnote) {
restoreSavedPosition();
updateRequired = true;
return;
} else {
onGoBack();
return;
}
}
const bool prevReleased =
@ -168,7 +170,7 @@ void EpubReaderScreen::handleInput() {
return;
}
const bool skipChapter = inputManager.getHeldTime() > SKIP_CHAPTER_MS;
const bool skipChapter = inputManager.getHeldTime() > skipChapterMs;
if (skipChapter) {
// We don't want to delete the section mid-render, so grab the semaphore
@ -214,7 +216,7 @@ void EpubReaderScreen::handleInput() {
}
}
void EpubReaderScreen::displayTaskLoop() {
void EpubReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -226,7 +228,7 @@ void EpubReaderScreen::displayTaskLoop() {
}
}
void EpubReaderScreen::renderScreen() {
void EpubReaderActivity::renderScreen() {
if (!epub) {
return;
}
@ -346,12 +348,12 @@ void EpubReaderScreen::renderScreen() {
}
}
void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page) {
page->render(renderer, READER_FONT_ID);
renderStatusBar();
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
pagesUntilFullRefresh = PAGES_PER_REFRESH;
pagesUntilFullRefresh = pagesPerRefresh;
} else {
renderer.displayBuffer();
pagesUntilFullRefresh--;
@ -383,17 +385,23 @@ void EpubReaderScreen::renderContents(std::unique_ptr<Page> page) {
renderer.restoreBwBuffer();
}
void EpubReaderScreen::renderStatusBar() const {
void EpubReaderActivity::renderStatusBar() const {
constexpr auto textY = 776;
// Calculate progress in book
float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
uint8_t bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg);
// Right aligned text for progress counter
char progressBuf[32]; // Use fixed buffer instead of std::string
snprintf(progressBuf, sizeof(progressBuf), "%d / %d", section->currentPage + 1, section->pageCount);
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressBuf);
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, progressBuf);
const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) +
" " + std::to_string(bookProgress) + "%";
const auto progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str());
renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY,
progress.c_str());
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
char percentageBuf[8]; // Use fixed buffer instead of std::string
char percentageBuf[8];
snprintf(percentageBuf, sizeof(percentageBuf), "%d%%", percentage);
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageBuf);
renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageBuf);
@ -448,7 +456,7 @@ void EpubReaderScreen::renderStatusBar() const {
}
}
void EpubReaderScreen::navigateToHref(const char* href, bool savePosition) {
void EpubReaderActivity::navigateToHref(const char* href, bool savePosition) {
if (!epub || !href) return;
// Save current position if requested
@ -546,8 +554,7 @@ void EpubReaderScreen::navigateToHref(const char* href, bool savePosition) {
Serial.printf("[%lu] [ERS] Navigated to spine index: %d\n", millis(), targetSpineIndex);
}
// Method to restore saved position
void EpubReaderScreen::restoreSavedPosition() {
void EpubReaderActivity::restoreSavedPosition() {
if (savedSpineIndex >= 0 && savedPageNumber >= 0) {
Serial.printf("[%lu] [ERS] Restoring position: spine %d, page %d\n", millis(), savedSpineIndex, savedPageNumber);

View File

@ -5,15 +5,15 @@
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "EpubReaderFootnotesScreen.h"
#include "Screen.h"
#include "../Activity.h"
#include "EpubReaderFootnotesActivity.h"
class EpubReaderScreen final : public Screen {
class EpubReaderActivity final : public Activity {
std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::unique_ptr<Screen> subScreen = nullptr;
std::unique_ptr<Activity> subAcitivity = nullptr;
int currentSpineIndex = 0;
int nextPageNumber = 0;
int pagesUntilFullRefresh = 0;
@ -36,10 +36,10 @@ class EpubReaderScreen final : public Screen {
void restoreSavedPosition();
public:
explicit EpubReaderScreen(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoBack)
: Screen(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {}
explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr<Epub> epub,
const std::function<void()>& onGoBack)
: Activity(renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
void loop() override;
};

View File

@ -1,4 +1,4 @@
#include "EpubReaderChapterSelectionScreen.h"
#include "EpubReaderChapterSelectionActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
@ -8,52 +8,12 @@
constexpr int PAGE_ITEMS = 24;
constexpr int SKIP_PAGE_MS = 700;
void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionScreen*>(param);
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderChapterSelectionScreen::onEnter() {
if (!epub) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
// Build filtered chapter list (excluding footnote pages)
buildFilteredChapterList();
// Find the index in filtered list that corresponds to currentSpineIndex
selectorIndex = 0;
for (size_t i = 0; i < filteredSpineIndices.size(); i++) {
if (filteredSpineIndices[i] == currentSpineIndex) {
selectorIndex = i;
break;
}
}
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionScreen::taskTrampoline, "EpubReaderChapterSelectionScreenTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void EpubReaderChapterSelectionScreen::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionScreen::buildFilteredChapterList() {
void EpubReaderChapterSelectionActivity::buildFilteredChapterList() {
filteredSpineIndices.clear();
for (int i = 0; i < epub->getSpineItemsCount(); i++) {
@ -77,7 +37,47 @@ void EpubReaderChapterSelectionScreen::buildFilteredChapterList() {
epub->getSpineItemsCount());
}
void EpubReaderChapterSelectionScreen::handleInput() {
void EpubReaderChapterSelectionActivity::onEnter() {
if (!epub) {
return;
}
renderingMutex = xSemaphoreCreateMutex();
// Build filtered chapter list (excluding footnote pages)
buildFilteredChapterList();
// Find the index in filtered list that corresponds to currentSpineIndex
selectorIndex = 0;
for (size_t i = 0; i < filteredSpineIndices.size(); i++) {
if (filteredSpineIndices[i] == currentSpineIndex) {
selectorIndex = i;
break;
}
}
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void EpubReaderChapterSelectionActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionActivity::loop() {
const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased =
@ -110,7 +110,7 @@ void EpubReaderChapterSelectionScreen::handleInput() {
}
}
void EpubReaderChapterSelectionScreen::displayTaskLoop() {
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -122,7 +122,7 @@ void EpubReaderChapterSelectionScreen::displayTaskLoop() {
}
}
void EpubReaderChapterSelectionScreen::renderScreen() {
void EpubReaderChapterSelectionActivity::renderScreen() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
@ -151,4 +151,4 @@ void EpubReaderChapterSelectionScreen::renderScreen() {
}
renderer.displayBuffer();
}
}

View File

@ -7,9 +7,9 @@
#include <memory>
#include <vector>
#include "Screen.h"
#include "../Activity.h"
class EpubReaderChapterSelectionScreen final : public Screen {
class EpubReaderChapterSelectionActivity final : public Activity {
std::shared_ptr<Epub> epub;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
@ -28,16 +28,16 @@ class EpubReaderChapterSelectionScreen final : public Screen {
void buildFilteredChapterList();
public:
explicit EpubReaderChapterSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Screen(renderer, inputManager),
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::shared_ptr<Epub>& epub, const int currentSpineIndex,
const std::function<void()>& onGoBack,
const std::function<void(int newSpineIndex)>& onSelectSpineIndex)
: Activity(renderer, inputManager),
epub(epub),
currentSpineIndex(currentSpineIndex),
onGoBack(onGoBack),
onSelectSpineIndex(onSelectSpineIndex) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
};
void loop() override;
};

View File

@ -1,19 +1,19 @@
#include "EpubReaderFootnotesScreen.h"
#include "EpubReaderFootnotesActivity.h"
#include <GfxRenderer.h>
#include "config.h"
void EpubReaderFootnotesScreen::onEnter() {
void EpubReaderFootnotesActivity::onEnter() {
selectedIndex = 0;
render();
}
void EpubReaderFootnotesScreen::onExit() {
void EpubReaderFootnotesActivity::onExit() {
// Nothing to clean up
}
void EpubReaderFootnotesScreen::handleInput() {
void EpubReaderFootnotesActivity::loop() {
if (inputManager.wasPressed(InputManager::BTN_BACK)) {
onGoBack();
return;
@ -23,8 +23,6 @@ void EpubReaderFootnotesScreen::handleInput() {
const FootnoteEntry* entry = footnotes.getEntry(selectedIndex);
if (entry) {
Serial.printf("[%lu] [FNS] Selected footnote: %s -> %s\n", millis(), entry->number, entry->href);
// Appeler le callback - EpubReaderScreen gère la navigation
onSelectFootnote(entry->href);
}
return;
@ -51,7 +49,7 @@ void EpubReaderFootnotesScreen::handleInput() {
}
}
void EpubReaderFootnotesScreen::render() {
void EpubReaderFootnotesActivity::render() {
renderer.clearScreen();
constexpr int startY = 50;
@ -88,4 +86,4 @@ void EpubReaderFootnotesScreen::render() {
"UP/DOWN: Select CONFIRM: Go to footnote BACK: Return");
renderer.displayBuffer();
}
}

View File

@ -4,7 +4,7 @@
#include <memory>
#include "../../lib/Epub/Epub/FootnoteEntry.h"
#include "Screen.h"
#include "../Activity.h"
class FootnotesData {
private:
@ -36,17 +36,17 @@ class FootnotesData {
}
};
class EpubReaderFootnotesScreen final : public Screen {
class EpubReaderFootnotesActivity final : public Activity {
const FootnotesData& footnotes;
const std::function<void()> onGoBack;
const std::function<void(const char*)> onSelectFootnote;
int selectedIndex;
public:
EpubReaderFootnotesScreen(GfxRenderer& renderer, InputManager& inputManager, const FootnotesData& footnotes,
const std::function<void()>& onGoBack,
const std::function<void(const char*)>& onSelectFootnote)
: Screen(renderer, inputManager),
EpubReaderFootnotesActivity(GfxRenderer& renderer, InputManager& inputManager, const FootnotesData& footnotes,
const std::function<void()>& onGoBack,
const std::function<void(const char*)>& onSelectFootnote)
: Activity(renderer, inputManager),
footnotes(footnotes),
onGoBack(onGoBack),
onSelectFootnote(onSelectFootnote),
@ -54,8 +54,8 @@ class EpubReaderFootnotesScreen final : public Screen {
void onEnter() override;
void onExit() override;
void handleInput() override;
void loop() override;
private:
void render();
};
};

View File

@ -1,7 +1,4 @@
//
// Created by jlaunay on 13/12/2025.
//
#include "EpubReaderMenuScreen.h"
#include "EpubReaderMenuActivity.h"
#include <GfxRenderer.h>
@ -9,18 +6,18 @@
constexpr int MENU_ITEMS_COUNT = 2;
void EpubReaderMenuScreen::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderMenuScreen*>(param);
void EpubReaderMenuActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderMenuActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderMenuScreen::onEnter() {
void EpubReaderMenuActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderMenuScreen::taskTrampoline, "EpubReaderMenuTask",
xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubReaderMenuTask",
2048, // Stack size
this, // Parameters
1, // Priority
@ -28,7 +25,7 @@ void EpubReaderMenuScreen::onEnter() {
);
}
void EpubReaderMenuScreen::onExit() {
void EpubReaderMenuActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
@ -39,7 +36,7 @@ void EpubReaderMenuScreen::onExit() {
renderingMutex = nullptr;
}
void EpubReaderMenuScreen::handleInput() {
void EpubReaderMenuActivity::loop() {
const bool prevReleased =
inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased =
@ -58,7 +55,7 @@ void EpubReaderMenuScreen::handleInput() {
}
}
void EpubReaderMenuScreen::displayTaskLoop() {
void EpubReaderMenuActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -70,7 +67,7 @@ void EpubReaderMenuScreen::displayTaskLoop() {
}
}
void EpubReaderMenuScreen::renderScreen() {
void EpubReaderMenuActivity::renderScreen() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
@ -94,4 +91,4 @@ void EpubReaderMenuScreen::renderScreen() {
}
renderer.displayBuffer();
}
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "../Activity.h"
class EpubReaderMenuActivity final : public Activity {
public:
enum MenuOption { CHAPTERS, FOOTNOTES };
private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void(MenuOption option)> onSelectOption;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public:
explicit EpubReaderMenuActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onGoBack,
const std::function<void(MenuOption option)>& onSelectOption)
: Activity(renderer, inputManager), onGoBack(onGoBack), onSelectOption(onSelectOption) {}
void onEnter() override;
void onExit() override;
void loop() override;
};

View File

@ -1,4 +1,4 @@
#include "FileSelectionScreen.h"
#include "FileSelectionActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
@ -15,12 +15,12 @@ void sortFileList(std::vector<std::string>& strs) {
});
}
void FileSelectionScreen::taskTrampoline(void* param) {
auto* self = static_cast<FileSelectionScreen*>(param);
void FileSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<FileSelectionActivity*>(param);
self->displayTaskLoop();
}
void FileSelectionScreen::loadFiles() {
void FileSelectionActivity::loadFiles() {
files.clear();
selectorIndex = 0;
auto root = SD.open(basepath.c_str());
@ -42,7 +42,7 @@ void FileSelectionScreen::loadFiles() {
sortFileList(files);
}
void FileSelectionScreen::onEnter() {
void FileSelectionActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
basepath = "/";
@ -52,7 +52,7 @@ void FileSelectionScreen::onEnter() {
// Trigger first update
updateRequired = true;
xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask",
xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
@ -60,7 +60,7 @@ void FileSelectionScreen::onEnter() {
);
}
void FileSelectionScreen::onExit() {
void FileSelectionActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
@ -72,7 +72,7 @@ void FileSelectionScreen::onExit() {
files.clear();
}
void FileSelectionScreen::handleInput() {
void FileSelectionActivity::loop() {
const bool prevPressed =
inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT);
const bool nextPressed =
@ -110,7 +110,7 @@ void FileSelectionScreen::handleInput() {
}
}
void FileSelectionScreen::displayTaskLoop() {
void FileSelectionActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -122,7 +122,7 @@ void FileSelectionScreen::displayTaskLoop() {
}
}
void FileSelectionScreen::render() const {
void FileSelectionActivity::render() const {
renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth();

View File

@ -7,9 +7,9 @@
#include <string>
#include <vector>
#include "Screen.h"
#include "../Activity.h"
class FileSelectionScreen final : public Screen {
class FileSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::string basepath = "/";
@ -25,11 +25,11 @@ class FileSelectionScreen final : public Screen {
void loadFiles();
public:
explicit FileSelectionScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {}
explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onGoHome)
: Activity(renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
void loop() override;
};

View File

@ -0,0 +1,68 @@
#include "ReaderActivity.h"
#include <SD.h>
#include "CrossPointState.h"
#include "Epub.h"
#include "EpubReaderActivity.h"
#include "FileSelectionActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
return nullptr;
}
void ReaderActivity::onSelectEpubFile(const std::string& path) {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading..."));
auto epub = loadEpub(path);
if (epub) {
APP_STATE.openEpubPath = path;
APP_STATE.saveToFile();
onGoToEpubReader(std::move(epub));
} else {
exitActivity();
enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Failed to load epub", REGULAR,
EInkDisplay::HALF_REFRESH));
delay(2000);
onGoToFileSelection();
}
}
void ReaderActivity::onGoToFileSelection() {
exitActivity();
enterNewActivity(new FileSelectionActivity(
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack));
}
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
exitActivity();
enterNewActivity(new EpubReaderActivity(renderer, inputManager, std::move(epub), [this] { onGoToFileSelection(); }));
}
void ReaderActivity::onEnter() {
if (initialEpubPath.empty()) {
onGoToFileSelection();
return;
}
auto epub = loadEpub(initialEpubPath);
if (!epub) {
onGoBack();
return;
}
onGoToEpubReader(std::move(epub));
}

View File

@ -0,0 +1,24 @@
#pragma once
#include <memory>
#include "../ActivityWithSubactivity.h"
class Epub;
class ReaderActivity final : public ActivityWithSubactivity {
std::string initialEpubPath;
const std::function<void()> onGoBack;
static std::unique_ptr<Epub> loadEpub(const std::string& path);
void onSelectEpubFile(const std::string& path);
void onGoToFileSelection();
void onGoToEpubReader(std::unique_ptr<Epub> epub);
public:
explicit ReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::string initialEpubPath,
const std::function<void()>& onGoBack)
: ActivityWithSubactivity(renderer, inputManager),
initialEpubPath(std::move(initialEpubPath)),
onGoBack(onGoBack) {}
void onEnter() override;
};

View File

@ -1,4 +1,4 @@
#include "SettingsScreen.h"
#include "SettingsActivity.h"
#include <GfxRenderer.h>
@ -7,16 +7,16 @@
// Define the static settings list
const SettingInfo SettingsScreen::settingsList[SettingsScreen::settingsCount] = {
const SettingInfo SettingsActivity::settingsList[settingsCount] = {
{"White Sleep Screen", &CrossPointSettings::whiteSleepScreen},
{"Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing}};
void SettingsScreen::taskTrampoline(void* param) {
auto* self = static_cast<SettingsScreen*>(param);
void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param);
self->displayTaskLoop();
}
void SettingsScreen::onEnter() {
void SettingsActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first item
@ -25,7 +25,7 @@ void SettingsScreen::onEnter() {
// Trigger first update
updateRequired = true;
xTaskCreate(&SettingsScreen::taskTrampoline, "SettingsScreenTask",
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
2048, // Stack size
this, // Parameters
1, // Priority
@ -33,7 +33,7 @@ void SettingsScreen::onEnter() {
);
}
void SettingsScreen::onExit() {
void SettingsActivity::onExit() {
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
@ -44,7 +44,7 @@ void SettingsScreen::onExit() {
renderingMutex = nullptr;
}
void SettingsScreen::handleInput() {
void SettingsActivity::loop() {
// Handle actions with early return
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
toggleCurrentSetting();
@ -70,7 +70,7 @@ void SettingsScreen::handleInput() {
}
}
void SettingsScreen::toggleCurrentSetting() {
void SettingsActivity::toggleCurrentSetting() {
// Validate index
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
return;
@ -84,7 +84,7 @@ void SettingsScreen::toggleCurrentSetting() {
SETTINGS.saveToFile();
}
void SettingsScreen::displayTaskLoop() {
void SettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
@ -96,7 +96,7 @@ void SettingsScreen::displayTaskLoop() {
}
}
void SettingsScreen::render() const {
void SettingsActivity::render() const {
renderer.clearScreen();
const auto pageWidth = GfxRenderer::getScreenWidth();

View File

@ -7,7 +7,7 @@
#include <string>
#include <vector>
#include "Screen.h"
#include "../Activity.h"
class CrossPointSettings;
@ -17,7 +17,7 @@ struct SettingInfo {
uint8_t CrossPointSettings::* valuePtr; // Pointer to member in CrossPointSettings
};
class SettingsScreen final : public Screen {
class SettingsActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
@ -34,9 +34,9 @@ class SettingsScreen final : public Screen {
void toggleCurrentSetting();
public:
explicit SettingsScreen(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: Screen(renderer, inputManager), onGoHome(onGoHome) {}
explicit SettingsActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function<void()>& onGoHome)
: Activity(renderer, inputManager), onGoHome(onGoHome) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
void loop() override;
};

View File

@ -1,10 +1,10 @@
#include "FullScreenMessageScreen.h"
#include "FullScreenMessageActivity.h"
#include <GfxRenderer.h>
#include "config.h"
void FullScreenMessageScreen::onEnter() {
void FullScreenMessageActivity::onEnter() {
const auto height = renderer.getLineHeight(UI_FONT_ID);
const auto top = (GfxRenderer::getScreenHeight() - height) / 2;

View File

@ -0,0 +1,21 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <string>
#include <utility>
#include "../Activity.h"
class FullScreenMessageActivity final : public Activity {
std::string text;
EpdFontStyle style;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageActivity(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Activity(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -16,14 +16,13 @@
#include "Battery.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h"
#include "activities/home/HomeActivity.h"
#include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "config.h"
#include "screens/BootLogoScreen.h"
#include "screens/EpubReaderScreen.h"
#include "screens/FileSelectionScreen.h"
#include "screens/FullScreenMessageScreen.h"
#include "screens/HomeScreen.h"
#include "screens/SettingsScreen.h"
#include "screens/SleepScreen.h"
#define SPI_FQ 40000000
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
@ -42,8 +41,7 @@
EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager;
GfxRenderer renderer(einkDisplay);
Screen* currentScreen;
CrossPointState appState;
Activity* currentActivity;
// Fonts
EpdFont bookerlyFont(&bookerly_2b);
@ -67,31 +65,16 @@ constexpr unsigned long POWER_BUTTON_SLEEP_MS = 500;
// Auto-sleep timeout (10 minutes of inactivity)
constexpr unsigned long AUTO_SLEEP_TIMEOUT_MS = 10 * 60 * 1000;
std::unique_ptr<Epub> loadEpub(const std::string& path) {
if (!SD.exists(path.c_str())) {
Serial.printf("[%lu] [ ] File does not exist: %s\n", millis(), path.c_str());
return nullptr;
}
auto epub = std::unique_ptr<Epub>(new Epub(path, "/.crosspoint"));
if (epub->load()) {
return epub;
}
Serial.printf("[%lu] [ ] Failed to load epub\n", millis());
return nullptr;
}
void exitScreen() {
if (currentScreen) {
currentScreen->onExit();
delete currentScreen;
void exitActivity() {
if (currentActivity) {
currentActivity->onExit();
delete currentActivity;
}
}
void enterNewScreen(Screen* screen) {
currentScreen = screen;
currentScreen->onEnter();
void enterNewActivity(Activity* activity) {
currentActivity = activity;
currentActivity->onEnter();
}
// Verify long press on wake-up from deep sleep
@ -135,8 +118,8 @@ void waitForPowerRelease() {
// Enter deep sleep mode
void enterDeepSleep() {
exitScreen();
enterNewScreen(new SleepScreen(renderer, inputManager));
exitActivity();
enterNewActivity(new SleepActivity(renderer, inputManager));
Serial.printf("[%lu] [ ] Power button released after a long press. Entering deep sleep.\n", millis());
delay(1000); // Allow Serial buffer to empty and display to update
@ -151,39 +134,20 @@ void enterDeepSleep() {
}
void onGoHome();
void onGoToFileSelection();
void onSelectEpubFile(const std::string& path) {
exitScreen();
enterNewScreen(new FullScreenMessageScreen(renderer, inputManager, "Loading..."));
auto epub = loadEpub(path);
if (epub) {
appState.openEpubPath = path;
appState.saveToFile();
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoToFileSelection));
} else {
exitScreen();
enterNewScreen(
new FullScreenMessageScreen(renderer, inputManager, "Failed to load epub", REGULAR, EInkDisplay::HALF_REFRESH));
delay(2000);
onGoToFileSelection();
}
}
void onGoToFileSelection() {
exitScreen();
enterNewScreen(new FileSelectionScreen(renderer, inputManager, onSelectEpubFile, onGoHome));
void onGoToReader(const std::string& initialEpubPath) {
exitActivity();
enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome));
}
void onGoToReaderHome() { onGoToReader(std::string()); }
void onGoToSettings() {
exitScreen();
enterNewScreen(new SettingsScreen(renderer, inputManager, onGoHome));
exitActivity();
enterNewActivity(new SettingsActivity(renderer, inputManager, onGoHome));
}
void onGoHome() {
exitScreen();
enterNewScreen(new HomeScreen(renderer, inputManager, onGoToFileSelection, onGoToSettings));
exitActivity();
enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings));
}
void setup() {
@ -209,27 +173,20 @@ void setup() {
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
Serial.printf("[%lu] [ ] Fonts setup\n", millis());
exitScreen();
enterNewScreen(new BootLogoScreen(renderer, inputManager));
exitActivity();
enterNewActivity(new BootActivity(renderer, inputManager));
// SD Card Initialization
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
SETTINGS.loadFromFile();
appState.loadFromFile();
if (!appState.openEpubPath.empty()) {
auto epub = loadEpub(appState.openEpubPath);
if (epub) {
exitScreen();
enterNewScreen(new EpubReaderScreen(renderer, inputManager, std::move(epub), onGoHome));
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
return;
}
APP_STATE.loadFromFile();
if (APP_STATE.openEpubPath.empty()) {
onGoHome();
} else {
onGoToReader(APP_STATE.openEpubPath);
}
onGoHome();
// Ensure we're not still holding the power button before leaving setup
waitForPowerRelease();
}
@ -265,7 +222,7 @@ void loop() {
return;
}
if (currentScreen) {
currentScreen->handleInput();
if (currentActivity) {
currentActivity->loop();
}
}

View File

@ -1,8 +0,0 @@
#pragma once
#include "Screen.h"
class BootLogoScreen final : public Screen {
public:
explicit BootLogoScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};

View File

@ -1,40 +0,0 @@
//
// Created by jlaunay on 13/12/2025.
//
#ifndef CROSSPOINT_READER_EPUBREADERMENUSCREEN_H
#define CROSSPOINT_READER_EPUBREADERMENUSCREEN_H
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "Screen.h"
class EpubReaderMenuScreen final : public Screen {
public:
enum MenuOption { CHAPTERS, FOOTNOTES };
private:
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void(MenuOption option)> onSelectOption;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public:
explicit EpubReaderMenuScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onGoBack,
const std::function<void(MenuOption option)>& onSelectOption)
: Screen(renderer, inputManager), onGoBack(onGoBack), onSelectOption(onSelectOption) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
};
#endif // CROSSPOINT_READER_EPUBREADERMENUSCREEN_H

View File

@ -1,21 +0,0 @@
#pragma once
#include <EInkDisplay.h>
#include <EpdFontFamily.h>
#include <string>
#include <utility>
#include "Screen.h"
class FullScreenMessageScreen final : public Screen {
std::string text;
EpdFontStyle style;
EInkDisplay::RefreshMode refreshMode;
public:
explicit FullScreenMessageScreen(GfxRenderer& renderer, InputManager& inputManager, std::string text,
const EpdFontStyle style = REGULAR,
const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH)
: Screen(renderer, inputManager), text(std::move(text)), style(style), refreshMode(refreshMode) {}
void onEnter() override;
};

View File

@ -1,31 +0,0 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include "Screen.h"
class HomeScreen final : public Screen {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onFileSelectionOpen;
const std::function<void()> onSettingsOpen;
static constexpr int menuItemCount = 2;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render() const;
public:
explicit HomeScreen(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void()>& onFileSelectionOpen, const std::function<void()>& onSettingsOpen)
: Screen(renderer, inputManager), onFileSelectionOpen(onFileSelectionOpen), onSettingsOpen(onSettingsOpen) {}
void onEnter() override;
void onExit() override;
void handleInput() override;
};

View File

@ -1,17 +0,0 @@
#pragma once
#include <InputManager.h>
class GfxRenderer;
class Screen {
protected:
GfxRenderer& renderer;
InputManager& inputManager;
public:
explicit Screen(GfxRenderer& renderer, InputManager& inputManager) : renderer(renderer), inputManager(inputManager) {}
virtual ~Screen() = default;
virtual void onEnter() {}
virtual void onExit() {}
virtual void handleInput() {}
};

View File

@ -1,8 +0,0 @@
#pragma once
#include "Screen.h"
class SleepScreen final : public Screen {
public:
explicit SleepScreen(GfxRenderer& renderer, InputManager& inputManager) : Screen(renderer, inputManager) {}
void onEnter() override;
};