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/ScreenComponents.cpp b/src/ScreenComponents.cpp index 42b6ef7b..2e8d9e7c 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -42,6 +42,75 @@ 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 = 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 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); + } + + // 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, + 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()); +} + 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) { diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 150fb0c8..48c40f42 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -2,13 +2,28 @@ #include #include +#include class GfxRenderer; +struct TabInfo { + const char* label; + bool selected; +}; + class ScreenComponents { 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); + + // 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); + /** * Draw a progress bar with percentage text. * @param renderer The graphics renderer diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 3a97e132..14ec1c53 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -23,7 +23,7 @@ void HomeActivity::taskTrampoline(void* param) { } int HomeActivity::getMenuItemCount() const { - int count = 3; // Browse files, File transfer, Settings + int count = 3; // My Library, File transfer, Settings if (hasContinueReading) count++; if (hasOpdsUrl) count++; return count; @@ -169,15 +169,15 @@ void HomeActivity::loop() { // Calculate dynamic indices based on which options are available int idx = 0; const int continueIdx = hasContinueReading ? idx++ : -1; - const int browseFilesIdx = idx++; + const int myLibraryIdx = idx++; const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1; const int fileTransferIdx = idx++; const int settingsIdx = idx; if (selectorIndex == continueIdx) { onContinueReading(); - } else if (selectorIndex == browseFilesIdx) { - onReaderOpen(); + } else if (selectorIndex == myLibraryIdx) { + onMyLibraryOpen(); } else if (selectorIndex == opdsLibraryIdx) { onOpdsBrowserOpen(); } else if (selectorIndex == fileTransferIdx) { @@ -496,9 +496,9 @@ void HomeActivity::render() { // --- Bottom menu tiles --- // Build menu items dynamically - std::vector menuItems = {"Browse Files", "File Transfer", "Settings"}; + std::vector menuItems = {"My Library", "File Transfer", "Settings"}; if (hasOpdsUrl) { - // Insert Calibre Library after Browse Files + // Insert Calibre Library after My Library menuItems.insert(menuItems.begin() + 1, "Calibre Library"); } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 68af0591..52963514 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -22,7 +22,7 @@ class HomeActivity final : public Activity { std::string lastBookAuthor; std::string coverBmpPath; const std::function onContinueReading; - const std::function onReaderOpen; + const std::function onMyLibraryOpen; const std::function onSettingsOpen; const std::function onFileTransferOpen; const std::function onOpdsBrowserOpen; @@ -37,12 +37,12 @@ class HomeActivity final : public Activity { public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onContinueReading, const std::function& onReaderOpen, + 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), - onReaderOpen(onReaderOpen), + onMyLibraryOpen(onMyLibraryOpen), onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen), onOpdsBrowserOpen(onOpdsBrowserOpen) {} diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp new file mode 100644 index 00000000..60b7ad35 --- /dev/null +++ b/src/activities/home/MyLibraryActivity.cpp @@ -0,0 +1,367 @@ +#include "MyLibraryActivity.h" + +#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() { + constexpr size_t MAX_RECENT_BOOKS = 20; + + bookTitles.clear(); + bookPaths.clear(); + 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) { + // 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; + } + + // 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); + } + + 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 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 only + const bool prevReleased = upReleased; + const bool nextReleased = downReleased; + + 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 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 + // 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(); +} + +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..e6de28ca --- /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 + 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/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 2eeba80f..074cd513 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" @@ -74,9 +75,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 9cdf5c97..bfb29a47 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 8a7c3b91..6341b04b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,10 +13,12 @@ #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/browser/OpdsBookBrowserActivity.h" #include "activities/home/HomeActivity.h" +#include "activities/home/MyLibraryActivity.h" #include "activities/network/CrossPointWebServerActivity.h" #include "activities/reader/ReaderActivity.h" #include "activities/settings/SettingsActivity.h" @@ -214,7 +216,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() { @@ -227,6 +228,11 @@ void onGoToSettings() { enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } +void onGoToMyLibrary() { + exitActivity(); + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); +} + void onGoToBrowser() { exitActivity(); enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); @@ -234,7 +240,7 @@ void onGoToBrowser() { void onGoHome() { exitActivity(); - enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, + enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser)); } @@ -302,6 +308,8 @@ void setup() { enterNewActivity(new BootActivity(renderer, mappedInputManager)); APP_STATE.loadFromFile(); + RECENT_BOOKS.loadFromFile(); + if (APP_STATE.openEpubPath.empty()) { onGoHome(); } else {