From e4c8ef07dd86ef8112d3ff42a7b37f5f8f86b62a Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sun, 4 Jan 2026 19:15:54 -0500 Subject: [PATCH 01/11] Add recent books to home screen --- src/RecentBooksStore.cpp | 86 +++++++++ src/RecentBooksStore.h | 32 ++++ src/activities/home/HomeActivity.cpp | 76 +++++--- src/activities/home/HomeActivity.h | 8 +- src/activities/home/RecentBooksActivity.cpp | 182 +++++++++++++++++++ src/activities/home/RecentBooksActivity.h | 36 ++++ src/activities/reader/EpubReaderActivity.cpp | 4 +- src/activities/reader/XtcReaderActivity.cpp | 4 +- src/main.cpp | 12 +- 9 files changed, 408 insertions(+), 32 deletions(-) create mode 100644 src/RecentBooksStore.cpp create mode 100644 src/RecentBooksStore.h create mode 100644 src/activities/home/RecentBooksActivity.cpp create mode 100644 src/activities/home/RecentBooksActivity.h diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp new file mode 100644 index 00000000..03cfbbd7 --- /dev/null +++ b/src/RecentBooksStore.cpp @@ -0,0 +1,86 @@ +#include "RecentBooksStore.h" + +#include +#include +#include + +#include + +namespace { +constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 1; +constexpr char RECENT_BOOKS_FILE[] = "/.crosspoint/recent.bin"; +constexpr int MAX_RECENT_BOOKS = 10; +} // namespace + +RecentBooksStore RecentBooksStore::instance; + +void RecentBooksStore::addBook(const std::string& path) { + // Remove existing entry if present + auto it = std::find(recentBooks.begin(), recentBooks.end(), path); + if (it != recentBooks.end()) { + recentBooks.erase(it); + } + + // Add to front + recentBooks.insert(recentBooks.begin(), path); + + // Trim to max size + if (recentBooks.size() > MAX_RECENT_BOOKS) { + recentBooks.resize(MAX_RECENT_BOOKS); + } + + saveToFile(); +} + +bool RecentBooksStore::saveToFile() const { + // Make sure the directory exists + SdMan.mkdir("/.crosspoint"); + + FsFile outputFile; + if (!SdMan.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) { + return false; + } + + serialization::writePod(outputFile, RECENT_BOOKS_FILE_VERSION); + const uint8_t count = static_cast(recentBooks.size()); + serialization::writePod(outputFile, count); + + for (const auto& book : recentBooks) { + serialization::writeString(outputFile, book); + } + + outputFile.close(); + Serial.printf("[%lu] [RBS] Recent books saved to file (%d entries)\n", millis(), count); + return true; +} + +bool RecentBooksStore::loadFromFile() { + FsFile inputFile; + if (!SdMan.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) { + return false; + } + + uint8_t version; + serialization::readPod(inputFile, version); + if (version != RECENT_BOOKS_FILE_VERSION) { + Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version); + inputFile.close(); + return false; + } + + uint8_t count; + serialization::readPod(inputFile, count); + + recentBooks.clear(); + recentBooks.reserve(count); + + for (uint8_t i = 0; i < count; i++) { + std::string path; + serialization::readString(inputFile, path); + recentBooks.push_back(path); + } + + inputFile.close(); + Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), count); + return true; +} diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h new file mode 100644 index 00000000..b98bd406 --- /dev/null +++ b/src/RecentBooksStore.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include + +class RecentBooksStore { + // Static instance + static RecentBooksStore instance; + + std::vector recentBooks; + + public: + ~RecentBooksStore() = default; + + // Get singleton instance + static RecentBooksStore& getInstance() { return instance; } + + // Add a book path to the recent list (moves to front if already exists) + void addBook(const std::string& path); + + // Get the list of recent book paths (most recent first) + const std::vector& getBooks() const { return recentBooks; } + + // Get the count of recent books + int getCount() const { return static_cast(recentBooks.size()); } + + bool saveToFile() const; + + bool loadFromFile(); +}; + +// Helper macro to access recent books store +#define RECENT_BOOKS RecentBooksStore::getInstance() diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 11107fdc..ff0e6b8f 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -6,6 +6,7 @@ #include "CrossPointState.h" #include "MappedInputManager.h" +#include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" @@ -14,7 +15,12 @@ void HomeActivity::taskTrampoline(void* param) { self->displayTaskLoop(); } -int HomeActivity::getMenuItemCount() const { return hasContinueReading ? 4 : 3; } +int HomeActivity::getMenuItemCount() const { + int count = 3; // Base: Browse, File transfer, Settings + if (hasContinueReading) count++; + if (hasRecentBooks) count++; + return count; +} void HomeActivity::onEnter() { Activity::onEnter(); @@ -24,6 +30,9 @@ void HomeActivity::onEnter() { // Check if we have a book to continue reading hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); + // Check if we have more than one recent book + hasRecentBooks = RECENT_BOOKS.getCount() > 1; + if (hasContinueReading) { // Extract filename from path for display lastBookTitle = APP_STATE.openEpubPath; @@ -86,26 +95,29 @@ void HomeActivity::loop() { const int menuCount = getMenuItemCount(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - 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(); - } + // Menu order: [Continue Reading], [Recent Books], Browse, File transfer, Settings + // Calculate index offsets based on what's shown + int idx = selectorIndex; + + if (hasContinueReading && idx == 0) { + onContinueReading(); + return; + } + if (hasContinueReading) idx--; + + if (hasRecentBooks && idx == 0) { + onRecentBooksOpen(); + return; + } + if (hasRecentBooks) idx--; + + // Now idx is 0-based for: Browse, File transfer, Settings + if (idx == 0) { + onReaderOpen(); + } else if (idx == 1) { + onFileTransferOpen(); + } else if (idx == 2) { + onSettingsOpen(); } } else if (prevPressed) { selectorIndex = (selectorIndex + menuCount - 1) % menuCount; @@ -277,11 +289,13 @@ void HomeActivity::render() const { renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); } - // --- Bottom menu tiles (indices 1-3) --- + // --- Bottom menu tiles --- + // Menu tiles: [Recent books], Browse files, File transfer, Settings + const int menuTileCount = hasRecentBooks ? 4 : 3; const int menuTileWidth = pageWidth - 2 * margin; constexpr int menuTileHeight = 50; constexpr int menuSpacing = 10; - constexpr int totalMenuHeight = 3 * menuTileHeight + 2 * menuSpacing; + const int totalMenuHeight = menuTileCount * menuTileHeight + (menuTileCount - 1) * menuSpacing; int menuStartY = bookY + bookHeight + 20; // Ensure we don't collide with the bottom button legend @@ -290,9 +304,8 @@ void HomeActivity::render() const { menuStartY = maxMenuStartY; } - for (int i = 0; i < 3; ++i) { - constexpr const char* items[3] = {"Browse files", "File transfer", "Settings"}; - const int overallIndex = i + (getMenuItemCount() - 3); + for (int i = 0; i < menuTileCount; ++i) { + const int overallIndex = i + (getMenuItemCount() - menuTileCount); constexpr int tileX = margin; const int tileY = menuStartY + i * (menuTileHeight + menuSpacing); const bool selected = selectorIndex == overallIndex; @@ -303,7 +316,16 @@ void HomeActivity::render() const { renderer.drawRect(tileX, tileY, menuTileWidth, menuTileHeight); } - const char* label = items[i]; + // Determine label based on tile index and whether recent books is shown + const char* label; + if (hasRecentBooks) { + constexpr const char* itemsWithRecent[4] = {"Recent books", "Browse files", "File transfer", "Settings"}; + label = itemsWithRecent[i]; + } else { + constexpr const char* itemsNoRecent[3] = {"Browse files", "File transfer", "Settings"}; + label = itemsNoRecent[i]; + } + const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label); const int textX = tileX + (menuTileWidth - textWidth) / 2; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index b6c9767d..30b0d011 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -13,12 +13,14 @@ class HomeActivity final : public Activity { int selectorIndex = 0; bool updateRequired = false; bool hasContinueReading = false; + bool hasRecentBooks = false; std::string lastBookTitle; std::string lastBookAuthor; const std::function onContinueReading; const std::function onReaderOpen; const std::function onSettingsOpen; const std::function onFileTransferOpen; + const std::function onRecentBooksOpen; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -28,12 +30,14 @@ class HomeActivity final : public Activity { public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onContinueReading, const std::function& onReaderOpen, - const std::function& onSettingsOpen, const std::function& onFileTransferOpen) + const std::function& onSettingsOpen, const std::function& onFileTransferOpen, + const std::function& onRecentBooksOpen) : Activity("Home", renderer, mappedInput), onContinueReading(onContinueReading), onReaderOpen(onReaderOpen), onSettingsOpen(onSettingsOpen), - onFileTransferOpen(onFileTransferOpen) {} + onFileTransferOpen(onFileTransferOpen), + onRecentBooksOpen(onRecentBooksOpen) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp new file mode 100644 index 00000000..22e311a9 --- /dev/null +++ b/src/activities/home/RecentBooksActivity.cpp @@ -0,0 +1,182 @@ +#include "RecentBooksActivity.h" + +#include +#include +#include + +#include "MappedInputManager.h" +#include "RecentBooksStore.h" +#include "fontIds.h" + +namespace { +// Time threshold for treating a long press as a page-up/page-down +constexpr int SKIP_PAGE_MS = 700; +} // namespace + +int RecentBooksActivity::getPageItems() const { + // Layout constants used in render + constexpr int startY = 60; + constexpr int lineHeight = 30; + + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - startY; + int items = availableHeight / lineHeight; + + // Ensure we always have at least one item per page to avoid division by zero + if (items < 1) { + items = 1; + } + return items; +} + +void RecentBooksActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void RecentBooksActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + + // Load book titles from recent books list + bookTitles.clear(); + const auto& books = RECENT_BOOKS.getBooks(); + bookTitles.reserve(books.size()); + + for (const auto& path : books) { + // Skip if file no longer exists + if (!SdMan.exists(path.c_str())) { + bookTitles.emplace_back("[Missing]"); + continue; + } + + // Extract filename from path for display + std::string title = path; + const size_t lastSlash = title.find_last_of('/'); + if (lastSlash != std::string::npos) { + title = title.substr(lastSlash + 1); + } + + const std::string ext5 = title.length() >= 5 ? title.substr(title.length() - 5) : ""; + const std::string ext4 = title.length() >= 4 ? title.substr(title.length() - 4) : ""; + + // If epub, try to load the metadata for title + if (ext5 == ".epub") { + Epub epub(path, "/.crosspoint"); + epub.load(false); + if (!epub.getTitle().empty()) { + title = std::string(epub.getTitle()); + } + } else if (ext5 == ".xtch") { + title.resize(title.length() - 5); + } else if (ext4 == ".xtc") { + title.resize(title.length() - 4); + } + + bookTitles.push_back(title); + } + + selectorIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&RecentBooksActivity::taskTrampoline, "RecentBooksActivityTask", + 4096, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void RecentBooksActivity::onExit() { + Activity::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; + bookTitles.clear(); +} + +void RecentBooksActivity::loop() { + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || + mappedInput.wasReleased(MappedInputManager::Button::Right); + + const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + const int pageItems = getPageItems(); + const int bookCount = RECENT_BOOKS.getCount(); + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (bookCount > 0 && selectorIndex < bookCount) { + const auto& books = RECENT_BOOKS.getBooks(); + onSelectBook(books[selectorIndex]); + } + } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onGoBack(); + } else if (prevReleased && bookCount > 0) { + if (skipPage) { + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + bookCount) % bookCount; + } else { + selectorIndex = (selectorIndex + bookCount - 1) % bookCount; + } + updateRequired = true; + } else if (nextReleased && bookCount > 0) { + if (skipPage) { + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % bookCount; + } else { + selectorIndex = (selectorIndex + 1) % bookCount; + } + updateRequired = true; + } +} + +void RecentBooksActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void RecentBooksActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + const int pageItems = getPageItems(); + const int bookCount = RECENT_BOOKS.getCount(); + + // Draw header + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Recent Books", true, EpdFontFamily::BOLD); + + // Help text + const auto labels = mappedInput.mapLabels("« Back", "Open", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + if (bookCount == 0) { + renderer.drawText(UI_10_FONT_ID, 20, 60, "No recent books"); + renderer.displayBuffer(); + return; + } + + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); + + for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - 40); + renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, item.c_str(), i != selectorIndex); + } + + renderer.displayBuffer(); +} diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h new file mode 100644 index 00000000..1bb693cf --- /dev/null +++ b/src/activities/home/RecentBooksActivity.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "../Activity.h" + +class RecentBooksActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + int selectorIndex = 0; + bool updateRequired = false; + std::vector bookTitles; // Display titles for each book + const std::function onGoBack; + const std::function onSelectBook; + + // Number of items that fit on a page + int getPageItems() const; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + + public: + explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onGoBack, + const std::function& onSelectBook) + : Activity("RecentBooks", renderer, mappedInput), onGoBack(onGoBack), onSelectBook(onSelectBook) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index d3cd5016..2b0da344 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -9,6 +9,7 @@ #include "CrossPointState.h" #include "EpubReaderChapterSelectionActivity.h" #include "MappedInputManager.h" +#include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" @@ -76,9 +77,10 @@ void EpubReaderActivity::onEnter() { } } - // Save current epub as last opened epub + // Save current epub as last opened epub and add to recent books APP_STATE.openEpubPath = epub->getPath(); APP_STATE.saveToFile(); + RECENT_BOOKS.addBook(epub->getPath()); // Trigger first update updateRequired = true; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index c0580cf6..28823e9a 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -14,6 +14,7 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" +#include "RecentBooksStore.h" #include "XtcReaderChapterSelectionActivity.h" #include "fontIds.h" @@ -41,9 +42,10 @@ void XtcReaderActivity::onEnter() { // Load saved progress loadProgress(); - // Save current XTC as last opened book + // Save current XTC as last opened book and add to recent books APP_STATE.openEpubPath = xtc->getPath(); APP_STATE.saveToFile(); + RECENT_BOOKS.addBook(xtc->getPath()); // Trigger first update updateRequired = true; diff --git a/src/main.cpp b/src/main.cpp index e81448bd..a56e15b6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,9 +11,11 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" +#include "RecentBooksStore.h" #include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/SleepActivity.h" #include "activities/home/HomeActivity.h" +#include "activities/home/RecentBooksActivity.h" #include "activities/network/CrossPointWebServerActivity.h" #include "activities/reader/ReaderActivity.h" #include "activities/settings/SettingsActivity.h" @@ -222,10 +224,16 @@ void onGoToSettings() { enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } +void onGoHome(); +void onGoToRecentBooks() { + exitActivity(); + enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); +} + void onGoHome() { exitActivity(); enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, - onGoToFileTransfer)); + onGoToFileTransfer, onGoToRecentBooks)); } void setupDisplayAndFonts() { @@ -289,6 +297,8 @@ void setup() { enterNewActivity(new BootActivity(renderer, mappedInputManager)); APP_STATE.loadFromFile(); + RECENT_BOOKS.loadFromFile(); + if (APP_STATE.openEpubPath.empty()) { onGoHome(); } else { From 067a36a7b4e49ddc124bf1592ae7d44907aaf681 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Sun, 4 Jan 2026 19:28:22 -0500 Subject: [PATCH 02/11] syntax --- src/activities/home/HomeActivity.cpp | 56 ++++++++++++++++------------ 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index ff0e6b8f..39ea0ed5 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -95,29 +95,39 @@ void HomeActivity::loop() { const int menuCount = getMenuItemCount(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - // Menu order: [Continue Reading], [Recent Books], Browse, File transfer, Settings - // Calculate index offsets based on what's shown - int idx = selectorIndex; - - if (hasContinueReading && idx == 0) { - onContinueReading(); - return; - } - if (hasContinueReading) idx--; - - if (hasRecentBooks && idx == 0) { - onRecentBooksOpen(); - return; - } - if (hasRecentBooks) idx--; - - // Now idx is 0-based for: Browse, File transfer, Settings - if (idx == 0) { - onReaderOpen(); - } else if (idx == 1) { - onFileTransferOpen(); - } else if (idx == 2) { - onSettingsOpen(); + if (hasContinueReading && hasRecentBooks) { + // Menu: Continue Reading, Recent Books, Browse, File transfer, Settings + if (selectorIndex == 0) { + onContinueReading(); + } else if (selectorIndex == 1) { + onRecentBooksOpen(); + } else if (selectorIndex == 2) { + onReaderOpen(); + } else if (selectorIndex == 3) { + onFileTransferOpen(); + } else if (selectorIndex == 4) { + onSettingsOpen(); + } + } else 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 + menuCount - 1) % menuCount; From 6401f099f4f9b4d65185272b29049ad353341265 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Mon, 5 Jan 2026 08:03:02 -0500 Subject: [PATCH 03/11] Enhance RecentBooksActivity to manage book paths - Added a new vector to store book paths alongside titles, ensuring that missing files are handled correctly. - Updated methods to utilize the new bookPaths vector for selecting and rendering recent books. - Cleared bookPaths during activity exit to prevent memory leaks. --- src/activities/home/RecentBooksActivity.cpp | 12 +++++++----- src/activities/home/RecentBooksActivity.h | 1 + src/main.cpp | 1 - 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 22e311a9..b5d081ec 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -41,13 +41,14 @@ void RecentBooksActivity::onEnter() { // Load book titles from recent books list bookTitles.clear(); + bookPaths.clear(); const auto& books = RECENT_BOOKS.getBooks(); bookTitles.reserve(books.size()); + bookPaths.reserve(books.size()); for (const auto& path : books) { // Skip if file no longer exists if (!SdMan.exists(path.c_str())) { - bookTitles.emplace_back("[Missing]"); continue; } @@ -75,6 +76,7 @@ void RecentBooksActivity::onEnter() { } bookTitles.push_back(title); + bookPaths.push_back(path); } selectorIndex = 0; @@ -102,6 +104,7 @@ void RecentBooksActivity::onExit() { vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; bookTitles.clear(); + bookPaths.clear(); } void RecentBooksActivity::loop() { @@ -112,12 +115,11 @@ void RecentBooksActivity::loop() { const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = getPageItems(); - const int bookCount = RECENT_BOOKS.getCount(); + const int bookCount = static_cast(bookTitles.size()); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (bookCount > 0 && selectorIndex < bookCount) { - const auto& books = RECENT_BOOKS.getBooks(); - onSelectBook(books[selectorIndex]); + onSelectBook(bookPaths[selectorIndex]); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoBack(); @@ -155,7 +157,7 @@ void RecentBooksActivity::render() const { const auto pageWidth = renderer.getScreenWidth(); const int pageItems = getPageItems(); - const int bookCount = RECENT_BOOKS.getCount(); + const int bookCount = static_cast(bookTitles.size()); // Draw header renderer.drawCenteredText(UI_12_FONT_ID, 15, "Recent Books", true, EpdFontFamily::BOLD); diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index 1bb693cf..3e386787 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -15,6 +15,7 @@ class RecentBooksActivity final : public Activity { int selectorIndex = 0; bool updateRequired = false; std::vector bookTitles; // Display titles for each book + std::vector bookPaths; // Paths for each visible book (excludes missing) const std::function onGoBack; const std::function onSelectBook; diff --git a/src/main.cpp b/src/main.cpp index a56e15b6..21dc4d20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -224,7 +224,6 @@ void onGoToSettings() { enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } -void onGoHome(); void onGoToRecentBooks() { exitActivity(); enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); From e2d2c4a9b042c5b8e38f154a100ee30bfdb5b78b Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 12:44:54 -0500 Subject: [PATCH 04/11] Introduce Tab bar --- src/ScreenComponents.cpp | 69 ++++ src/ScreenComponents.h | 16 + src/activities/home/HomeActivity.cpp | 47 +-- src/activities/home/HomeActivity.h | 14 +- src/activities/home/MyLibraryActivity.cpp | 371 ++++++++++++++++++++++ src/activities/home/MyLibraryActivity.h | 65 ++++ src/main.cpp | 11 +- 7 files changed, 542 insertions(+), 51 deletions(-) create mode 100644 src/activities/home/MyLibraryActivity.cpp create mode 100644 src/activities/home/MyLibraryActivity.h diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index 2900f3e4..cb0fc50e 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -39,3 +39,72 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); } + +int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { + constexpr int tabPadding = 20; // Horizontal padding between tabs + constexpr int leftMargin = 20; // Left margin for first tab + constexpr int underlineHeight = 2; // Height of selection underline + constexpr int underlineGap = 4; // Gap between text and underline + + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int tabBarHeight = lineHeight + underlineGap + underlineHeight; + + int currentX = leftMargin; + + for (const auto& tab : tabs) { + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, + tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + + // Draw tab label + renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true, + tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + + // Draw underline for selected tab + if (tab.selected) { + renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight); + } + + currentX += textWidth + tabPadding; + } + + return tabBarHeight; +} + +void ScreenComponents::drawScrollIndicator(const GfxRenderer& renderer, const int currentPage, const int totalPages, + const int contentTop, const int contentHeight) { + if (totalPages <= 1) { + return; // No need for indicator if only one page + } + + const int screenWidth = renderer.getScreenWidth(); + constexpr int indicatorWidth = 20; + constexpr int arrowSize = 6; + constexpr int margin = 5; + + const int centerX = screenWidth - indicatorWidth / 2 - margin; + const int indicatorTop = contentTop + 10; + const int indicatorBottom = contentTop + contentHeight - 30; + + // Draw up arrow (triangle pointing up) + for (int i = 0; i < arrowSize; ++i) { + const int lineWidth = 1 + i * 2; + const int startX = centerX - i; + renderer.drawLine(startX, indicatorTop + arrowSize - 1 - i, startX + lineWidth - 1, indicatorTop + arrowSize - 1 - i); + } + + // Draw down arrow (triangle pointing down) + for (int i = 0; i < arrowSize; ++i) { + const int lineWidth = 1 + i * 2; + const int startX = centerX - i; + renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1, + indicatorBottom - arrowSize + 1 + i); + } + + // Draw page fraction in the middle (e.g., "1/3") + const std::string pageText = std::to_string(currentPage) + "/" + std::to_string(totalPages); + const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, pageText.c_str()); + const int textX = centerX - textWidth / 2; + const int textY = (indicatorTop + indicatorBottom) / 2 - renderer.getLineHeight(SMALL_FONT_ID) / 2; + + renderer.drawText(SMALL_FONT_ID, textX, textY, pageText.c_str()); +} diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 2598a3e3..b2b7bc70 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -1,8 +1,24 @@ #pragma once +#include + class GfxRenderer; +struct TabInfo { + const char* label; + bool selected; +}; + class ScreenComponents { public: static void drawBattery(const GfxRenderer& renderer, int left, int top); + + // Draw a horizontal tab bar with underline indicator for selected tab + // Returns the height of the tab bar (for positioning content below) + static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector& tabs); + + // Draw a scroll/page indicator on the right side of the screen + // Shows up/down arrows and current page fraction (e.g., "1/3") + static void drawScrollIndicator(const GfxRenderer& renderer, int currentPage, int totalPages, int contentTop, + int contentHeight); }; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 39ea0ed5..036ac1f0 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -6,7 +6,6 @@ #include "CrossPointState.h" #include "MappedInputManager.h" -#include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" @@ -16,9 +15,8 @@ void HomeActivity::taskTrampoline(void* param) { } int HomeActivity::getMenuItemCount() const { - int count = 3; // Base: Browse, File transfer, Settings + int count = 3; // Base: My Library, File transfer, Settings if (hasContinueReading) count++; - if (hasRecentBooks) count++; return count; } @@ -30,9 +28,6 @@ void HomeActivity::onEnter() { // Check if we have a book to continue reading hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); - // Check if we have more than one recent book - hasRecentBooks = RECENT_BOOKS.getCount() > 1; - if (hasContinueReading) { // Extract filename from path for display lastBookTitle = APP_STATE.openEpubPath; @@ -95,34 +90,21 @@ void HomeActivity::loop() { const int menuCount = getMenuItemCount(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (hasContinueReading && hasRecentBooks) { - // Menu: Continue Reading, Recent Books, Browse, File transfer, Settings + if (hasContinueReading) { + // Menu: Continue Reading, My Library, File transfer, Settings if (selectorIndex == 0) { onContinueReading(); } else if (selectorIndex == 1) { - onRecentBooksOpen(); - } else if (selectorIndex == 2) { - onReaderOpen(); - } else if (selectorIndex == 3) { - onFileTransferOpen(); - } else if (selectorIndex == 4) { - onSettingsOpen(); - } - } else if (hasContinueReading) { - // Menu: Continue Reading, Browse, File transfer, Settings - if (selectorIndex == 0) { - onContinueReading(); - } else if (selectorIndex == 1) { - onReaderOpen(); + onMyLibraryOpen(); } else if (selectorIndex == 2) { onFileTransferOpen(); } else if (selectorIndex == 3) { onSettingsOpen(); } } else { - // Menu: Browse, File transfer, Settings + // Menu: My Library, File transfer, Settings if (selectorIndex == 0) { - onReaderOpen(); + onMyLibraryOpen(); } else if (selectorIndex == 1) { onFileTransferOpen(); } else if (selectorIndex == 2) { @@ -300,8 +282,8 @@ void HomeActivity::render() const { } // --- Bottom menu tiles --- - // Menu tiles: [Recent books], Browse files, File transfer, Settings - const int menuTileCount = hasRecentBooks ? 4 : 3; + // Menu tiles: My Library, File transfer, Settings + constexpr int menuTileCount = 3; const int menuTileWidth = pageWidth - 2 * margin; constexpr int menuTileHeight = 50; constexpr int menuSpacing = 10; @@ -314,6 +296,8 @@ void HomeActivity::render() const { menuStartY = maxMenuStartY; } + constexpr const char* menuItems[3] = {"My Library", "File transfer", "Settings"}; + for (int i = 0; i < menuTileCount; ++i) { const int overallIndex = i + (getMenuItemCount() - menuTileCount); constexpr int tileX = margin; @@ -326,16 +310,7 @@ void HomeActivity::render() const { renderer.drawRect(tileX, tileY, menuTileWidth, menuTileHeight); } - // Determine label based on tile index and whether recent books is shown - const char* label; - if (hasRecentBooks) { - constexpr const char* itemsWithRecent[4] = {"Recent books", "Browse files", "File transfer", "Settings"}; - label = itemsWithRecent[i]; - } else { - constexpr const char* itemsNoRecent[3] = {"Browse files", "File transfer", "Settings"}; - label = itemsNoRecent[i]; - } - + const char* label = menuItems[i]; const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label); const int textX = tileX + (menuTileWidth - textWidth) / 2; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 30b0d011..6cc02a93 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -13,14 +13,12 @@ class HomeActivity final : public Activity { int selectorIndex = 0; bool updateRequired = false; bool hasContinueReading = false; - bool hasRecentBooks = false; std::string lastBookTitle; std::string lastBookAuthor; const std::function onContinueReading; - const std::function onReaderOpen; + const std::function onMyLibraryOpen; const std::function onSettingsOpen; const std::function onFileTransferOpen; - const std::function onRecentBooksOpen; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); @@ -29,15 +27,13 @@ class HomeActivity final : public Activity { public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onContinueReading, const std::function& onReaderOpen, - const std::function& onSettingsOpen, const std::function& onFileTransferOpen, - const std::function& onRecentBooksOpen) + const std::function& onContinueReading, const std::function& onMyLibraryOpen, + const std::function& onSettingsOpen, const std::function& onFileTransferOpen) : Activity("Home", renderer, mappedInput), onContinueReading(onContinueReading), - onReaderOpen(onReaderOpen), + onMyLibraryOpen(onMyLibraryOpen), onSettingsOpen(onSettingsOpen), - onFileTransferOpen(onFileTransferOpen), - onRecentBooksOpen(onRecentBooksOpen) {} + onFileTransferOpen(onFileTransferOpen) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp new file mode 100644 index 00000000..7103c8bb --- /dev/null +++ b/src/activities/home/MyLibraryActivity.cpp @@ -0,0 +1,371 @@ +#include "MyLibraryActivity.h" + +#include +#include +#include + +#include + +#include "MappedInputManager.h" +#include "RecentBooksStore.h" +#include "ScreenComponents.h" +#include "fontIds.h" + +namespace { +// Layout constants +constexpr int TAB_BAR_Y = 15; +constexpr int CONTENT_START_Y = 60; +constexpr int LINE_HEIGHT = 30; +constexpr int LEFT_MARGIN = 20; +constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator + +// Timing thresholds +constexpr int SKIP_PAGE_MS = 700; +constexpr unsigned long GO_HOME_MS = 1000; + +void sortFileList(std::vector& strs) { + std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { + if (str1.back() == '/' && str2.back() != '/') return true; + if (str1.back() != '/' && str2.back() == '/') return false; + return lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); + }); +} +} // namespace + +int MyLibraryActivity::getPageItems() const { + const int screenHeight = renderer.getScreenHeight(); + const int bottomBarHeight = 60; // Space for button hints + const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight; + int items = availableHeight / LINE_HEIGHT; + if (items < 1) { + items = 1; + } + return items; +} + +int MyLibraryActivity::getCurrentItemCount() const { + if (currentTab == Tab::Recent) { + return static_cast(bookTitles.size()); + } + return static_cast(files.size()); +} + +int MyLibraryActivity::getTotalPages() const { + const int itemCount = getCurrentItemCount(); + const int pageItems = getPageItems(); + if (itemCount == 0) return 1; + return (itemCount + pageItems - 1) / pageItems; +} + +int MyLibraryActivity::getCurrentPage() const { + const int pageItems = getPageItems(); + return selectorIndex / pageItems + 1; +} + +void MyLibraryActivity::loadRecentBooks() { + bookTitles.clear(); + bookPaths.clear(); + const auto& books = RECENT_BOOKS.getBooks(); + bookTitles.reserve(books.size()); + bookPaths.reserve(books.size()); + + for (const auto& path : books) { + // Skip if file no longer exists + if (!SdMan.exists(path.c_str())) { + continue; + } + + // Extract filename from path for display + std::string title = path; + const size_t lastSlash = title.find_last_of('/'); + if (lastSlash != std::string::npos) { + title = title.substr(lastSlash + 1); + } + + const std::string ext5 = title.length() >= 5 ? title.substr(title.length() - 5) : ""; + const std::string ext4 = title.length() >= 4 ? title.substr(title.length() - 4) : ""; + + // If epub, try to load the metadata for title + if (ext5 == ".epub") { + Epub epub(path, "/.crosspoint"); + epub.load(false); + if (!epub.getTitle().empty()) { + title = std::string(epub.getTitle()); + } + } else if (ext5 == ".xtch") { + title.resize(title.length() - 5); + } else if (ext4 == ".xtc") { + title.resize(title.length() - 4); + } + + bookTitles.push_back(title); + bookPaths.push_back(path); + } +} + +void MyLibraryActivity::loadFiles() { + files.clear(); + + auto root = SdMan.open(basepath.c_str()); + if (!root || !root.isDirectory()) { + if (root) root.close(); + return; + } + + root.rewindDirectory(); + + char name[128]; + for (auto file = root.openNextFile(); file; file = root.openNextFile()) { + file.getName(name, sizeof(name)); + if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) { + file.close(); + continue; + } + + if (file.isDirectory()) { + files.emplace_back(std::string(name) + "/"); + } else { + auto filename = std::string(name); + std::string ext4 = filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; + std::string ext5 = filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; + if (ext5 == ".epub" || ext5 == ".xtch" || ext4 == ".xtc") { + files.emplace_back(filename); + } + } + file.close(); + } + root.close(); + sortFileList(files); +} + +void MyLibraryActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void MyLibraryActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + + // Load data for both tabs + loadRecentBooks(); + loadFiles(); + + selectorIndex = 0; + updateRequired = true; + + xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", + 4096, // Stack size (increased for epub metadata loading) + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void MyLibraryActivity::onExit() { + Activity::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; + + bookTitles.clear(); + bookPaths.clear(); + files.clear(); +} + +void MyLibraryActivity::loop() { + const int itemCount = getCurrentItemCount(); + const int pageItems = getPageItems(); + + // Long press BACK (1s+) in Files tab goes to root folder + if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) && + mappedInput.getHeldTime() >= GO_HOME_MS) { + if (basepath != "/") { + basepath = "/"; + loadFiles(); + selectorIndex = 0; + updateRequired = true; + } + return; + } + + const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); + const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); + const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); + + const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + + // Confirm button - open selected item + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (currentTab == Tab::Recent) { + if (!bookPaths.empty() && selectorIndex < static_cast(bookPaths.size())) { + onSelectBook(bookPaths[selectorIndex]); + } + } else { + // Files tab + if (!files.empty() && selectorIndex < static_cast(files.size())) { + if (basepath.back() != '/') basepath += "/"; + if (files[selectorIndex].back() == '/') { + // Enter directory + basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + loadFiles(); + selectorIndex = 0; + updateRequired = true; + } else { + // Open file + onSelectBook(basepath + files[selectorIndex]); + } + } + } + return; + } + + // Back button + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + if (mappedInput.getHeldTime() < GO_HOME_MS) { + if (currentTab == Tab::Files && basepath != "/") { + // Go up one directory + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); + if (basepath.empty()) basepath = "/"; + loadFiles(); + selectorIndex = 0; + updateRequired = true; + } else { + // Go home + onGoHome(); + } + } + return; + } + + // Tab switching: Left/Right when selectorIndex == 0 + if (selectorIndex == 0) { + if (leftReleased && currentTab == Tab::Files) { + currentTab = Tab::Recent; + selectorIndex = 0; + updateRequired = true; + return; + } + if (rightReleased && currentTab == Tab::Recent) { + currentTab = Tab::Files; + selectorIndex = 0; + updateRequired = true; + return; + } + } + + // Navigation: Up/Down moves through items, Left/Right also work as prev/next + const bool prevReleased = upReleased || leftReleased; + const bool nextReleased = downReleased || rightReleased; + + if (prevReleased && itemCount > 0) { + if (skipPage) { + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; + } else { + selectorIndex = (selectorIndex + itemCount - 1) % itemCount; + } + updateRequired = true; + } else if (nextReleased && itemCount > 0) { + if (skipPage) { + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount; + } else { + selectorIndex = (selectorIndex + 1) % itemCount; + } + updateRequired = true; + } +} + +void MyLibraryActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void MyLibraryActivity::render() const { + renderer.clearScreen(); + + // Draw tab bar + std::vector tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; + ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); + + // Draw content based on current tab + if (currentTab == Tab::Recent) { + renderRecentTab(); + } else { + renderFilesTab(); + } + + // Draw scroll indicator + const int screenHeight = renderer.getScreenHeight(); + const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar + ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight); + + // Draw bottom button hints + const auto labels = mappedInput.mapLabels("HOME", "OPEN", "<", ">"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} + +void MyLibraryActivity::renderRecentTab() const { + const auto pageWidth = renderer.getScreenWidth(); + const int pageItems = getPageItems(); + const int bookCount = static_cast(bookTitles.size()); + + if (bookCount == 0) { + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); + return; + } + + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + + // Draw selection highlight + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, + LINE_HEIGHT); + + // Draw items + for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), + i != selectorIndex); + } +} + +void MyLibraryActivity::renderFilesTab() const { + const auto pageWidth = renderer.getScreenWidth(); + const int pageItems = getPageItems(); + const int fileCount = static_cast(files.size()); + + if (fileCount == 0) { + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); + return; + } + + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + + // Draw selection highlight + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, + LINE_HEIGHT); + + // Draw items + for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), + i != selectorIndex); + } +} diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h new file mode 100644 index 00000000..dc87aae6 --- /dev/null +++ b/src/activities/home/MyLibraryActivity.h @@ -0,0 +1,65 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "../Activity.h" + +class MyLibraryActivity final : public Activity { + public: + enum class Tab { Recent, Files }; + + private: + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + + Tab currentTab = Tab::Recent; + int selectorIndex = 0; + bool updateRequired = false; + + // Recent tab state (from RecentBooksActivity) + std::vector bookTitles; // Display titles for each book + std::vector bookPaths; // Paths for each visible book (excludes missing) + + // Files tab state (from FileSelectionActivity) + std::string basepath = "/"; + std::vector files; + + // Callbacks + const std::function onGoHome; + const std::function onSelectBook; + + // Number of items that fit on a page + int getPageItems() const; + int getCurrentItemCount() const; + int getTotalPages() const; + int getCurrentPage() const; + + // Data loading + void loadRecentBooks(); + void loadFiles(); + + // Rendering + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void renderRecentTab() const; + void renderFilesTab() const; + + public: + explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onGoHome, + const std::function& onSelectBook, + Tab initialTab = Tab::Recent) + : Activity("MyLibrary", renderer, mappedInput), + currentTab(initialTab), + onGoHome(onGoHome), + onSelectBook(onSelectBook) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/main.cpp b/src/main.cpp index 21dc4d20..9b41d8f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,7 +15,7 @@ #include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/SleepActivity.h" #include "activities/home/HomeActivity.h" -#include "activities/home/RecentBooksActivity.h" +#include "activities/home/MyLibraryActivity.h" #include "activities/network/CrossPointWebServerActivity.h" #include "activities/reader/ReaderActivity.h" #include "activities/settings/SettingsActivity.h" @@ -211,7 +211,6 @@ void onGoToReader(const std::string& initialEpubPath) { exitActivity(); enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome)); } -void onGoToReaderHome() { onGoToReader(std::string()); } void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onGoToFileTransfer() { @@ -224,15 +223,15 @@ void onGoToSettings() { enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } -void onGoToRecentBooks() { +void onGoToMyLibrary() { exitActivity(); - enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); } void onGoHome() { exitActivity(); - enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, - onGoToFileTransfer, onGoToRecentBooks)); + enterNewActivity( + new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, onGoToFileTransfer)); } void setupDisplayAndFonts() { From 7a4d2b377dfe87648438bf13bdbb029f69fef7b2 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 13:02:29 -0500 Subject: [PATCH 05/11] Improvements to button labels and navigation --- src/activities/home/MyLibraryActivity.cpp | 153 ++++++++++++++-------- 1 file changed, 99 insertions(+), 54 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 7103c8bb..0b684bcf 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -17,25 +17,31 @@ constexpr int TAB_BAR_Y = 15; constexpr int CONTENT_START_Y = 60; constexpr int LINE_HEIGHT = 30; constexpr int LEFT_MARGIN = 20; -constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator +constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator // Timing thresholds constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; -void sortFileList(std::vector& strs) { - std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { - if (str1.back() == '/' && str2.back() != '/') return true; - if (str1.back() != '/' && str2.back() == '/') return false; - return lexicographical_compare(begin(str1), end(str1), begin(str2), end(str2), - [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); - }); +void sortFileList(std::vector &strs) { + std::sort(begin(strs), end(strs), + [](const std::string &str1, const std::string &str2) { + if (str1.back() == '/' && str2.back() != '/') + return true; + if (str1.back() != '/' && str2.back() == '/') + return false; + return lexicographical_compare( + begin(str1), end(str1), begin(str2), end(str2), + [](const char &char1, const char &char2) { + return tolower(char1) < tolower(char2); + }); + }); } -} // namespace +} // namespace int MyLibraryActivity::getPageItems() const { const int screenHeight = renderer.getScreenHeight(); - const int bottomBarHeight = 60; // Space for button hints + const int bottomBarHeight = 60; // Space for button hints const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight; int items = availableHeight / LINE_HEIGHT; if (items < 1) { @@ -54,7 +60,8 @@ int MyLibraryActivity::getCurrentItemCount() const { int MyLibraryActivity::getTotalPages() const { const int itemCount = getCurrentItemCount(); const int pageItems = getPageItems(); - if (itemCount == 0) return 1; + if (itemCount == 0) + return 1; return (itemCount + pageItems - 1) / pageItems; } @@ -66,11 +73,11 @@ int MyLibraryActivity::getCurrentPage() const { void MyLibraryActivity::loadRecentBooks() { bookTitles.clear(); bookPaths.clear(); - const auto& books = RECENT_BOOKS.getBooks(); + const auto &books = RECENT_BOOKS.getBooks(); bookTitles.reserve(books.size()); bookPaths.reserve(books.size()); - for (const auto& path : books) { + for (const auto &path : books) { // Skip if file no longer exists if (!SdMan.exists(path.c_str())) { continue; @@ -83,8 +90,10 @@ void MyLibraryActivity::loadRecentBooks() { title = title.substr(lastSlash + 1); } - const std::string ext5 = title.length() >= 5 ? title.substr(title.length() - 5) : ""; - const std::string ext4 = title.length() >= 4 ? title.substr(title.length() - 4) : ""; + const std::string ext5 = + title.length() >= 5 ? title.substr(title.length() - 5) : ""; + const std::string ext4 = + title.length() >= 4 ? title.substr(title.length() - 4) : ""; // If epub, try to load the metadata for title if (ext5 == ".epub") { @@ -109,7 +118,8 @@ void MyLibraryActivity::loadFiles() { auto root = SdMan.open(basepath.c_str()); if (!root || !root.isDirectory()) { - if (root) root.close(); + if (root) + root.close(); return; } @@ -127,8 +137,10 @@ void MyLibraryActivity::loadFiles() { files.emplace_back(std::string(name) + "/"); } else { auto filename = std::string(name); - std::string ext4 = filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; - std::string ext5 = filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; + std::string ext4 = + filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; + std::string ext5 = + filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; if (ext5 == ".epub" || ext5 == ".xtch" || ext4 == ".xtc") { files.emplace_back(filename); } @@ -139,8 +151,8 @@ void MyLibraryActivity::loadFiles() { sortFileList(files); } -void MyLibraryActivity::taskTrampoline(void* param) { - auto* self = static_cast(param); +void MyLibraryActivity::taskTrampoline(void *param) { + auto *self = static_cast(param); self->displayTaskLoop(); } @@ -157,17 +169,18 @@ void MyLibraryActivity::onEnter() { updateRequired = true; xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", - 4096, // Stack size (increased for epub metadata loading) - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle + 4096, // Stack size (increased for epub metadata loading) + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle ); } void MyLibraryActivity::onExit() { Activity::onExit(); - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + // Wait until not rendering to delete task to avoid killing mid-instruction to + // EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); @@ -186,7 +199,8 @@ void MyLibraryActivity::loop() { const int pageItems = getPageItems(); // Long press BACK (1s+) in Files tab goes to root folder - if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) && + if (currentTab == Tab::Files && + mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (basepath != "/") { basepath = "/"; @@ -197,26 +211,33 @@ void MyLibraryActivity::loop() { return; } - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); - const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); + const bool upReleased = + mappedInput.wasReleased(MappedInputManager::Button::Up); + const bool downReleased = + mappedInput.wasReleased(MappedInputManager::Button::Down); + const bool leftReleased = + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool rightReleased = + mappedInput.wasReleased(MappedInputManager::Button::Right); const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; // Confirm button - open selected item if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (currentTab == Tab::Recent) { - if (!bookPaths.empty() && selectorIndex < static_cast(bookPaths.size())) { + if (!bookPaths.empty() && + selectorIndex < static_cast(bookPaths.size())) { onSelectBook(bookPaths[selectorIndex]); } } else { // Files tab if (!files.empty() && selectorIndex < static_cast(files.size())) { - if (basepath.back() != '/') basepath += "/"; + if (basepath.back() != '/') + basepath += "/"; if (files[selectorIndex].back() == '/') { // Enter directory - basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + basepath += + files[selectorIndex].substr(0, files[selectorIndex].length() - 1); loadFiles(); selectorIndex = 0; updateRequired = true; @@ -235,7 +256,8 @@ void MyLibraryActivity::loop() { if (currentTab == Tab::Files && basepath != "/") { // Go up one directory basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) basepath = "/"; + if (basepath.empty()) + basepath = "/"; loadFiles(); selectorIndex = 0; updateRequired = true; @@ -269,7 +291,8 @@ void MyLibraryActivity::loop() { if (prevReleased && itemCount > 0) { if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; + selectorIndex = + ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; } else { selectorIndex = (selectorIndex + itemCount - 1) % itemCount; } @@ -300,7 +323,8 @@ void MyLibraryActivity::render() const { renderer.clearScreen(); // Draw tab bar - std::vector tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; + std::vector tabs = {{"Recent", currentTab == Tab::Recent}, + {"Files", currentTab == Tab::Files}}; ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); // Draw content based on current tab @@ -312,12 +336,23 @@ void MyLibraryActivity::render() const { // Draw scroll indicator const int screenHeight = renderer.getScreenHeight(); - const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar - ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight); + const int contentHeight = + screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar + ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), + getTotalPages(), CONTENT_START_Y, + contentHeight); + + // Draw side button hints (up/down navigation on right side) + // Note: text is rotated 90° CW, so ">" appears as "^" and "<" appears as "v" + renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<"); // Draw bottom button hints - const auto labels = mappedInput.mapLabels("HOME", "OPEN", "<", ">"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // In Files tab, show "BACK" when in subdirectory, "HOME" when at root + const char *backLabel = + (currentTab == Tab::Files && basepath != "/") ? "BACK" : "HOME"; + const auto labels = mappedInput.mapLabels(backLabel, "OPEN", "<", ">"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, + labels.btn4); renderer.displayBuffer(); } @@ -328,21 +363,26 @@ void MyLibraryActivity::renderRecentTab() const { const int bookCount = static_cast(bookTitles.size()); if (bookCount == 0) { - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, + "No recent books"); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, - LINE_HEIGHT); + renderer.fillRect( + 0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, + pageWidth - RIGHT_MARGIN, LINE_HEIGHT); // Draw items - for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), - i != selectorIndex); + for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; + i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), + pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, + CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, + item.c_str(), i != selectorIndex); } } @@ -352,20 +392,25 @@ void MyLibraryActivity::renderFilesTab() const { const int fileCount = static_cast(files.size()); if (fileCount == 0) { - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, + "No books found"); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, - LINE_HEIGHT); + renderer.fillRect( + 0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, + pageWidth - RIGHT_MARGIN, LINE_HEIGHT); // Draw items - for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), - i != selectorIndex); + for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; + i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), + pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, + CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, + item.c_str(), i != selectorIndex); } } From c2d0dce438ceb0a692c74e162bf00c26d9787333 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 13:19:22 -0500 Subject: [PATCH 06/11] Update ScreenComponents.cpp --- src/ScreenComponents.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index 9b931bdb..e6bd9825 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -92,21 +92,23 @@ void ScreenComponents::drawScrollIndicator(const GfxRenderer &renderer, const int screenWidth = renderer.getScreenWidth(); constexpr int indicatorWidth = 20; constexpr int arrowSize = 6; - constexpr int margin = 5; + constexpr int margin = 15; // Offset from right edge const int centerX = screenWidth - indicatorWidth / 2 - margin; - const int indicatorTop = contentTop + 10; + const int indicatorTop = + contentTop + 60; // Offset to avoid overlapping side button hints const int indicatorBottom = contentTop + contentHeight - 30; - // Draw up arrow (triangle pointing up) + // Draw up arrow at top (triangle pointing up - wide at bottom, narrow at top) for (int i = 0; i < arrowSize; ++i) { - const int lineWidth = 1 + i * 2; - const int startX = centerX - i; - renderer.drawLine(startX, indicatorTop + arrowSize - 1 - i, - startX + lineWidth - 1, indicatorTop + arrowSize - 1 - i); + const int lineWidth = 1 + (arrowSize - 1 - i) * 2; + const int startX = centerX - (arrowSize - 1 - i); + renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, + indicatorTop + i); } - // Draw down arrow (triangle pointing down) + // Draw down arrow at bottom (triangle pointing down - narrow at top, wide at + // bottom) for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + i * 2; const int startX = centerX - i; From 5d81d7cac3f59809f6e90570aac9798bf42312b1 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 13:33:45 -0500 Subject: [PATCH 07/11] Improvements to indicators and button interaction --- src/ScreenComponents.cpp | 13 +++++---- src/activities/home/MyLibraryActivity.cpp | 32 +++++++++++------------ src/activities/home/MyLibraryActivity.h | 30 ++++++++++----------- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index e6bd9825..f414ca06 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -99,19 +99,18 @@ void ScreenComponents::drawScrollIndicator(const GfxRenderer &renderer, contentTop + 60; // Offset to avoid overlapping side button hints const int indicatorBottom = contentTop + contentHeight - 30; - // Draw up arrow at top (triangle pointing up - wide at bottom, narrow at top) + // Draw up arrow at top (^) - narrow point at top, wide base at bottom for (int i = 0; i < arrowSize; ++i) { - const int lineWidth = 1 + (arrowSize - 1 - i) * 2; - const int startX = centerX - (arrowSize - 1 - i); + const int lineWidth = 1 + i * 2; + const int startX = centerX - i; renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i); } - // Draw down arrow at bottom (triangle pointing down - narrow at top, wide at - // bottom) + // Draw down arrow at bottom (v) - wide base at top, narrow point at bottom for (int i = 0; i < arrowSize; ++i) { - const int lineWidth = 1 + i * 2; - const int startX = centerX - i; + const int lineWidth = 1 + (arrowSize - 1 - i) * 2; + const int startX = centerX - (arrowSize - 1 - i); renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1, indicatorBottom - arrowSize + 1 + i); diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 0b684bcf..23bc9a85 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -269,25 +269,23 @@ void MyLibraryActivity::loop() { return; } - // Tab switching: Left/Right when selectorIndex == 0 - if (selectorIndex == 0) { - if (leftReleased && currentTab == Tab::Files) { - currentTab = Tab::Recent; - selectorIndex = 0; - updateRequired = true; - return; - } - if (rightReleased && currentTab == Tab::Recent) { - currentTab = Tab::Files; - selectorIndex = 0; - updateRequired = true; - return; - } + // Tab switching: Left/Right always control tabs + if (leftReleased && currentTab == Tab::Files) { + currentTab = Tab::Recent; + selectorIndex = 0; + updateRequired = true; + return; + } + if (rightReleased && currentTab == Tab::Recent) { + currentTab = Tab::Files; + selectorIndex = 0; + updateRequired = true; + return; } - // Navigation: Up/Down moves through items, Left/Right also work as prev/next - const bool prevReleased = upReleased || leftReleased; - const bool nextReleased = downReleased || rightReleased; + // Navigation: Up/Down moves through items only + const bool prevReleased = upReleased; + const bool nextReleased = downReleased; if (prevReleased && itemCount > 0) { if (skipPage) { diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index dc87aae6..8e01c666 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -10,10 +10,10 @@ #include "../Activity.h" class MyLibraryActivity final : public Activity { - public: +public: enum class Tab { Recent, Files }; - private: +private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; @@ -22,8 +22,9 @@ class MyLibraryActivity final : public Activity { bool updateRequired = false; // Recent tab state (from RecentBooksActivity) - std::vector bookTitles; // Display titles for each book - std::vector bookPaths; // Paths for each visible book (excludes missing) + std::vector bookTitles; // Display titles for each book + std::vector + bookPaths; // Paths for each visible book (excludes missing) // Files tab state (from FileSelectionActivity) std::string basepath = "/"; @@ -31,7 +32,7 @@ class MyLibraryActivity final : public Activity { // Callbacks const std::function onGoHome; - const std::function onSelectBook; + const std::function onSelectBook; // Number of items that fit on a page int getPageItems() const; @@ -44,21 +45,20 @@ class MyLibraryActivity final : public Activity { void loadFiles(); // Rendering - static void taskTrampoline(void* param); + static void taskTrampoline(void *param); [[noreturn]] void displayTaskLoop(); void render() const; void renderRecentTab() const; void renderFilesTab() const; - public: - explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome, - const std::function& onSelectBook, - Tab initialTab = Tab::Recent) - : Activity("MyLibrary", renderer, mappedInput), - currentTab(initialTab), - onGoHome(onGoHome), - onSelectBook(onSelectBook) {} +public: + explicit MyLibraryActivity( + GfxRenderer &renderer, MappedInputManager &mappedInput, + const std::function &onGoHome, + const std::function &onSelectBook, + Tab initialTab = Tab::Recent) + : Activity("MyLibrary", renderer, mappedInput), currentTab(initialTab), + onGoHome(onGoHome), onSelectBook(onSelectBook) {} void onEnter() override; void onExit() override; void loop() override; From 5d04440407b74d1f8070e784924b9f3f9799c1ba Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 14:13:37 -0500 Subject: [PATCH 08/11] Syntax --- src/activities/home/HomeActivity.cpp | 115 +++++++------------- src/activities/home/HomeActivity.h | 20 ++-- src/main.cpp | 155 ++++++++++----------------- 3 files changed, 107 insertions(+), 183 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 72e4a64b..4e3eb6db 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -15,17 +15,15 @@ #include "fontIds.h" #include "util/StringUtils.h" -void HomeActivity::taskTrampoline(void *param) { - auto *self = static_cast(param); +void HomeActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } int HomeActivity::getMenuItemCount() const { - int count = 3; // Base: My Library, File transfer, Settings - if (hasContinueReading) - count++; - if (hasOpdsUrl) - count++; + int count = 3; // My Library, File transfer, Settings + if (hasContinueReading) count++; + if (hasOpdsUrl) count++; return count; } @@ -35,8 +33,7 @@ void HomeActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); // Check if we have a book to continue reading - hasContinueReading = !APP_STATE.openEpubPath.empty() && - SdMan.exists(APP_STATE.openEpubPath.c_str()); + hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; @@ -72,18 +69,17 @@ void HomeActivity::onEnter() { updateRequired = true; xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask", - 4096, // Stack size - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle + 4096, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle ); } void HomeActivity::onExit() { Activity::onExit(); - // Wait until not rendering to delete task to avoid killing mid-instruction to - // EPD + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); @@ -94,12 +90,10 @@ void HomeActivity::onExit() { } void HomeActivity::loop() { - const bool prevPressed = - mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left); - const bool nextPressed = - mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right); + const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || + mappedInput.wasPressed(MappedInputManager::Button::Left); + const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || + mappedInput.wasPressed(MappedInputManager::Button::Right); const int menuCount = getMenuItemCount(); @@ -175,12 +169,10 @@ void HomeActivity::render() const { constexpr int bookmarkY = bookY + 1; // Main bookmark body (solid) - renderer.fillRect(bookmarkX, bookmarkY, bookmarkWidth, bookmarkHeight, - !bookSelected); + renderer.fillRect(bookmarkX, bookmarkY, bookmarkWidth, bookmarkHeight, !bookSelected); - // Carve out an inverted triangle notch at the bottom center to create - // angled points - const int notchHeight = bookmarkHeight / 2; // depth of the notch + // Carve out an inverted triangle notch at the bottom center to create angled points + const int notchHeight = bookmarkHeight / 2; // depth of the notch for (int i = 0; i < notchHeight; ++i) { const int y = bookmarkY + bookmarkHeight - 1 - i; const int xStart = bookmarkX + i; @@ -218,16 +210,14 @@ void HomeActivity::render() const { const int maxLineWidth = bookWidth - 40; const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID); - for (auto &i : words) { + for (auto& i : words) { // If we just hit the line limit (3), stop processing words if (lines.size() >= 3) { // Limit to 3 lines // Still have words left, so add ellipsis to last line lines.back().append("..."); - while (!lines.back().empty() && - renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > - maxLineWidth) { + while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { lines.back().resize(lines.back().size() - 5); lines.back().append("..."); } @@ -242,8 +232,7 @@ void HomeActivity::render() const { wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); } - int newLineWidth = - renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str()); + int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str()); if (newLineWidth > 0) { newLineWidth += spaceWidth; } @@ -264,8 +253,7 @@ void HomeActivity::render() const { } // Book title text - int totalTextHeight = - renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); + int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); if (!lastBookAuthor.empty()) { totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; } @@ -273,9 +261,8 @@ void HomeActivity::render() const { // Vertically center the title block within the card int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; - for (const auto &line : lines) { - renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), - !bookSelected); + for (const auto& line : lines) { + renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); titleYStart += renderer.getLineHeight(UI_12_FONT_ID); } @@ -283,35 +270,26 @@ void HomeActivity::render() const { titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; std::string trimmedAuthor = lastBookAuthor; // Trim author if too long - while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > - maxLineWidth && - !trimmedAuthor.empty()) { + while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { trimmedAuthor.resize(trimmedAuthor.size() - 5); trimmedAuthor.append("..."); } - renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, - trimmedAuthor.c_str(), !bookSelected); + renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected); } - renderer.drawCenteredText(UI_10_FONT_ID, - bookY + bookHeight - - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2, + renderer.drawCenteredText(UI_10_FONT_ID, bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2, "Continue Reading", !bookSelected); } else { // No book to continue reading - const int y = bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - - renderer.getLineHeight(UI_10_FONT_ID)) / - 2; + const int y = + bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2; renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book"); - renderer.drawCenteredText(UI_10_FONT_ID, - y + renderer.getLineHeight(UI_12_FONT_ID), - "Start reading below"); + renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); } // --- Bottom menu tiles --- // Build menu items dynamically - std::vector menuItems = {"My Library", "File Transfer", - "Settings"}; + std::vector menuItems = {"My Library", "File Transfer", "Settings"}; if (hasOpdsUrl) { // Insert Calibre Library after My Library menuItems.insert(menuItems.begin() + 1, "Calibre Library"); @@ -321,13 +299,11 @@ void HomeActivity::render() const { constexpr int menuTileHeight = 45; constexpr int menuSpacing = 8; const int totalMenuHeight = - static_cast(menuItems.size()) * menuTileHeight + - (static_cast(menuItems.size()) - 1) * menuSpacing; + static_cast(menuItems.size()) * menuTileHeight + (static_cast(menuItems.size()) - 1) * menuSpacing; int menuStartY = bookY + bookHeight + 15; // Ensure we don't collide with the bottom button legend - const int maxMenuStartY = - pageHeight - bottomMargin - totalMenuHeight - margin; + const int maxMenuStartY = pageHeight - bottomMargin - totalMenuHeight - margin; if (menuStartY > maxMenuStartY) { menuStartY = maxMenuStartY; } @@ -335,8 +311,7 @@ void HomeActivity::render() const { for (size_t i = 0; i < menuItems.size(); ++i) { const int overallIndex = static_cast(i) + (hasContinueReading ? 1 : 0); constexpr int tileX = margin; - const int tileY = - menuStartY + static_cast(i) * (menuTileHeight + menuSpacing); + const int tileY = menuStartY + static_cast(i) * (menuTileHeight + menuSpacing); const bool selected = selectorIndex == overallIndex; if (selected) { @@ -345,33 +320,25 @@ void HomeActivity::render() const { renderer.drawRect(tileX, tileY, menuTileWidth, menuTileHeight); } - const char *label = menuItems[i]; + const char* label = menuItems[i]; const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label); const int textX = tileX + (menuTileWidth - textWidth) / 2; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); - const int textY = - tileY + (menuTileHeight - lineHeight) / - 2; // vertically centered assuming y is top of text + const int textY = tileY + (menuTileHeight - lineHeight) / 2; // vertically centered assuming y is top of text - // Invert text when the tile is selected, to contrast with the filled - // background + // Invert text when the tile is selected, to contrast with the filled background renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected); } const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, - labels.btn4); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); const bool showBatteryPercentage = - SETTINGS.hideBatteryPercentage != - CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; + SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; // get percentage so we can align text properly const uint16_t percentage = battery.readPercentage(); - const auto percentageText = - showBatteryPercentage ? std::to_string(percentage) + "%" : ""; - const auto batteryX = - pageWidth - 25 - - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); + const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : ""; + const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); ScreenComponents::drawBattery(renderer, batteryX, 10, showBatteryPercentage); renderer.displayBuffer(); diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 5ec1e416..797efd5c 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -22,21 +22,21 @@ class HomeActivity final : public Activity { const std::function onFileTransferOpen; const std::function onOpdsBrowserOpen; - static void taskTrampoline(void *param); + static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; int getMenuItemCount() const; -public: - explicit HomeActivity(GfxRenderer &renderer, MappedInputManager &mappedInput, - const std::function &onContinueReading, - const std::function &onMyLibraryOpen, - const std::function &onSettingsOpen, - const std::function &onFileTransferOpen, - const std::function &onOpdsBrowserOpen) + public: + explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onContinueReading, const std::function& onMyLibraryOpen, + const std::function& onSettingsOpen, const std::function& onFileTransferOpen, + const std::function& onOpdsBrowserOpen) : Activity("Home", renderer, mappedInput), - onContinueReading(onContinueReading), onMyLibraryOpen(onMyLibraryOpen), - onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen), + onContinueReading(onContinueReading), + onMyLibraryOpen(onMyLibraryOpen), + onSettingsOpen(onSettingsOpen), + onFileTransferOpen(onFileTransferOpen), onOpdsBrowserOpen(onOpdsBrowserOpen) {} void onEnter() override; void onExit() override; diff --git a/src/main.cpp b/src/main.cpp index 41a9918d..25f23512 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,14 +27,14 @@ #define SPI_FQ 40000000 // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) -#define EPD_SCLK 8 // SPI Clock -#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) -#define EPD_CS 21 // Chip Select -#define EPD_DC 4 // Data/Command -#define EPD_RST 5 // Reset -#define EPD_BUSY 6 // Busy +#define EPD_SCLK 8 // SPI Clock +#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) +#define EPD_CS 21 // Chip Select +#define EPD_DC 4 // Data/Command +#define EPD_RST 5 // Reset +#define EPD_BUSY 6 // Busy -#define UART0_RXD 20 // Used for USB connection detection +#define UART0_RXD 20 // Used for USB connection detection #define SD_SPI_MISO 7 @@ -42,98 +42,82 @@ EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); InputManager inputManager; MappedInputManager mappedInputManager(inputManager); GfxRenderer renderer(einkDisplay); -Activity *currentActivity; +Activity* currentActivity; // Fonts EpdFont bookerly12RegularFont(&bookerly_12_regular); EpdFont bookerly12BoldFont(&bookerly_12_bold); EpdFont bookerly12ItalicFont(&bookerly_12_italic); EpdFont bookerly12BoldItalicFont(&bookerly_12_bolditalic); -EpdFontFamily bookerly12FontFamily(&bookerly12RegularFont, &bookerly12BoldFont, - &bookerly12ItalicFont, +EpdFontFamily bookerly12FontFamily(&bookerly12RegularFont, &bookerly12BoldFont, &bookerly12ItalicFont, &bookerly12BoldItalicFont); EpdFont bookerly14RegularFont(&bookerly_14_regular); EpdFont bookerly14BoldFont(&bookerly_14_bold); EpdFont bookerly14ItalicFont(&bookerly_14_italic); EpdFont bookerly14BoldItalicFont(&bookerly_14_bolditalic); -EpdFontFamily bookerly14FontFamily(&bookerly14RegularFont, &bookerly14BoldFont, - &bookerly14ItalicFont, +EpdFontFamily bookerly14FontFamily(&bookerly14RegularFont, &bookerly14BoldFont, &bookerly14ItalicFont, &bookerly14BoldItalicFont); EpdFont bookerly16RegularFont(&bookerly_16_regular); EpdFont bookerly16BoldFont(&bookerly_16_bold); EpdFont bookerly16ItalicFont(&bookerly_16_italic); EpdFont bookerly16BoldItalicFont(&bookerly_16_bolditalic); -EpdFontFamily bookerly16FontFamily(&bookerly16RegularFont, &bookerly16BoldFont, - &bookerly16ItalicFont, +EpdFontFamily bookerly16FontFamily(&bookerly16RegularFont, &bookerly16BoldFont, &bookerly16ItalicFont, &bookerly16BoldItalicFont); EpdFont bookerly18RegularFont(&bookerly_18_regular); EpdFont bookerly18BoldFont(&bookerly_18_bold); EpdFont bookerly18ItalicFont(&bookerly_18_italic); EpdFont bookerly18BoldItalicFont(&bookerly_18_bolditalic); -EpdFontFamily bookerly18FontFamily(&bookerly18RegularFont, &bookerly18BoldFont, - &bookerly18ItalicFont, +EpdFontFamily bookerly18FontFamily(&bookerly18RegularFont, &bookerly18BoldFont, &bookerly18ItalicFont, &bookerly18BoldItalicFont); EpdFont notosans12RegularFont(¬osans_12_regular); EpdFont notosans12BoldFont(¬osans_12_bold); EpdFont notosans12ItalicFont(¬osans_12_italic); EpdFont notosans12BoldItalicFont(¬osans_12_bolditalic); -EpdFontFamily notosans12FontFamily(¬osans12RegularFont, ¬osans12BoldFont, - ¬osans12ItalicFont, +EpdFontFamily notosans12FontFamily(¬osans12RegularFont, ¬osans12BoldFont, ¬osans12ItalicFont, ¬osans12BoldItalicFont); EpdFont notosans14RegularFont(¬osans_14_regular); EpdFont notosans14BoldFont(¬osans_14_bold); EpdFont notosans14ItalicFont(¬osans_14_italic); EpdFont notosans14BoldItalicFont(¬osans_14_bolditalic); -EpdFontFamily notosans14FontFamily(¬osans14RegularFont, ¬osans14BoldFont, - ¬osans14ItalicFont, +EpdFontFamily notosans14FontFamily(¬osans14RegularFont, ¬osans14BoldFont, ¬osans14ItalicFont, ¬osans14BoldItalicFont); EpdFont notosans16RegularFont(¬osans_16_regular); EpdFont notosans16BoldFont(¬osans_16_bold); EpdFont notosans16ItalicFont(¬osans_16_italic); EpdFont notosans16BoldItalicFont(¬osans_16_bolditalic); -EpdFontFamily notosans16FontFamily(¬osans16RegularFont, ¬osans16BoldFont, - ¬osans16ItalicFont, +EpdFontFamily notosans16FontFamily(¬osans16RegularFont, ¬osans16BoldFont, ¬osans16ItalicFont, ¬osans16BoldItalicFont); EpdFont notosans18RegularFont(¬osans_18_regular); EpdFont notosans18BoldFont(¬osans_18_bold); EpdFont notosans18ItalicFont(¬osans_18_italic); EpdFont notosans18BoldItalicFont(¬osans_18_bolditalic); -EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, - ¬osans18ItalicFont, +EpdFontFamily notosans18FontFamily(¬osans18RegularFont, ¬osans18BoldFont, ¬osans18ItalicFont, ¬osans18BoldItalicFont); EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular); EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold); EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic); EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic); -EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, - &opendyslexic8BoldFont, - &opendyslexic8ItalicFont, +EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, &opendyslexic8BoldFont, &opendyslexic8ItalicFont, &opendyslexic8BoldItalicFont); EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular); EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold); EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic); EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic); -EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, - &opendyslexic10BoldFont, - &opendyslexic10ItalicFont, +EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, &opendyslexic10BoldFont, &opendyslexic10ItalicFont, &opendyslexic10BoldItalicFont); EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular); EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold); EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic); EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic); -EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, - &opendyslexic12BoldFont, - &opendyslexic12ItalicFont, +EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, &opendyslexic12BoldFont, &opendyslexic12ItalicFont, &opendyslexic12BoldItalicFont); EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular); EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold); EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic); EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic); -EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, - &opendyslexic14BoldFont, - &opendyslexic14ItalicFont, +EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, &opendyslexic14BoldFont, &opendyslexic14ItalicFont, &opendyslexic14BoldItalicFont); EpdFont smallFont(¬osans_8_regular); @@ -159,33 +143,27 @@ void exitActivity() { } } -void enterNewActivity(Activity *activity) { +void enterNewActivity(Activity* activity) { currentActivity = activity; currentActivity->onEnter(); } // Verify long press on wake-up from deep sleep void verifyWakeupLongPress() { - // Give the user up to 1000ms to start holding the power button, and must hold - // for SETTINGS.getPowerButtonDuration() + // Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration() const auto start = millis(); bool abort = false; - // Subtract the current time, because inputManager only starts counting the - // HeldTime from the first update() This way, we remove the time we already - // took to reach here from the duration, assuming the button was held until - // now from millis()==0 (i.e. device start time). + // Subtract the current time, because inputManager only starts counting the HeldTime from the first update() + // This way, we remove the time we already took to reach here from the duration, + // assuming the button was held until now from millis()==0 (i.e. device start time). const uint16_t calibration = start; const uint16_t calibratedPressDuration = - (calibration < SETTINGS.getPowerButtonDuration()) - ? SETTINGS.getPowerButtonDuration() - calibration - : 1; + (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; inputManager.update(); // Verify the user has actually pressed - while (!inputManager.isPressed(InputManager::BTN_POWER) && - millis() - start < 1000) { - delay(10); // only wait 10ms each iteration to not delay too much in case of - // short configured duration. + while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { + delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. inputManager.update(); } @@ -194,8 +172,7 @@ void verifyWakeupLongPress() { do { delay(10); inputManager.update(); - } while (inputManager.isPressed(InputManager::BTN_POWER) && - inputManager.getHeldTime() < calibratedPressDuration); + } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration); abort = inputManager.getHeldTime() < calibratedPressDuration; } else { abort = true; @@ -204,8 +181,7 @@ void verifyWakeupLongPress() { if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, - ESP_GPIO_WAKEUP_GPIO_LOW); + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); esp_deep_sleep_start(); } } @@ -224,55 +200,46 @@ void enterDeepSleep() { enterNewActivity(new SleepActivity(renderer, mappedInputManager)); einkDisplay.deepSleep(); - Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", - millis(), t2 - t1); + Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, - ESP_GPIO_WAKEUP_GPIO_LOW); - // Ensure that the power button has been released to avoid immediately turning - // back on if you're holding it + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); + // Ensure that the power button has been released to avoid immediately turning back on if you're holding it waitForPowerRelease(); // Enter Deep Sleep esp_deep_sleep_start(); } void onGoHome(); -void onGoToReader(const std::string &initialEpubPath) { +void onGoToReader(const std::string& initialEpubPath) { exitActivity(); - enterNewActivity(new ReaderActivity(renderer, mappedInputManager, - initialEpubPath, onGoHome)); + enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome)); } void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } void onGoToFileTransfer() { exitActivity(); - enterNewActivity( - new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); + enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); } void onGoToSettings() { exitActivity(); - enterNewActivity( - new SettingsActivity(renderer, mappedInputManager, onGoHome)); + enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } void onGoToMyLibrary() { exitActivity(); - enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, - onGoToReader)); + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); } void onGoToBrowser() { exitActivity(); - enterNewActivity( - new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); + enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); } void onGoHome() { exitActivity(); - enterNewActivity(new HomeActivity( - renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, - onGoToSettings, onGoToFileTransfer, onGoToBrowser)); + enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, + onGoToFileTransfer, onGoToBrowser)); } void setupDisplayAndFonts() { @@ -318,8 +285,7 @@ void setup() { Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); setupDisplayAndFonts(); exitActivity(); - enterNewActivity(new FullScreenMessageActivity( - renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); + enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); return; } @@ -328,11 +294,8 @@ void setup() { // verify power button press duration after we've read settings. verifyWakeupLongPress(); - // First serial output only here to avoid timing inconsistencies for power - // button press duration verification - Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION - "\n", - millis()); + // First serial output only here to avoid timing inconsistencies for power button press duration verification + Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis()); setupDisplayAndFonts(); @@ -345,8 +308,7 @@ void setup() { if (APP_STATE.openEpubPath.empty()) { onGoHome(); } else { - // Clear app state to avoid getting into a boot loop if the epub doesn't - // load + // Clear app state to avoid getting into a boot loop if the epub doesn't load const auto path = APP_STATE.openEpubPath; APP_STATE.openEpubPath = ""; APP_STATE.saveToFile(); @@ -365,25 +327,21 @@ void loop() { inputManager.update(); if (Serial && millis() - lastMemPrint >= 10000) { - Serial.printf( - "[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", - millis(), ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap()); + Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), + ESP.getHeapSize(), ESP.getMinFreeHeap()); lastMemPrint = millis(); } - // Check for any user activity (button press or release) or active background - // work + // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { - lastActivityTime = millis(); // Reset inactivity timer + lastActivityTime = millis(); // Reset inactivity timer } const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); if (millis() - lastActivityTime >= sleepTimeoutMs) { - Serial.printf( - "[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", - millis(), sleepTimeoutMs); + Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), sleepTimeoutMs); enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return; @@ -406,18 +364,17 @@ void loop() { if (loopDuration > maxLoopDuration) { maxLoopDuration = loopDuration; if (maxLoopDuration > 50) { - Serial.printf( - "[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", - millis(), maxLoopDuration, activityDuration); + Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration, + activityDuration); } } // Add delay at the end of the loop to prevent tight spinning - // When an activity requests skip loop delay (e.g., webserver running), use - // yield() for faster response Otherwise, use longer delay to save power + // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response + // Otherwise, use longer delay to save power if (currentActivity && currentActivity->skipLoopDelay()) { - yield(); // Give FreeRTOS a chance to run tasks, but return immediately + yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else { - delay(10); // Normal delay when no activity requires fast response + delay(10); // Normal delay when no activity requires fast response } } From 373a7194e7f860d8bd6e1a97ca4a704317e90d36 Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 14:25:39 -0500 Subject: [PATCH 09/11] Update MyLibraryActivity.cpp --- src/activities/home/MyLibraryActivity.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 23bc9a85..0feee15d 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -71,13 +71,20 @@ int MyLibraryActivity::getCurrentPage() const { } void MyLibraryActivity::loadRecentBooks() { + constexpr size_t MAX_RECENT_BOOKS = 20; + bookTitles.clear(); bookPaths.clear(); const auto &books = RECENT_BOOKS.getBooks(); - bookTitles.reserve(books.size()); - bookPaths.reserve(books.size()); + bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); + bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); for (const auto &path : books) { + // Limit to maximum number of recent books + if (bookTitles.size() >= MAX_RECENT_BOOKS) { + break; + } + // Skip if file no longer exists if (!SdMan.exists(path.c_str())) { continue; From 2f62c0bcc44830ce338e0c115f137339f4e0991b Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 14:36:37 -0500 Subject: [PATCH 10/11] Clean up unused files --- src/activities/home/MyLibraryActivity.cpp | 19 -- src/activities/home/MyLibraryActivity.h | 2 +- src/activities/home/RecentBooksActivity.cpp | 184 -------------------- src/activities/home/RecentBooksActivity.h | 37 ---- 4 files changed, 1 insertion(+), 241 deletions(-) delete mode 100644 src/activities/home/RecentBooksActivity.cpp delete mode 100644 src/activities/home/RecentBooksActivity.h diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 0feee15d..01b34a4f 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -1,6 +1,5 @@ #include "MyLibraryActivity.h" -#include #include #include @@ -97,24 +96,6 @@ void MyLibraryActivity::loadRecentBooks() { title = title.substr(lastSlash + 1); } - const std::string ext5 = - title.length() >= 5 ? title.substr(title.length() - 5) : ""; - const std::string ext4 = - title.length() >= 4 ? title.substr(title.length() - 4) : ""; - - // If epub, try to load the metadata for title - if (ext5 == ".epub") { - Epub epub(path, "/.crosspoint"); - epub.load(false); - if (!epub.getTitle().empty()) { - title = std::string(epub.getTitle()); - } - } else if (ext5 == ".xtch") { - title.resize(title.length() - 5); - } else if (ext4 == ".xtc") { - title.resize(title.length() - 4); - } - bookTitles.push_back(title); bookPaths.push_back(path); } diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index 8e01c666..ccea820a 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -21,7 +21,7 @@ private: int selectorIndex = 0; bool updateRequired = false; - // Recent tab state (from RecentBooksActivity) + // Recent tab state std::vector bookTitles; // Display titles for each book std::vector bookPaths; // Paths for each visible book (excludes missing) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp deleted file mode 100644 index b5d081ec..00000000 --- a/src/activities/home/RecentBooksActivity.cpp +++ /dev/null @@ -1,184 +0,0 @@ -#include "RecentBooksActivity.h" - -#include -#include -#include - -#include "MappedInputManager.h" -#include "RecentBooksStore.h" -#include "fontIds.h" - -namespace { -// Time threshold for treating a long press as a page-up/page-down -constexpr int SKIP_PAGE_MS = 700; -} // namespace - -int RecentBooksActivity::getPageItems() const { - // Layout constants used in render - constexpr int startY = 60; - constexpr int lineHeight = 30; - - const int screenHeight = renderer.getScreenHeight(); - const int availableHeight = screenHeight - startY; - int items = availableHeight / lineHeight; - - // Ensure we always have at least one item per page to avoid division by zero - if (items < 1) { - items = 1; - } - return items; -} - -void RecentBooksActivity::taskTrampoline(void* param) { - auto* self = static_cast(param); - self->displayTaskLoop(); -} - -void RecentBooksActivity::onEnter() { - Activity::onEnter(); - - renderingMutex = xSemaphoreCreateMutex(); - - // Load book titles from recent books list - bookTitles.clear(); - bookPaths.clear(); - const auto& books = RECENT_BOOKS.getBooks(); - bookTitles.reserve(books.size()); - bookPaths.reserve(books.size()); - - for (const auto& path : books) { - // Skip if file no longer exists - if (!SdMan.exists(path.c_str())) { - continue; - } - - // Extract filename from path for display - std::string title = path; - const size_t lastSlash = title.find_last_of('/'); - if (lastSlash != std::string::npos) { - title = title.substr(lastSlash + 1); - } - - const std::string ext5 = title.length() >= 5 ? title.substr(title.length() - 5) : ""; - const std::string ext4 = title.length() >= 4 ? title.substr(title.length() - 4) : ""; - - // If epub, try to load the metadata for title - if (ext5 == ".epub") { - Epub epub(path, "/.crosspoint"); - epub.load(false); - if (!epub.getTitle().empty()) { - title = std::string(epub.getTitle()); - } - } else if (ext5 == ".xtch") { - title.resize(title.length() - 5); - } else if (ext4 == ".xtc") { - title.resize(title.length() - 4); - } - - bookTitles.push_back(title); - bookPaths.push_back(path); - } - - selectorIndex = 0; - - // Trigger first update - updateRequired = true; - - xTaskCreate(&RecentBooksActivity::taskTrampoline, "RecentBooksActivityTask", - 4096, // Stack size - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle - ); -} - -void RecentBooksActivity::onExit() { - Activity::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; - bookTitles.clear(); - bookPaths.clear(); -} - -void RecentBooksActivity::loop() { - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - const int pageItems = getPageItems(); - const int bookCount = static_cast(bookTitles.size()); - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (bookCount > 0 && selectorIndex < bookCount) { - onSelectBook(bookPaths[selectorIndex]); - } - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoBack(); - } else if (prevReleased && bookCount > 0) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + bookCount) % bookCount; - } else { - selectorIndex = (selectorIndex + bookCount - 1) % bookCount; - } - updateRequired = true; - } else if (nextReleased && bookCount > 0) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % bookCount; - } else { - selectorIndex = (selectorIndex + 1) % bookCount; - } - updateRequired = true; - } -} - -void RecentBooksActivity::displayTaskLoop() { - while (true) { - if (updateRequired) { - updateRequired = false; - xSemaphoreTake(renderingMutex, portMAX_DELAY); - render(); - xSemaphoreGive(renderingMutex); - } - vTaskDelay(10 / portTICK_PERIOD_MS); - } -} - -void RecentBooksActivity::render() const { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const int pageItems = getPageItems(); - const int bookCount = static_cast(bookTitles.size()); - - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Recent Books", true, EpdFontFamily::BOLD); - - // Help text - const auto labels = mappedInput.mapLabels("« Back", "Open", "", ""); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - if (bookCount == 0) { - renderer.drawText(UI_10_FONT_ID, 20, 60, "No recent books"); - renderer.displayBuffer(); - return; - } - - const auto pageStartIndex = selectorIndex / pageItems * pageItems; - renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); - - for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, item.c_str(), i != selectorIndex); - } - - renderer.displayBuffer(); -} diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h deleted file mode 100644 index 3e386787..00000000 --- a/src/activities/home/RecentBooksActivity.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include -#include -#include - -#include -#include -#include - -#include "../Activity.h" - -class RecentBooksActivity final : public Activity { - TaskHandle_t displayTaskHandle = nullptr; - SemaphoreHandle_t renderingMutex = nullptr; - int selectorIndex = 0; - bool updateRequired = false; - std::vector bookTitles; // Display titles for each book - std::vector bookPaths; // Paths for each visible book (excludes missing) - const std::function onGoBack; - const std::function onSelectBook; - - // Number of items that fit on a page - int getPageItems() const; - - static void taskTrampoline(void* param); - [[noreturn]] void displayTaskLoop(); - void render() const; - - public: - explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoBack, - const std::function& onSelectBook) - : Activity("RecentBooks", renderer, mappedInput), onGoBack(onGoBack), onSelectBook(onSelectBook) {} - void onEnter() override; - void onExit() override; - void loop() override; -}; From 958d1e032da120853dd61d6df11cd513e956446c Mon Sep 17 00:00:00 2001 From: Kenneth Date: Tue, 13 Jan 2026 14:43:19 -0500 Subject: [PATCH 11/11] clang format --- src/ScreenComponents.cpp | 80 +++++-------- src/ScreenComponents.h | 16 +-- src/activities/home/MyLibraryActivity.cpp | 139 ++++++++-------------- src/activities/home/MyLibraryActivity.h | 30 ++--- 4 files changed, 103 insertions(+), 162 deletions(-) diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index f414ca06..2e8d9e7c 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -8,12 +8,11 @@ #include "Battery.h" #include "fontIds.h" -void ScreenComponents::drawBattery(const GfxRenderer &renderer, const int left, - const int top, const bool showPercentage) { +void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top, + const bool showPercentage) { // Left aligned battery icon and percentage const uint16_t percentage = battery.readPercentage(); - const auto percentageText = - showPercentage ? std::to_string(percentage) + "%" : ""; + const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : ""; renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str()); // 1 column on left, 2 columns on right, 5 columns of battery body @@ -25,53 +24,46 @@ void ScreenComponents::drawBattery(const GfxRenderer &renderer, const int left, // Top line renderer.drawLine(x + 1, y, x + batteryWidth - 3, y); // Bottom line - renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, - y + batteryHeight - 1); + renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1); // Left line renderer.drawLine(x, y + 1, x, y + batteryHeight - 2); // Battery end - renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, - y + batteryHeight - 2); + renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2); renderer.drawPixel(x + batteryWidth - 1, y + 3); renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4); - renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, - y + batteryHeight - 5); + renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5); // The +1 is to round up, so that we always fill at least one pixel int filledWidth = percentage * (batteryWidth - 5) / 100 + 1; if (filledWidth > batteryWidth - 5) { - filledWidth = batteryWidth - 5; // Ensure we don't overflow + filledWidth = batteryWidth - 5; // Ensure we don't overflow } renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); } -int ScreenComponents::drawTabBar(const GfxRenderer &renderer, const int y, - const std::vector &tabs) { - constexpr int tabPadding = 20; // Horizontal padding between tabs - constexpr int leftMargin = 20; // Left margin for first tab - constexpr int underlineHeight = 2; // Height of selection underline - constexpr int underlineGap = 4; // Gap between text and underline +int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { + constexpr int tabPadding = 20; // Horizontal padding between tabs + constexpr int leftMargin = 20; // Left margin for first tab + constexpr int underlineHeight = 2; // Height of selection underline + constexpr int underlineGap = 4; // Gap between text and underline const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); const int tabBarHeight = lineHeight + underlineGap + underlineHeight; int currentX = leftMargin; - for (const auto &tab : tabs) { - const int textWidth = renderer.getTextWidth( - UI_12_FONT_ID, tab.label, - tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + for (const auto& tab : tabs) { + const int textWidth = + renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); // Draw tab label renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true, - tab.selected ? EpdFontFamily::BOLD - : EpdFontFamily::REGULAR); + tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); // Draw underline for selected tab if (tab.selected) { - renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, - underlineHeight); + renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight); } currentX += textWidth + tabPadding; @@ -80,64 +72,53 @@ int ScreenComponents::drawTabBar(const GfxRenderer &renderer, const int y, return tabBarHeight; } -void ScreenComponents::drawScrollIndicator(const GfxRenderer &renderer, - const int currentPage, - const int totalPages, - const int contentTop, - const int contentHeight) { +void ScreenComponents::drawScrollIndicator(const GfxRenderer& renderer, const int currentPage, const int totalPages, + const int contentTop, const int contentHeight) { if (totalPages <= 1) { - return; // No need for indicator if only one page + return; // No need for indicator if only one page } const int screenWidth = renderer.getScreenWidth(); constexpr int indicatorWidth = 20; constexpr int arrowSize = 6; - constexpr int margin = 15; // Offset from right edge + constexpr int margin = 15; // Offset from right edge const int centerX = screenWidth - indicatorWidth / 2 - margin; - const int indicatorTop = - contentTop + 60; // Offset to avoid overlapping side button hints + const int indicatorTop = contentTop + 60; // Offset to avoid overlapping side button hints const int indicatorBottom = contentTop + contentHeight - 30; // Draw up arrow at top (^) - narrow point at top, wide base at bottom for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + i * 2; const int startX = centerX - i; - renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, - indicatorTop + i); + renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i); } // Draw down arrow at bottom (v) - wide base at top, narrow point at bottom for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + (arrowSize - 1 - i) * 2; const int startX = centerX - (arrowSize - 1 - i); - renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, - startX + lineWidth - 1, + renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1, indicatorBottom - arrowSize + 1 + i); } // Draw page fraction in the middle (e.g., "1/3") - const std::string pageText = - std::to_string(currentPage) + "/" + std::to_string(totalPages); + const std::string pageText = std::to_string(currentPage) + "/" + std::to_string(totalPages); const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, pageText.c_str()); const int textX = centerX - textWidth / 2; - const int textY = (indicatorTop + indicatorBottom) / 2 - - renderer.getLineHeight(SMALL_FONT_ID) / 2; + const int textY = (indicatorTop + indicatorBottom) / 2 - renderer.getLineHeight(SMALL_FONT_ID) / 2; renderer.drawText(SMALL_FONT_ID, textX, textY, pageText.c_str()); } -void ScreenComponents::drawProgressBar(const GfxRenderer &renderer, const int x, - const int y, const int width, - const int height, const size_t current, - const size_t total) { +void ScreenComponents::drawProgressBar(const GfxRenderer& renderer, const int x, const int y, const int width, + const int height, const size_t current, const size_t total) { if (total == 0) { return; } // Use 64-bit arithmetic to avoid overflow for large files - const int percent = - static_cast((static_cast(current) * 100) / total); + const int percent = static_cast((static_cast(current) * 100) / total); // Draw outline renderer.drawRect(x, y, width, height); @@ -150,6 +131,5 @@ void ScreenComponents::drawProgressBar(const GfxRenderer &renderer, const int x, // Draw percentage text centered below bar const std::string percentText = std::to_string(percent) + "%"; - renderer.drawCenteredText(UI_10_FONT_ID, y + height + 15, - percentText.c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, y + height + 15, percentText.c_str()); } diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 7ebb43f8..48c40f42 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -7,24 +7,21 @@ class GfxRenderer; struct TabInfo { - const char *label; + const char* label; bool selected; }; class ScreenComponents { -public: - static void drawBattery(const GfxRenderer &renderer, int left, int top, - bool showPercentage = true); + public: + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); // Draw a horizontal tab bar with underline indicator for selected tab // Returns the height of the tab bar (for positioning content below) - static int drawTabBar(const GfxRenderer &renderer, int y, - const std::vector &tabs); + static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector& tabs); // Draw a scroll/page indicator on the right side of the screen // Shows up/down arrows and current page fraction (e.g., "1/3") - static void drawScrollIndicator(const GfxRenderer &renderer, int currentPage, - int totalPages, int contentTop, + static void drawScrollIndicator(const GfxRenderer& renderer, int currentPage, int totalPages, int contentTop, int contentHeight); /** @@ -37,7 +34,6 @@ public: * @param current Current progress value * @param total Total value for 100% progress */ - static void drawProgressBar(const GfxRenderer &renderer, int x, int y, - int width, int height, size_t current, + static void drawProgressBar(const GfxRenderer& renderer, int x, int y, int width, int height, size_t current, size_t total); }; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 01b34a4f..60b7ad35 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -16,31 +16,26 @@ constexpr int TAB_BAR_Y = 15; constexpr int CONTENT_START_Y = 60; constexpr int LINE_HEIGHT = 30; constexpr int LEFT_MARGIN = 20; -constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator +constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator // Timing thresholds constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; -void sortFileList(std::vector &strs) { - std::sort(begin(strs), end(strs), - [](const std::string &str1, const std::string &str2) { - if (str1.back() == '/' && str2.back() != '/') - return true; - if (str1.back() != '/' && str2.back() == '/') - return false; - return lexicographical_compare( - begin(str1), end(str1), begin(str2), end(str2), - [](const char &char1, const char &char2) { - return tolower(char1) < tolower(char2); - }); - }); +void sortFileList(std::vector& strs) { + std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { + if (str1.back() == '/' && str2.back() != '/') return true; + if (str1.back() != '/' && str2.back() == '/') return false; + return lexicographical_compare( + begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); + }); } -} // namespace +} // namespace int MyLibraryActivity::getPageItems() const { const int screenHeight = renderer.getScreenHeight(); - const int bottomBarHeight = 60; // Space for button hints + const int bottomBarHeight = 60; // Space for button hints const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight; int items = availableHeight / LINE_HEIGHT; if (items < 1) { @@ -59,8 +54,7 @@ int MyLibraryActivity::getCurrentItemCount() const { int MyLibraryActivity::getTotalPages() const { const int itemCount = getCurrentItemCount(); const int pageItems = getPageItems(); - if (itemCount == 0) - return 1; + if (itemCount == 0) return 1; return (itemCount + pageItems - 1) / pageItems; } @@ -74,11 +68,11 @@ void MyLibraryActivity::loadRecentBooks() { bookTitles.clear(); bookPaths.clear(); - const auto &books = RECENT_BOOKS.getBooks(); + const auto& books = RECENT_BOOKS.getBooks(); bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); - for (const auto &path : books) { + for (const auto& path : books) { // Limit to maximum number of recent books if (bookTitles.size() >= MAX_RECENT_BOOKS) { break; @@ -106,8 +100,7 @@ void MyLibraryActivity::loadFiles() { auto root = SdMan.open(basepath.c_str()); if (!root || !root.isDirectory()) { - if (root) - root.close(); + if (root) root.close(); return; } @@ -125,10 +118,8 @@ void MyLibraryActivity::loadFiles() { files.emplace_back(std::string(name) + "/"); } else { auto filename = std::string(name); - std::string ext4 = - filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; - std::string ext5 = - filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; + std::string ext4 = filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; + std::string ext5 = filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; if (ext5 == ".epub" || ext5 == ".xtch" || ext4 == ".xtc") { files.emplace_back(filename); } @@ -139,8 +130,8 @@ void MyLibraryActivity::loadFiles() { sortFileList(files); } -void MyLibraryActivity::taskTrampoline(void *param) { - auto *self = static_cast(param); +void MyLibraryActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } @@ -157,10 +148,10 @@ void MyLibraryActivity::onEnter() { updateRequired = true; xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", - 4096, // Stack size (increased for epub metadata loading) - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle + 4096, // Stack size (increased for epub metadata loading) + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle ); } @@ -187,8 +178,7 @@ void MyLibraryActivity::loop() { const int pageItems = getPageItems(); // Long press BACK (1s+) in Files tab goes to root folder - if (currentTab == Tab::Files && - mappedInput.isPressed(MappedInputManager::Button::Back) && + if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (basepath != "/") { basepath = "/"; @@ -199,33 +189,26 @@ void MyLibraryActivity::loop() { return; } - const bool upReleased = - mappedInput.wasReleased(MappedInputManager::Button::Up); - const bool downReleased = - mappedInput.wasReleased(MappedInputManager::Button::Down); - const bool leftReleased = - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool rightReleased = - mappedInput.wasReleased(MappedInputManager::Button::Right); + const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); + const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); + const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; // Confirm button - open selected item if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (currentTab == Tab::Recent) { - if (!bookPaths.empty() && - selectorIndex < static_cast(bookPaths.size())) { + if (!bookPaths.empty() && selectorIndex < static_cast(bookPaths.size())) { onSelectBook(bookPaths[selectorIndex]); } } else { // Files tab if (!files.empty() && selectorIndex < static_cast(files.size())) { - if (basepath.back() != '/') - basepath += "/"; + if (basepath.back() != '/') basepath += "/"; if (files[selectorIndex].back() == '/') { // Enter directory - basepath += - files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); loadFiles(); selectorIndex = 0; updateRequired = true; @@ -244,8 +227,7 @@ void MyLibraryActivity::loop() { if (currentTab == Tab::Files && basepath != "/") { // Go up one directory basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) - basepath = "/"; + if (basepath.empty()) basepath = "/"; loadFiles(); selectorIndex = 0; updateRequired = true; @@ -277,8 +259,7 @@ void MyLibraryActivity::loop() { if (prevReleased && itemCount > 0) { if (skipPage) { - selectorIndex = - ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; } else { selectorIndex = (selectorIndex + itemCount - 1) % itemCount; } @@ -309,8 +290,7 @@ void MyLibraryActivity::render() const { renderer.clearScreen(); // Draw tab bar - std::vector tabs = {{"Recent", currentTab == Tab::Recent}, - {"Files", currentTab == Tab::Files}}; + std::vector tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); // Draw content based on current tab @@ -322,11 +302,8 @@ void MyLibraryActivity::render() const { // Draw scroll indicator const int screenHeight = renderer.getScreenHeight(); - const int contentHeight = - screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar - ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), - getTotalPages(), CONTENT_START_Y, - contentHeight); + const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar + ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight); // Draw side button hints (up/down navigation on right side) // Note: text is rotated 90° CW, so ">" appears as "^" and "<" appears as "v" @@ -334,11 +311,9 @@ void MyLibraryActivity::render() const { // Draw bottom button hints // In Files tab, show "BACK" when in subdirectory, "HOME" when at root - const char *backLabel = - (currentTab == Tab::Files && basepath != "/") ? "BACK" : "HOME"; + const char* backLabel = (currentTab == Tab::Files && basepath != "/") ? "BACK" : "HOME"; const auto labels = mappedInput.mapLabels(backLabel, "OPEN", "<", ">"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, - labels.btn4); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); } @@ -349,26 +324,21 @@ void MyLibraryActivity::renderRecentTab() const { const int bookCount = static_cast(bookTitles.size()); if (bookCount == 0) { - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, - "No recent books"); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect( - 0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, - pageWidth - RIGHT_MARGIN, LINE_HEIGHT); + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, + LINE_HEIGHT); // Draw items - for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; - i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), - pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, - CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, - item.c_str(), i != selectorIndex); + for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), + i != selectorIndex); } } @@ -378,25 +348,20 @@ void MyLibraryActivity::renderFilesTab() const { const int fileCount = static_cast(files.size()); if (fileCount == 0) { - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, - "No books found"); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect( - 0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, - pageWidth - RIGHT_MARGIN, LINE_HEIGHT); + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, + LINE_HEIGHT); // Draw items - for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; - i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), - pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, - CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, - item.c_str(), i != selectorIndex); + for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), + i != selectorIndex); } } diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index ccea820a..e6de28ca 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -10,10 +10,10 @@ #include "../Activity.h" class MyLibraryActivity final : public Activity { -public: + public: enum class Tab { Recent, Files }; -private: + private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; @@ -22,9 +22,8 @@ private: bool updateRequired = false; // Recent tab state - std::vector bookTitles; // Display titles for each book - std::vector - bookPaths; // Paths for each visible book (excludes missing) + std::vector bookTitles; // Display titles for each book + std::vector bookPaths; // Paths for each visible book (excludes missing) // Files tab state (from FileSelectionActivity) std::string basepath = "/"; @@ -32,7 +31,7 @@ private: // Callbacks const std::function onGoHome; - const std::function onSelectBook; + const std::function onSelectBook; // Number of items that fit on a page int getPageItems() const; @@ -45,20 +44,21 @@ private: void loadFiles(); // Rendering - static void taskTrampoline(void *param); + static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; void renderRecentTab() const; void renderFilesTab() const; -public: - explicit MyLibraryActivity( - GfxRenderer &renderer, MappedInputManager &mappedInput, - const std::function &onGoHome, - const std::function &onSelectBook, - Tab initialTab = Tab::Recent) - : Activity("MyLibrary", renderer, mappedInput), currentTab(initialTab), - onGoHome(onGoHome), onSelectBook(onSelectBook) {} + public: + explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onGoHome, + const std::function& onSelectBook, + Tab initialTab = Tab::Recent) + : Activity("MyLibrary", renderer, mappedInput), + currentTab(initialTab), + onGoHome(onGoHome), + onSelectBook(onSelectBook) {} void onEnter() override; void onExit() override; void loop() override;