diff --git a/lib/EpdFont/EpdFont.cpp b/lib/EpdFont/EpdFont.cpp index 0f53f95b..661317d6 100644 --- a/lib/EpdFont/EpdFont.cpp +++ b/lib/EpdFont/EpdFont.cpp @@ -59,14 +59,28 @@ bool EpdFont::hasPrintableChars(const char* string) const { const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const { const EpdUnicodeInterval* intervals = data->intervals; - for (int i = 0; i < data->intervalCount; i++) { - const EpdUnicodeInterval* interval = &intervals[i]; - if (cp >= interval->first && cp <= interval->last) { + const int count = data->intervalCount; + + if (count == 0) return nullptr; + + // Binary search for O(log n) lookup instead of O(n) + // Critical for Korean fonts with many unicode intervals + int left = 0; + int right = count - 1; + + while (left <= right) { + const int mid = left + (right - left) / 2; + const EpdUnicodeInterval* interval = &intervals[mid]; + + if (cp < interval->first) { + right = mid - 1; + } else if (cp > interval->last) { + left = mid + 1; + } else { + // Found: cp >= interval->first && cp <= interval->last return &data->glyph[interval->offset + (cp - interval->first)]; } - if (cp < interval->first) { - return nullptr; - } } + return nullptr; } diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index a4b9369b..6433748e 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -239,6 +239,28 @@ int GfxRenderer::getLineHeight(const int fontId) const { return fontMap.at(fontId).getData(REGULAR)->advanceY; } +void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3, + const char* btn4) const { + const int pageHeight = getScreenHeight(); + constexpr int buttonWidth = 106; + constexpr int buttonHeight = 40; + constexpr int buttonY = 40; // Distance from bottom + constexpr int textYOffset = 5; // Distance from top of button to text baseline + constexpr int buttonPositions[] = {25, 130, 245, 350}; + const char* labels[] = {btn1, btn2, btn3, btn4}; + + for (int i = 0; i < 4; i++) { + // Only draw if the label is non-empty + if (labels[i] != nullptr && labels[i][0] != '\0') { + const int x = buttonPositions[i]; + drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight); + const int textWidth = getTextWidth(fontId, labels[i]); + const int textX = x + (buttonWidth - 1 - textWidth) / 2; + drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]); + } + } +} + uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 838e0180..00a525dd 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -57,6 +57,9 @@ class GfxRenderer { int getSpaceWidth(int fontId) const; int getLineHeight(int fontId) const; + // UI Components + void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const; + // Grayscale functions void setRenderMode(const RenderMode mode) { this->renderMode = mode; } void copyGrayscaleLsbBuffers() const; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index bbda1307..38dc8542 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -4,22 +4,24 @@ #include #include +#include "CrossPointState.h" #include "config.h" -namespace { -constexpr int menuItemCount = 3; -} - void HomeActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); } +int HomeActivity::getMenuItemCount() const { return hasContinueReading ? 4 : 3; } + void HomeActivity::onEnter() { Activity::onEnter(); renderingMutex = xSemaphoreCreateMutex(); + // Check if we have a book to continue reading + hasContinueReading = !APP_STATE.openEpubPath.empty() && SD.exists(APP_STATE.openEpubPath.c_str()); + selectorIndex = 0; // Trigger first update @@ -52,19 +54,35 @@ void HomeActivity::loop() { const bool nextPressed = inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT); + const int menuCount = getMenuItemCount(); + if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { - if (selectorIndex == 0) { - onReaderOpen(); - } else if (selectorIndex == 1) { - onFileTransferOpen(); - } else if (selectorIndex == 2) { - onSettingsOpen(); + if (hasContinueReading) { + // Menu: Continue Reading, Browse, File transfer, Settings + if (selectorIndex == 0) { + onContinueReading(); + } else if (selectorIndex == 1) { + onReaderOpen(); + } else if (selectorIndex == 2) { + onFileTransferOpen(); + } else if (selectorIndex == 3) { + onSettingsOpen(); + } + } else { + // Menu: Browse, File transfer, Settings + if (selectorIndex == 0) { + onReaderOpen(); + } else if (selectorIndex == 1) { + onFileTransferOpen(); + } else if (selectorIndex == 2) { + onSettingsOpen(); + } } } else if (prevPressed) { - selectorIndex = (selectorIndex + menuItemCount - 1) % menuItemCount; + selectorIndex = (selectorIndex + menuCount - 1) % menuCount; updateRequired = true; } else if (nextPressed) { - selectorIndex = (selectorIndex + 1) % menuItemCount; + selectorIndex = (selectorIndex + 1) % menuCount; updateRequired = true; } } @@ -85,27 +103,47 @@ void HomeActivity::render() const { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); renderer.drawCenteredText(READER_FONT_ID, 10, "CrossPoint Reader", true, BOLD); // Draw selection renderer.fillRect(0, 60 + selectorIndex * 30 + 2, pageWidth - 1, 30); - renderer.drawText(UI_FONT_ID, 20, 60, "Read", selectorIndex != 0); - renderer.drawText(UI_FONT_ID, 20, 90, "File transfer", selectorIndex != 1); - renderer.drawText(UI_FONT_ID, 20, 120, "Settings", selectorIndex != 2); - renderer.drawRect(25, pageHeight - 40, 106, 40); - renderer.drawText(UI_FONT_ID, 25 + (105 - renderer.getTextWidth(UI_FONT_ID, "Back")) / 2, pageHeight - 35, "Back"); + int menuY = 60; + int menuIndex = 0; - renderer.drawRect(130, pageHeight - 40, 106, 40); - renderer.drawText(UI_FONT_ID, 130 + (105 - renderer.getTextWidth(UI_FONT_ID, "Confirm")) / 2, pageHeight - 35, - "Confirm"); + if (hasContinueReading) { + // Extract filename from path for display + std::string bookName = APP_STATE.openEpubPath; + const size_t lastSlash = bookName.find_last_of('/'); + if (lastSlash != std::string::npos) { + bookName = bookName.substr(lastSlash + 1); + } + // Remove .epub extension + if (bookName.length() > 5 && bookName.substr(bookName.length() - 5) == ".epub") { + bookName.resize(bookName.length() - 5); + } + // Truncate if too long + if (bookName.length() > 25) { + bookName.resize(22); + bookName += "..."; + } + std::string continueLabel = "Continue: " + bookName; + renderer.drawText(UI_FONT_ID, 20, menuY, continueLabel.c_str(), selectorIndex != menuIndex); + menuY += 30; + menuIndex++; + } - renderer.drawRect(245, pageHeight - 40, 106, 40); - renderer.drawText(UI_FONT_ID, 245 + (105 - renderer.getTextWidth(UI_FONT_ID, "Left")) / 2, pageHeight - 35, "Left"); + renderer.drawText(UI_FONT_ID, 20, menuY, "Browse", selectorIndex != menuIndex); + menuY += 30; + menuIndex++; - renderer.drawRect(350, pageHeight - 40, 106, 40); - renderer.drawText(UI_FONT_ID, 350 + (105 - renderer.getTextWidth(UI_FONT_ID, "Right")) / 2, pageHeight - 35, "Right"); + renderer.drawText(UI_FONT_ID, 20, menuY, "File transfer", selectorIndex != menuIndex); + menuY += 30; + menuIndex++; + + renderer.drawText(UI_FONT_ID, 20, menuY, "Settings", selectorIndex != menuIndex); + + renderer.drawButtonHints(UI_FONT_ID, "Back", "Confirm", "Left", "Right"); renderer.displayBuffer(); } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 943a4665..0704819c 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -12,6 +12,8 @@ class HomeActivity final : public Activity { SemaphoreHandle_t renderingMutex = nullptr; int selectorIndex = 0; bool updateRequired = false; + bool hasContinueReading = false; + const std::function onContinueReading; const std::function onReaderOpen; const std::function onSettingsOpen; const std::function onFileTransferOpen; @@ -19,11 +21,14 @@ class HomeActivity final : public Activity { static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; + int getMenuItemCount() const; public: - explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function& onReaderOpen, + explicit HomeActivity(GfxRenderer& renderer, InputManager& inputManager, + const std::function& onContinueReading, const std::function& onReaderOpen, const std::function& onSettingsOpen, const std::function& onFileTransferOpen) : Activity("Home", renderer, inputManager), + onContinueReading(onContinueReading), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen) {} diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index bc72c9c9..e5f9f49c 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -363,8 +363,6 @@ void drawQRCode(GfxRenderer& renderer, const int x, const int y, const std::stri } void CrossPointWebServerActivity::renderServerRunning() const { - const auto pageHeight = renderer.getScreenHeight(); - // Use consistent line spacing constexpr int LINE_SPACING = 28; // Space between lines @@ -431,5 +429,5 @@ void CrossPointWebServerActivity::renderServerRunning() const { REGULAR); } - renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press BACK to exit", true, REGULAR); + renderer.drawButtonHints(UI_FONT_ID, "« Exit", "", "", ""); } diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 637d82d9..af68a20b 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -122,7 +122,7 @@ void NetworkModeSelectionActivity::render() const { } // Draw help text at bottom - renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press OK to select, BACK to cancel", true, REGULAR); + renderer.drawButtonHints(UI_FONT_ID, "« Back", "Select", "", ""); renderer.displayBuffer(); } diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 3495ff0a..68c6481e 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -553,11 +553,12 @@ void WifiSelectionActivity::renderNetworkList() const { // Show network count char countStr[32]; snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size()); - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 45, countStr); + renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); } // Draw help text - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "OK: Connect | * = Encrypted | + = Saved"); + renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); + renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", ""); } void WifiSelectionActivity::renderPasswordEntry() const { diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6195ec22..f4905d60 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -14,6 +14,7 @@ namespace { constexpr int pagesPerRefresh = 15; constexpr unsigned long skipChapterMs = 700; +constexpr unsigned long goHomeMs = 1000; constexpr float lineCompression = 0.95f; constexpr int marginTop = 8; constexpr int marginRight = 10; @@ -108,7 +109,14 @@ void EpubReaderActivity::loop() { xSemaphoreGive(renderingMutex); } - if (inputManager.wasPressed(InputManager::BTN_BACK)) { + // Long press BACK (1s+) goes directly to home + if (inputManager.isPressed(InputManager::BTN_BACK) && inputManager.getHeldTime() >= goHomeMs) { + onGoHome(); + return; + } + + // Short press BACK goes to file selection + if (inputManager.wasReleased(InputManager::BTN_BACK) && inputManager.getHeldTime() < goHomeMs) { onGoBack(); return; } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 4edbabc2..143f56b1 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -17,6 +17,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity { int pagesUntilFullRefresh = 0; bool updateRequired = false; const std::function onGoBack; + const std::function onGoHome; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -26,8 +27,11 @@ class EpubReaderActivity final : public ActivityWithSubactivity { public: explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr epub, - const std::function& onGoBack) - : ActivityWithSubactivity("EpubReader", renderer, inputManager), epub(std::move(epub)), onGoBack(onGoBack) {} + const std::function& onGoBack, const std::function& onGoHome) + : ActivityWithSubactivity("EpubReader", renderer, inputManager), + epub(std::move(epub)), + onGoBack(onGoBack), + onGoHome(onGoHome) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/FileSelectionActivity.cpp b/src/activities/reader/FileSelectionActivity.cpp index a6c10834..853b06f1 100644 --- a/src/activities/reader/FileSelectionActivity.cpp +++ b/src/activities/reader/FileSelectionActivity.cpp @@ -9,6 +9,7 @@ namespace { constexpr int PAGE_ITEMS = 23; constexpr int SKIP_PAGE_MS = 700; +constexpr unsigned long GO_HOME_MS = 1000; } // namespace void sortFileList(std::vector& strs) { @@ -53,7 +54,7 @@ void FileSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); - basepath = "/"; + // basepath is set via constructor parameter (defaults to "/" if not specified) loadFiles(); selectorIndex = 0; @@ -83,6 +84,16 @@ void FileSelectionActivity::onExit() { } void FileSelectionActivity::loop() { + // Long press BACK (1s+) goes to root folder + if (inputManager.isPressed(InputManager::BTN_BACK) && inputManager.getHeldTime() >= GO_HOME_MS) { + if (basepath != "/") { + basepath = "/"; + loadFiles(); + updateRequired = true; + } + return; + } + const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT); const bool nextReleased = @@ -103,15 +114,17 @@ void FileSelectionActivity::loop() { } else { onSelect(basepath + files[selectorIndex]); } - } else if (inputManager.wasPressed(InputManager::BTN_BACK)) { - if (basepath != "/") { - basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) basepath = "/"; - loadFiles(); - updateRequired = true; - } else { - // At root level, go back home - onGoHome(); + } else if (inputManager.wasReleased(InputManager::BTN_BACK)) { + // Short press: go up one directory, or go home if at root + if (inputManager.getHeldTime() < GO_HOME_MS) { + if (basepath != "/") { + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); + if (basepath.empty()) basepath = "/"; + loadFiles(); + updateRequired = true; + } else { + onGoHome(); + } } } else if (prevReleased) { if (skipPage) { @@ -149,7 +162,7 @@ void FileSelectionActivity::render() const { renderer.drawCenteredText(READER_FONT_ID, 10, "Books", true, BOLD); // Help text - renderer.drawText(SMALL_FONT_ID, 20, GfxRenderer::getScreenHeight() - 30, "Press BACK for Home"); + renderer.drawButtonHints(UI_FONT_ID, "« Home", "", "", ""); if (files.empty()) { renderer.drawText(UI_FONT_ID, 20, 60, "No EPUBs found"); diff --git a/src/activities/reader/FileSelectionActivity.h b/src/activities/reader/FileSelectionActivity.h index 2a8f8ae1..f642e209 100644 --- a/src/activities/reader/FileSelectionActivity.h +++ b/src/activities/reader/FileSelectionActivity.h @@ -27,8 +27,11 @@ class FileSelectionActivity final : public Activity { public: explicit FileSelectionActivity(GfxRenderer& renderer, InputManager& inputManager, const std::function& onSelect, - const std::function& onGoHome) - : Activity("FileSelection", renderer, inputManager), onSelect(onSelect), onGoHome(onGoHome) {} + const std::function& onGoHome, std::string initialPath = "/") + : Activity("FileSelection", renderer, inputManager), + basepath(initialPath.empty() ? "/" : std::move(initialPath)), + onSelect(onSelect), + onGoHome(onGoHome) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index d888fb6e..519a33a2 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -7,6 +7,14 @@ #include "FileSelectionActivity.h" #include "activities/util/FullScreenMessageActivity.h" +std::string ReaderActivity::extractFolderPath(const std::string& filePath) { + const auto lastSlash = filePath.find_last_of('/'); + if (lastSlash == std::string::npos || lastSlash == 0) { + return "/"; + } + return filePath.substr(0, lastSlash); +} + std::unique_ptr 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()); @@ -23,6 +31,7 @@ std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { } void ReaderActivity::onSelectEpubFile(const std::string& path) { + currentEpubPath = path; // Track current book path exitActivity(); enterNewActivity(new FullScreenMessageActivity(renderer, inputManager, "Loading...")); @@ -38,25 +47,32 @@ void ReaderActivity::onSelectEpubFile(const std::string& path) { } } -void ReaderActivity::onGoToFileSelection() { +void ReaderActivity::onGoToFileSelection(const std::string& fromEpubPath) { exitActivity(); + // If coming from a book, start in that book's folder; otherwise start from root + const auto initialPath = fromEpubPath.empty() ? "/" : extractFolderPath(fromEpubPath); enterNewActivity(new FileSelectionActivity( - renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack)); + renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath)); } void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { + const auto epubPath = epub->getPath(); + currentEpubPath = epubPath; exitActivity(); - enterNewActivity(new EpubReaderActivity(renderer, inputManager, std::move(epub), [this] { onGoToFileSelection(); })); + enterNewActivity(new EpubReaderActivity( + renderer, inputManager, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); }, + [this] { onGoBack(); })); } void ReaderActivity::onEnter() { ActivityWithSubactivity::onEnter(); if (initialEpubPath.empty()) { - onGoToFileSelection(); + onGoToFileSelection(); // Start from root when entering via Browse return; } + currentEpubPath = initialEpubPath; auto epub = loadEpub(initialEpubPath); if (!epub) { onGoBack(); diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index e566d6d3..5bb34193 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -7,11 +7,13 @@ class Epub; class ReaderActivity final : public ActivityWithSubactivity { std::string initialEpubPath; + std::string currentEpubPath; // Track current book path for navigation const std::function onGoBack; static std::unique_ptr loadEpub(const std::string& path); + static std::string extractFolderPath(const std::string& filePath); void onSelectEpubFile(const std::string& path); - void onGoToFileSelection(); + void onGoToFileSelection(const std::string& fromEpubPath = ""); void onGoToEpubReader(std::unique_ptr epub); public: diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 37f2e5a1..f7af052e 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -169,7 +169,7 @@ void SettingsActivity::render() const { } // Draw help text - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 30, "Press OK to toggle, BACK to save & exit"); + renderer.drawButtonHints(UI_FONT_ID, "« Save", "Toggle", "", ""); renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), pageHeight - 30, CROSSPOINT_VERSION); diff --git a/src/main.cpp b/src/main.cpp index b71ea399..33e15f9e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -142,6 +142,7 @@ void onGoToReader(const std::string& initialEpubPath) { enterNewActivity(new ReaderActivity(renderer, inputManager, initialEpubPath, onGoHome)); } void onGoToReaderHome() { onGoToReader(std::string()); } +void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onGoToFileTransfer() { exitActivity(); @@ -155,7 +156,17 @@ void onGoToSettings() { void onGoHome() { exitActivity(); - enterNewActivity(new HomeActivity(renderer, inputManager, onGoToReaderHome, onGoToSettings, onGoToFileTransfer)); + enterNewActivity(new HomeActivity(renderer, inputManager, onContinueReading, onGoToReaderHome, onGoToSettings, + onGoToFileTransfer)); +} + +void setupDisplayAndFonts() { + einkDisplay.begin(); + Serial.printf("[%lu] [ ] Display initialized\n", millis()); + renderer.insertFont(READER_FONT_ID, bookerlyFontFamily); + renderer.insertFont(UI_FONT_ID, ubuntuFontFamily); + renderer.insertFont(SMALL_FONT_ID, smallFontFamily); + Serial.printf("[%lu] [ ] Fonts setup\n", millis()); } void setupDisplayAndFonts() {