From d2b6506d23c3eab17aebd82cf250f1eee71d36b5 Mon Sep 17 00:00:00 2001 From: Kenneth <6720728+codebykenny@users.noreply.github.com> Date: Wed, 21 Jan 2026 05:38:38 -0600 Subject: [PATCH] My Library: Tab bar w/ Recent Books + File Browser (#250) # Summary This PR introduces a reusable Tab Bar component and combines the Recent Books and File Browser into a unified tabbed page called "My Library" accessible from the Home screen. ## Features ### New Tab Bar Component A flexible, reusable tab bar component added to `ScreenComponents` that can be used throughout the application. ### New Scroll Indicator Component A page position indicator for lists that span multiple pages. **Features:** - Up/down arrow indicators - Current page fraction display (e.g., "1/3") - Only renders when content spans multiple pages ### My Library Activity A new unified view combining Recent Books and File Browser into a single tabbed page. **Tabs:** - **Recent** - Shows recently opened books - **Files** - Browse SD card directory structure **Navigation:** - Up/Down or Left/Right: Navigate through list items - Left/Right (when first item selected): Switch between tabs - Confirm: Open selected book or enter directory - Back: Go up directory (Files tab) or return home - Long press Back: Jump to root directory (Files tab) **UI Elements:** - Tab bar with selection indicator - Scroll/page indicator on right side - Side button hints (up/down arrows) - Dynamic bottom button labels ("BACK" in subdirectories, "HOME" at root) ## Tab Bar Usage The tab bar component is designed to be reusable across different activities. Here's how to use it: ### Basic Example ```cpp #include "ScreenComponents.h" void MyActivity::render() const { renderer.clearScreen(); // Define tabs with labels and selection state std::vector tabs = { {"Tab One", currentTab == 0}, // Selected when currentTab is 0 {"Tab Two", currentTab == 1}, // Selected when currentTab is 1 {"Tab Three", currentTab == 2} // Selected when currentTab is 2 }; // Draw tab bar at Y position 15, returns height of the tab bar int tabBarHeight = ScreenComponents::drawTabBar(renderer, 15, tabs); // Position your content below the tab bar int contentStartY = 15 + tabBarHeight + 10; // Add some padding // Draw content based on selected tab if (currentTab == 0) { renderTabOneContent(contentStartY); } else if (currentTab == 1) { renderTabTwoContent(contentStartY); } else { renderTabThreeContent(contentStartY); } renderer.displayBuffer(); } ``` Video Demo: https://share.cleanshot.com/P6NBncFS --------- Co-authored-by: Dave Allie --- src/RecentBooksStore.cpp | 86 ++++ src/RecentBooksStore.h | 32 ++ src/ScreenComponents.cpp | 69 ++++ src/ScreenComponents.h | 15 + src/activities/home/HomeActivity.cpp | 14 +- src/activities/home/HomeActivity.h | 6 +- src/activities/home/MyLibraryActivity.cpp | 378 ++++++++++++++++++ src/activities/home/MyLibraryActivity.h | 67 ++++ src/activities/reader/EpubReaderActivity.cpp | 4 +- .../reader/FileSelectionActivity.cpp | 209 ---------- src/activities/reader/FileSelectionActivity.h | 40 -- src/activities/reader/ReaderActivity.cpp | 62 +-- src/activities/reader/ReaderActivity.h | 15 +- src/activities/reader/XtcReaderActivity.cpp | 4 +- src/main.cpp | 27 +- 15 files changed, 700 insertions(+), 328 deletions(-) create mode 100644 src/RecentBooksStore.cpp create mode 100644 src/RecentBooksStore.h create mode 100644 src/activities/home/MyLibraryActivity.cpp create mode 100644 src/activities/home/MyLibraryActivity.h delete mode 100644 src/activities/reader/FileSelectionActivity.cpp delete mode 100644 src/activities/reader/FileSelectionActivity.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/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 6f27e39c..eb11ba95 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) { @@ -500,9 +500,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"); } @@ -541,7 +541,7 @@ void HomeActivity::render() { renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected); } - const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down"); + const auto labels = mappedInput.mapLabels("", "Select", "Up", "Down"); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); const bool showBatteryPercentage = 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..9e6f3734 --- /dev/null +++ b/src/activities/home/MyLibraryActivity.cpp @@ -0,0 +1,378 @@ +#include "MyLibraryActivity.h" + +#include +#include + +#include + +#include "MappedInputManager.h" +#include "RecentBooksStore.h" +#include "ScreenComponents.h" +#include "fontIds.h" +#include "util/StringUtils.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[500]; + 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); + if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || + StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt")) { + files.emplace_back(filename); + } + } + file.close(); + } + root.close(); + sortFileList(files); +} + +size_t MyLibraryActivity::findEntry(const std::string& name) const { + for (size_t i = 0; i < files.size(); i++) { + if (files[i] == name) return i; + } + return 0; +} + +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], currentTab); + } + } 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], currentTab); + } + } + } + return; + } + + // Back button + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + if (mappedInput.getHeldTime() < GO_HOME_MS) { + if (currentTab == Tab::Files && basepath != "/") { + // Go up one directory, remembering the directory we came from + const std::string oldPath = basepath; + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); + if (basepath.empty()) basepath = "/"; + loadFiles(); + + // Select the directory we just came from + const auto pos = oldPath.find_last_of('/'); + const std::string dirName = oldPath.substr(pos + 1) + "/"; + selectorIndex = static_cast(findEntry(dirName)); + + 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 + const auto labels = mappedInput.mapLabels("« Back", "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..c6c52b68 --- /dev/null +++ b/src/activities/home/MyLibraryActivity.h @@ -0,0 +1,67 @@ +#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(); + size_t findEntry(const std::string& name) const; + + // 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, std::string initialPath = "/") + : Activity("MyLibrary", renderer, mappedInput), + currentTab(initialTab), + basepath(initialPath.empty() ? "/" : std::move(initialPath)), + 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 d70a15c4..0ee62816 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/FileSelectionActivity.cpp b/src/activities/reader/FileSelectionActivity.cpp deleted file mode 100644 index 3ef42c1c..00000000 --- a/src/activities/reader/FileSelectionActivity.cpp +++ /dev/null @@ -1,209 +0,0 @@ -#include "FileSelectionActivity.h" - -#include -#include - -#include "MappedInputManager.h" -#include "fontIds.h" -#include "util/StringUtils.h" - -namespace { -constexpr int PAGE_ITEMS = 23; -constexpr int SKIP_PAGE_MS = 700; -constexpr unsigned long GO_HOME_MS = 1000; -} // namespace - -void sortFileList(std::vector& strs) { - 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 FileSelectionActivity::taskTrampoline(void* param) { - auto* self = static_cast(param); - self->displayTaskLoop(); -} - -void FileSelectionActivity::loadFiles() { - files.clear(); - - auto root = SdMan.open(basepath.c_str()); - if (!root || !root.isDirectory()) { - if (root) root.close(); - return; - } - - root.rewindDirectory(); - - char name[500]; - 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); - if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || - StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt")) { - files.emplace_back(filename); - } - } - file.close(); - } - root.close(); - sortFileList(files); -} - -void FileSelectionActivity::onEnter() { - Activity::onEnter(); - - renderingMutex = xSemaphoreCreateMutex(); - - // basepath is set via constructor parameter (defaults to "/" if not specified) - loadFiles(); - selectorIndex = 0; - - // Trigger first update - updateRequired = true; - - xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask", - 2048, // Stack size - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle - ); -} - -void FileSelectionActivity::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; - files.clear(); -} - -void FileSelectionActivity::loop() { - // Long press BACK (1s+) goes to root folder - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { - if (basepath != "/") { - basepath = "/"; - loadFiles(); - updateRequired = true; - } - return; - } - - 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; - - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - if (files.empty()) { - return; - } - - if (basepath.back() != '/') basepath += "/"; - if (files[selectorIndex].back() == '/') { - basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); - loadFiles(); - selectorIndex = 0; - updateRequired = true; - } else { - onSelect(basepath + files[selectorIndex]); - } - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - // Short press: go up one directory, or go home if at root - if (mappedInput.getHeldTime() < GO_HOME_MS) { - if (basepath != "/") { - const std::string oldPath = basepath; - - basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) basepath = "/"; - loadFiles(); - - const auto pos = oldPath.find_last_of('/'); - const std::string dirName = oldPath.substr(pos + 1) + "/"; - selectorIndex = findEntry(dirName); - - updateRequired = true; - } else { - onGoHome(); - } - } - } else if (prevReleased) { - if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size(); - } else { - selectorIndex = (selectorIndex + files.size() - 1) % files.size(); - } - updateRequired = true; - } else if (nextReleased) { - if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); - } else { - selectorIndex = (selectorIndex + 1) % files.size(); - } - updateRequired = true; - } -} - -void FileSelectionActivity::displayTaskLoop() { - while (true) { - if (updateRequired) { - updateRequired = false; - xSemaphoreTake(renderingMutex, portMAX_DELAY); - render(); - xSemaphoreGive(renderingMutex); - } - vTaskDelay(10 / portTICK_PERIOD_MS); - } -} - -void FileSelectionActivity::render() const { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Books", true, EpdFontFamily::BOLD); - - // Help text - const auto labels = mappedInput.mapLabels("« Home", "Open", "", ""); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - if (files.empty()) { - renderer.drawText(UI_10_FONT_ID, 20, 60, "No books found"); - renderer.displayBuffer(); - return; - } - - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); - for (size_t i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); - } - - renderer.displayBuffer(); -} - -size_t FileSelectionActivity::findEntry(const std::string& name) const { - for (size_t i = 0; i < files.size(); i++) - if (files[i] == name) return i; - return 0; -} diff --git a/src/activities/reader/FileSelectionActivity.h b/src/activities/reader/FileSelectionActivity.h deleted file mode 100644 index 3c71968a..00000000 --- a/src/activities/reader/FileSelectionActivity.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once -#include -#include -#include - -#include -#include -#include - -#include "../Activity.h" - -class FileSelectionActivity final : public Activity { - TaskHandle_t displayTaskHandle = nullptr; - SemaphoreHandle_t renderingMutex = nullptr; - std::string basepath = "/"; - std::vector files; - size_t selectorIndex = 0; - bool updateRequired = false; - const std::function onSelect; - const std::function onGoHome; - - static void taskTrampoline(void* param); - [[noreturn]] void displayTaskLoop(); - void render() const; - void loadFiles(); - - size_t findEntry(const std::string& name) const; - - public: - explicit FileSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onSelect, - const std::function& onGoHome, std::string initialPath = "/") - : Activity("FileSelection", renderer, mappedInput), - basepath(initialPath.empty() ? "/" : std::move(initialPath)), - onSelect(onSelect), - onGoHome(onGoHome) {} - void onEnter() override; - void onExit() override; - void loop() override; -}; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index c00f6236..14d6623c 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -2,7 +2,6 @@ #include "Epub.h" #include "EpubReaderActivity.h" -#include "FileSelectionActivity.h" #include "Txt.h" #include "TxtReaderActivity.h" #include "Xtc.h" @@ -73,56 +72,10 @@ std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { return nullptr; } -void ReaderActivity::onSelectBookFile(const std::string& path) { - currentBookPath = path; // Track current book path - exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Loading...")); - - if (isXtcFile(path)) { - // Load XTC file - auto xtc = loadXtc(path); - if (xtc) { - onGoToXtcReader(std::move(xtc)); - } else { - exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC", - EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); - delay(2000); - onGoToFileSelection(); - } - } else if (isTxtFile(path)) { - // Load TXT file - auto txt = loadTxt(path); - if (txt) { - onGoToTxtReader(std::move(txt)); - } else { - exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load TXT", - EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); - delay(2000); - onGoToFileSelection(); - } - } else { - // Load EPUB file - auto epub = loadEpub(path); - if (epub) { - onGoToEpubReader(std::move(epub)); - } else { - exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub", - EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); - delay(2000); - onGoToFileSelection(); - } - } -} - -void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) { - exitActivity(); +void ReaderActivity::goToLibrary(const std::string& fromBookPath) { // If coming from a book, start in that book's folder; otherwise start from root const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); - enterNewActivity(new FileSelectionActivity( - renderer, mappedInput, [this](const std::string& path) { onSelectBookFile(path); }, onGoBack, initialPath)); + onGoToLibrary(initialPath, libraryTab); } void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { @@ -130,8 +83,7 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { currentBookPath = epubPath; exitActivity(); enterNewActivity(new EpubReaderActivity( - renderer, mappedInput, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); }, - [this] { onGoBack(); })); + renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); } void ReaderActivity::onGoToXtcReader(std::unique_ptr xtc) { @@ -139,8 +91,7 @@ void ReaderActivity::onGoToXtcReader(std::unique_ptr xtc) { currentBookPath = xtcPath; exitActivity(); enterNewActivity(new XtcReaderActivity( - renderer, mappedInput, std::move(xtc), [this, xtcPath] { onGoToFileSelection(xtcPath); }, - [this] { onGoBack(); })); + renderer, mappedInput, std::move(xtc), [this, xtcPath] { goToLibrary(xtcPath); }, [this] { onGoBack(); })); } void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { @@ -148,15 +99,14 @@ void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { currentBookPath = txtPath; exitActivity(); enterNewActivity(new TxtReaderActivity( - renderer, mappedInput, std::move(txt), [this, txtPath] { onGoToFileSelection(txtPath); }, - [this] { onGoBack(); })); + renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); })); } void ReaderActivity::onEnter() { ActivityWithSubactivity::onEnter(); if (initialBookPath.empty()) { - onGoToFileSelection(); // Start from root when entering via Browse + goToLibrary(); // Start from root when entering via Browse return; } diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index bec2a45b..ab74878f 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -2,6 +2,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "activities/home/MyLibraryActivity.h" class Epub; class Xtc; @@ -9,8 +10,10 @@ class Txt; class ReaderActivity final : public ActivityWithSubactivity { std::string initialBookPath; - std::string currentBookPath; // Track current book path for navigation + std::string currentBookPath; // Track current book path for navigation + MyLibraryActivity::Tab libraryTab; // Track which tab to return to const std::function onGoBack; + const std::function onGoToLibrary; static std::unique_ptr loadEpub(const std::string& path); static std::unique_ptr loadXtc(const std::string& path); static std::unique_ptr loadTxt(const std::string& path); @@ -18,17 +21,19 @@ class ReaderActivity final : public ActivityWithSubactivity { static bool isTxtFile(const std::string& path); static std::string extractFolderPath(const std::string& filePath); - void onSelectBookFile(const std::string& path); - void onGoToFileSelection(const std::string& fromBookPath = ""); + void goToLibrary(const std::string& fromBookPath = ""); void onGoToEpubReader(std::unique_ptr epub); void onGoToXtcReader(std::unique_ptr xtc); void onGoToTxtReader(std::unique_ptr txt); public: explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, - const std::function& onGoBack) + MyLibraryActivity::Tab libraryTab, const std::function& onGoBack, + const std::function& onGoToLibrary) : ActivityWithSubactivity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)), - onGoBack(onGoBack) {} + libraryTab(libraryTab), + onGoBack(onGoBack), + onGoToLibrary(onGoToLibrary) {} void onEnter() override; }; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index a211e61c..0a58d7b3 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 80b54064..3075757b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,10 +14,12 @@ #include "CrossPointState.h" #include "KOReaderCredentialStore.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" @@ -211,12 +213,13 @@ void enterDeepSleep(bool fromTimeout) { } void onGoHome(); -void onGoToReader(const std::string& initialEpubPath) { +void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab); +void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) { exitActivity(); - enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome)); + enterNewActivity( + new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab)); } -void onGoToReaderHome() { onGoToReader(std::string()); } -void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } +void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); } void onGoToFileTransfer() { exitActivity(); @@ -228,6 +231,16 @@ void onGoToSettings() { enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); } +void onGoToMyLibrary() { + exitActivity(); + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); +} + +void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) { + exitActivity(); + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, tab, path)); +} + void onGoToBrowser() { exitActivity(); enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); @@ -235,7 +248,7 @@ void onGoToBrowser() { void onGoHome() { exitActivity(); - enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, + enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, onGoToFileTransfer, onGoToBrowser)); } @@ -304,6 +317,8 @@ void setup() { enterNewActivity(new BootActivity(renderer, mappedInputManager)); APP_STATE.loadFromFile(); + RECENT_BOOKS.loadFromFile(); + if (APP_STATE.openEpubPath.empty()) { onGoHome(); } else { @@ -312,7 +327,7 @@ void setup() { APP_STATE.openEpubPath = ""; APP_STATE.lastSleepImage = 0; APP_STATE.saveToFile(); - onGoToReader(path); + onGoToReader(path, MyLibraryActivity::Tab::Recent); } // Ensure we're not still holding the power button before leaving setup