A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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<TabInfo> 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

<img width="250"
src="https://github.com/user-attachments/assets/07de4418-968e-4a88-9b42-ac5f53d8a832"
/>
<img width="250"
src="https://github.com/user-attachments/assets/e40201ed-dcc8-4568-b008-cd2bf13ebb2a"
/>
<img width="250"
src="https://github.com/user-attachments/assets/73db269f-e629-4696-b8ca-0b8443451a05"
/>

---------

Co-authored-by: Dave Allie <dave@daveallie.com>

authored by

Kenneth
Dave Allie
and committed by
GitHub
e548bfc0 73c30748

+700 -328
+86
src/RecentBooksStore.cpp
··· 1 + #include "RecentBooksStore.h" 2 + 3 + #include <HardwareSerial.h> 4 + #include <SDCardManager.h> 5 + #include <Serialization.h> 6 + 7 + #include <algorithm> 8 + 9 + namespace { 10 + constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 1; 11 + constexpr char RECENT_BOOKS_FILE[] = "/.crosspoint/recent.bin"; 12 + constexpr int MAX_RECENT_BOOKS = 10; 13 + } // namespace 14 + 15 + RecentBooksStore RecentBooksStore::instance; 16 + 17 + void RecentBooksStore::addBook(const std::string& path) { 18 + // Remove existing entry if present 19 + auto it = std::find(recentBooks.begin(), recentBooks.end(), path); 20 + if (it != recentBooks.end()) { 21 + recentBooks.erase(it); 22 + } 23 + 24 + // Add to front 25 + recentBooks.insert(recentBooks.begin(), path); 26 + 27 + // Trim to max size 28 + if (recentBooks.size() > MAX_RECENT_BOOKS) { 29 + recentBooks.resize(MAX_RECENT_BOOKS); 30 + } 31 + 32 + saveToFile(); 33 + } 34 + 35 + bool RecentBooksStore::saveToFile() const { 36 + // Make sure the directory exists 37 + SdMan.mkdir("/.crosspoint"); 38 + 39 + FsFile outputFile; 40 + if (!SdMan.openFileForWrite("RBS", RECENT_BOOKS_FILE, outputFile)) { 41 + return false; 42 + } 43 + 44 + serialization::writePod(outputFile, RECENT_BOOKS_FILE_VERSION); 45 + const uint8_t count = static_cast<uint8_t>(recentBooks.size()); 46 + serialization::writePod(outputFile, count); 47 + 48 + for (const auto& book : recentBooks) { 49 + serialization::writeString(outputFile, book); 50 + } 51 + 52 + outputFile.close(); 53 + Serial.printf("[%lu] [RBS] Recent books saved to file (%d entries)\n", millis(), count); 54 + return true; 55 + } 56 + 57 + bool RecentBooksStore::loadFromFile() { 58 + FsFile inputFile; 59 + if (!SdMan.openFileForRead("RBS", RECENT_BOOKS_FILE, inputFile)) { 60 + return false; 61 + } 62 + 63 + uint8_t version; 64 + serialization::readPod(inputFile, version); 65 + if (version != RECENT_BOOKS_FILE_VERSION) { 66 + Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version); 67 + inputFile.close(); 68 + return false; 69 + } 70 + 71 + uint8_t count; 72 + serialization::readPod(inputFile, count); 73 + 74 + recentBooks.clear(); 75 + recentBooks.reserve(count); 76 + 77 + for (uint8_t i = 0; i < count; i++) { 78 + std::string path; 79 + serialization::readString(inputFile, path); 80 + recentBooks.push_back(path); 81 + } 82 + 83 + inputFile.close(); 84 + Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), count); 85 + return true; 86 + }
+32
src/RecentBooksStore.h
··· 1 + #pragma once 2 + #include <string> 3 + #include <vector> 4 + 5 + class RecentBooksStore { 6 + // Static instance 7 + static RecentBooksStore instance; 8 + 9 + std::vector<std::string> recentBooks; 10 + 11 + public: 12 + ~RecentBooksStore() = default; 13 + 14 + // Get singleton instance 15 + static RecentBooksStore& getInstance() { return instance; } 16 + 17 + // Add a book path to the recent list (moves to front if already exists) 18 + void addBook(const std::string& path); 19 + 20 + // Get the list of recent book paths (most recent first) 21 + const std::vector<std::string>& getBooks() const { return recentBooks; } 22 + 23 + // Get the count of recent books 24 + int getCount() const { return static_cast<int>(recentBooks.size()); } 25 + 26 + bool saveToFile() const; 27 + 28 + bool loadFromFile(); 29 + }; 30 + 31 + // Helper macro to access recent books store 32 + #define RECENT_BOOKS RecentBooksStore::getInstance()
+69
src/ScreenComponents.cpp
··· 42 42 renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); 43 43 } 44 44 45 + int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) { 46 + constexpr int tabPadding = 20; // Horizontal padding between tabs 47 + constexpr int leftMargin = 20; // Left margin for first tab 48 + constexpr int underlineHeight = 2; // Height of selection underline 49 + constexpr int underlineGap = 4; // Gap between text and underline 50 + 51 + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); 52 + const int tabBarHeight = lineHeight + underlineGap + underlineHeight; 53 + 54 + int currentX = leftMargin; 55 + 56 + for (const auto& tab : tabs) { 57 + const int textWidth = 58 + renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); 59 + 60 + // Draw tab label 61 + renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true, 62 + tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); 63 + 64 + // Draw underline for selected tab 65 + if (tab.selected) { 66 + renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight); 67 + } 68 + 69 + currentX += textWidth + tabPadding; 70 + } 71 + 72 + return tabBarHeight; 73 + } 74 + 75 + void ScreenComponents::drawScrollIndicator(const GfxRenderer& renderer, const int currentPage, const int totalPages, 76 + const int contentTop, const int contentHeight) { 77 + if (totalPages <= 1) { 78 + return; // No need for indicator if only one page 79 + } 80 + 81 + const int screenWidth = renderer.getScreenWidth(); 82 + constexpr int indicatorWidth = 20; 83 + constexpr int arrowSize = 6; 84 + constexpr int margin = 15; // Offset from right edge 85 + 86 + const int centerX = screenWidth - indicatorWidth / 2 - margin; 87 + const int indicatorTop = contentTop + 60; // Offset to avoid overlapping side button hints 88 + const int indicatorBottom = contentTop + contentHeight - 30; 89 + 90 + // Draw up arrow at top (^) - narrow point at top, wide base at bottom 91 + for (int i = 0; i < arrowSize; ++i) { 92 + const int lineWidth = 1 + i * 2; 93 + const int startX = centerX - i; 94 + renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i); 95 + } 96 + 97 + // Draw down arrow at bottom (v) - wide base at top, narrow point at bottom 98 + for (int i = 0; i < arrowSize; ++i) { 99 + const int lineWidth = 1 + (arrowSize - 1 - i) * 2; 100 + const int startX = centerX - (arrowSize - 1 - i); 101 + renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1, 102 + indicatorBottom - arrowSize + 1 + i); 103 + } 104 + 105 + // Draw page fraction in the middle (e.g., "1/3") 106 + const std::string pageText = std::to_string(currentPage) + "/" + std::to_string(totalPages); 107 + const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, pageText.c_str()); 108 + const int textX = centerX - textWidth / 2; 109 + const int textY = (indicatorTop + indicatorBottom) / 2 - renderer.getLineHeight(SMALL_FONT_ID) / 2; 110 + 111 + renderer.drawText(SMALL_FONT_ID, textX, textY, pageText.c_str()); 112 + } 113 + 45 114 void ScreenComponents::drawProgressBar(const GfxRenderer& renderer, const int x, const int y, const int width, 46 115 const int height, const size_t current, const size_t total) { 47 116 if (total == 0) {
+15
src/ScreenComponents.h
··· 2 2 3 3 #include <cstddef> 4 4 #include <cstdint> 5 + #include <vector> 5 6 6 7 class GfxRenderer; 7 8 9 + struct TabInfo { 10 + const char* label; 11 + bool selected; 12 + }; 13 + 8 14 class ScreenComponents { 9 15 public: 10 16 static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); 17 + 18 + // Draw a horizontal tab bar with underline indicator for selected tab 19 + // Returns the height of the tab bar (for positioning content below) 20 + static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<TabInfo>& tabs); 21 + 22 + // Draw a scroll/page indicator on the right side of the screen 23 + // Shows up/down arrows and current page fraction (e.g., "1/3") 24 + static void drawScrollIndicator(const GfxRenderer& renderer, int currentPage, int totalPages, int contentTop, 25 + int contentHeight); 11 26 12 27 /** 13 28 * Draw a progress bar with percentage text.
+7 -7
src/activities/home/HomeActivity.cpp
··· 23 23 } 24 24 25 25 int HomeActivity::getMenuItemCount() const { 26 - int count = 3; // Browse files, File transfer, Settings 26 + int count = 3; // My Library, File transfer, Settings 27 27 if (hasContinueReading) count++; 28 28 if (hasOpdsUrl) count++; 29 29 return count; ··· 169 169 // Calculate dynamic indices based on which options are available 170 170 int idx = 0; 171 171 const int continueIdx = hasContinueReading ? idx++ : -1; 172 - const int browseFilesIdx = idx++; 172 + const int myLibraryIdx = idx++; 173 173 const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1; 174 174 const int fileTransferIdx = idx++; 175 175 const int settingsIdx = idx; 176 176 177 177 if (selectorIndex == continueIdx) { 178 178 onContinueReading(); 179 - } else if (selectorIndex == browseFilesIdx) { 180 - onReaderOpen(); 179 + } else if (selectorIndex == myLibraryIdx) { 180 + onMyLibraryOpen(); 181 181 } else if (selectorIndex == opdsLibraryIdx) { 182 182 onOpdsBrowserOpen(); 183 183 } else if (selectorIndex == fileTransferIdx) { ··· 500 500 501 501 // --- Bottom menu tiles --- 502 502 // Build menu items dynamically 503 - std::vector<const char*> menuItems = {"Browse Files", "File Transfer", "Settings"}; 503 + std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"}; 504 504 if (hasOpdsUrl) { 505 - // Insert Calibre Library after Browse Files 505 + // Insert Calibre Library after My Library 506 506 menuItems.insert(menuItems.begin() + 1, "Calibre Library"); 507 507 } 508 508 ··· 541 541 renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected); 542 542 } 543 543 544 - const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down"); 544 + const auto labels = mappedInput.mapLabels("", "Select", "Up", "Down"); 545 545 renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 546 546 547 547 const bool showBatteryPercentage =
+3 -3
src/activities/home/HomeActivity.h
··· 22 22 std::string lastBookAuthor; 23 23 std::string coverBmpPath; 24 24 const std::function<void()> onContinueReading; 25 - const std::function<void()> onReaderOpen; 25 + const std::function<void()> onMyLibraryOpen; 26 26 const std::function<void()> onSettingsOpen; 27 27 const std::function<void()> onFileTransferOpen; 28 28 const std::function<void()> onOpdsBrowserOpen; ··· 37 37 38 38 public: 39 39 explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 40 - const std::function<void()>& onContinueReading, const std::function<void()>& onReaderOpen, 40 + const std::function<void()>& onContinueReading, const std::function<void()>& onMyLibraryOpen, 41 41 const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen, 42 42 const std::function<void()>& onOpdsBrowserOpen) 43 43 : Activity("Home", renderer, mappedInput), 44 44 onContinueReading(onContinueReading), 45 - onReaderOpen(onReaderOpen), 45 + onMyLibraryOpen(onMyLibraryOpen), 46 46 onSettingsOpen(onSettingsOpen), 47 47 onFileTransferOpen(onFileTransferOpen), 48 48 onOpdsBrowserOpen(onOpdsBrowserOpen) {}
+378
src/activities/home/MyLibraryActivity.cpp
··· 1 + #include "MyLibraryActivity.h" 2 + 3 + #include <GfxRenderer.h> 4 + #include <SDCardManager.h> 5 + 6 + #include <algorithm> 7 + 8 + #include "MappedInputManager.h" 9 + #include "RecentBooksStore.h" 10 + #include "ScreenComponents.h" 11 + #include "fontIds.h" 12 + #include "util/StringUtils.h" 13 + 14 + namespace { 15 + // Layout constants 16 + constexpr int TAB_BAR_Y = 15; 17 + constexpr int CONTENT_START_Y = 60; 18 + constexpr int LINE_HEIGHT = 30; 19 + constexpr int LEFT_MARGIN = 20; 20 + constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator 21 + 22 + // Timing thresholds 23 + constexpr int SKIP_PAGE_MS = 700; 24 + constexpr unsigned long GO_HOME_MS = 1000; 25 + 26 + void sortFileList(std::vector<std::string>& strs) { 27 + std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { 28 + if (str1.back() == '/' && str2.back() != '/') return true; 29 + if (str1.back() != '/' && str2.back() == '/') return false; 30 + return lexicographical_compare( 31 + begin(str1), end(str1), begin(str2), end(str2), 32 + [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); 33 + }); 34 + } 35 + } // namespace 36 + 37 + int MyLibraryActivity::getPageItems() const { 38 + const int screenHeight = renderer.getScreenHeight(); 39 + const int bottomBarHeight = 60; // Space for button hints 40 + const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight; 41 + int items = availableHeight / LINE_HEIGHT; 42 + if (items < 1) { 43 + items = 1; 44 + } 45 + return items; 46 + } 47 + 48 + int MyLibraryActivity::getCurrentItemCount() const { 49 + if (currentTab == Tab::Recent) { 50 + return static_cast<int>(bookTitles.size()); 51 + } 52 + return static_cast<int>(files.size()); 53 + } 54 + 55 + int MyLibraryActivity::getTotalPages() const { 56 + const int itemCount = getCurrentItemCount(); 57 + const int pageItems = getPageItems(); 58 + if (itemCount == 0) return 1; 59 + return (itemCount + pageItems - 1) / pageItems; 60 + } 61 + 62 + int MyLibraryActivity::getCurrentPage() const { 63 + const int pageItems = getPageItems(); 64 + return selectorIndex / pageItems + 1; 65 + } 66 + 67 + void MyLibraryActivity::loadRecentBooks() { 68 + constexpr size_t MAX_RECENT_BOOKS = 20; 69 + 70 + bookTitles.clear(); 71 + bookPaths.clear(); 72 + const auto& books = RECENT_BOOKS.getBooks(); 73 + bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); 74 + bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); 75 + 76 + for (const auto& path : books) { 77 + // Limit to maximum number of recent books 78 + if (bookTitles.size() >= MAX_RECENT_BOOKS) { 79 + break; 80 + } 81 + 82 + // Skip if file no longer exists 83 + if (!SdMan.exists(path.c_str())) { 84 + continue; 85 + } 86 + 87 + // Extract filename from path for display 88 + std::string title = path; 89 + const size_t lastSlash = title.find_last_of('/'); 90 + if (lastSlash != std::string::npos) { 91 + title = title.substr(lastSlash + 1); 92 + } 93 + 94 + bookTitles.push_back(title); 95 + bookPaths.push_back(path); 96 + } 97 + } 98 + 99 + void MyLibraryActivity::loadFiles() { 100 + files.clear(); 101 + 102 + auto root = SdMan.open(basepath.c_str()); 103 + if (!root || !root.isDirectory()) { 104 + if (root) root.close(); 105 + return; 106 + } 107 + 108 + root.rewindDirectory(); 109 + 110 + char name[500]; 111 + for (auto file = root.openNextFile(); file; file = root.openNextFile()) { 112 + file.getName(name, sizeof(name)); 113 + if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) { 114 + file.close(); 115 + continue; 116 + } 117 + 118 + if (file.isDirectory()) { 119 + files.emplace_back(std::string(name) + "/"); 120 + } else { 121 + auto filename = std::string(name); 122 + if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || 123 + StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt")) { 124 + files.emplace_back(filename); 125 + } 126 + } 127 + file.close(); 128 + } 129 + root.close(); 130 + sortFileList(files); 131 + } 132 + 133 + size_t MyLibraryActivity::findEntry(const std::string& name) const { 134 + for (size_t i = 0; i < files.size(); i++) { 135 + if (files[i] == name) return i; 136 + } 137 + return 0; 138 + } 139 + 140 + void MyLibraryActivity::taskTrampoline(void* param) { 141 + auto* self = static_cast<MyLibraryActivity*>(param); 142 + self->displayTaskLoop(); 143 + } 144 + 145 + void MyLibraryActivity::onEnter() { 146 + Activity::onEnter(); 147 + 148 + renderingMutex = xSemaphoreCreateMutex(); 149 + 150 + // Load data for both tabs 151 + loadRecentBooks(); 152 + loadFiles(); 153 + 154 + selectorIndex = 0; 155 + updateRequired = true; 156 + 157 + xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", 158 + 4096, // Stack size (increased for epub metadata loading) 159 + this, // Parameters 160 + 1, // Priority 161 + &displayTaskHandle // Task handle 162 + ); 163 + } 164 + 165 + void MyLibraryActivity::onExit() { 166 + Activity::onExit(); 167 + 168 + // Wait until not rendering to delete task to avoid killing mid-instruction to 169 + // EPD 170 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 171 + if (displayTaskHandle) { 172 + vTaskDelete(displayTaskHandle); 173 + displayTaskHandle = nullptr; 174 + } 175 + vSemaphoreDelete(renderingMutex); 176 + renderingMutex = nullptr; 177 + 178 + bookTitles.clear(); 179 + bookPaths.clear(); 180 + files.clear(); 181 + } 182 + 183 + void MyLibraryActivity::loop() { 184 + const int itemCount = getCurrentItemCount(); 185 + const int pageItems = getPageItems(); 186 + 187 + // Long press BACK (1s+) in Files tab goes to root folder 188 + if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) && 189 + mappedInput.getHeldTime() >= GO_HOME_MS) { 190 + if (basepath != "/") { 191 + basepath = "/"; 192 + loadFiles(); 193 + selectorIndex = 0; 194 + updateRequired = true; 195 + } 196 + return; 197 + } 198 + 199 + const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); 200 + const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); 201 + const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); 202 + const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); 203 + 204 + const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 205 + 206 + // Confirm button - open selected item 207 + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 208 + if (currentTab == Tab::Recent) { 209 + if (!bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size())) { 210 + onSelectBook(bookPaths[selectorIndex], currentTab); 211 + } 212 + } else { 213 + // Files tab 214 + if (!files.empty() && selectorIndex < static_cast<int>(files.size())) { 215 + if (basepath.back() != '/') basepath += "/"; 216 + if (files[selectorIndex].back() == '/') { 217 + // Enter directory 218 + basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); 219 + loadFiles(); 220 + selectorIndex = 0; 221 + updateRequired = true; 222 + } else { 223 + // Open file 224 + onSelectBook(basepath + files[selectorIndex], currentTab); 225 + } 226 + } 227 + } 228 + return; 229 + } 230 + 231 + // Back button 232 + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 233 + if (mappedInput.getHeldTime() < GO_HOME_MS) { 234 + if (currentTab == Tab::Files && basepath != "/") { 235 + // Go up one directory, remembering the directory we came from 236 + const std::string oldPath = basepath; 237 + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); 238 + if (basepath.empty()) basepath = "/"; 239 + loadFiles(); 240 + 241 + // Select the directory we just came from 242 + const auto pos = oldPath.find_last_of('/'); 243 + const std::string dirName = oldPath.substr(pos + 1) + "/"; 244 + selectorIndex = static_cast<int>(findEntry(dirName)); 245 + 246 + updateRequired = true; 247 + } else { 248 + // Go home 249 + onGoHome(); 250 + } 251 + } 252 + return; 253 + } 254 + 255 + // Tab switching: Left/Right always control tabs 256 + if (leftReleased && currentTab == Tab::Files) { 257 + currentTab = Tab::Recent; 258 + selectorIndex = 0; 259 + updateRequired = true; 260 + return; 261 + } 262 + if (rightReleased && currentTab == Tab::Recent) { 263 + currentTab = Tab::Files; 264 + selectorIndex = 0; 265 + updateRequired = true; 266 + return; 267 + } 268 + 269 + // Navigation: Up/Down moves through items only 270 + const bool prevReleased = upReleased; 271 + const bool nextReleased = downReleased; 272 + 273 + if (prevReleased && itemCount > 0) { 274 + if (skipPage) { 275 + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; 276 + } else { 277 + selectorIndex = (selectorIndex + itemCount - 1) % itemCount; 278 + } 279 + updateRequired = true; 280 + } else if (nextReleased && itemCount > 0) { 281 + if (skipPage) { 282 + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % itemCount; 283 + } else { 284 + selectorIndex = (selectorIndex + 1) % itemCount; 285 + } 286 + updateRequired = true; 287 + } 288 + } 289 + 290 + void MyLibraryActivity::displayTaskLoop() { 291 + while (true) { 292 + if (updateRequired) { 293 + updateRequired = false; 294 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 295 + render(); 296 + xSemaphoreGive(renderingMutex); 297 + } 298 + vTaskDelay(10 / portTICK_PERIOD_MS); 299 + } 300 + } 301 + 302 + void MyLibraryActivity::render() const { 303 + renderer.clearScreen(); 304 + 305 + // Draw tab bar 306 + std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; 307 + ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); 308 + 309 + // Draw content based on current tab 310 + if (currentTab == Tab::Recent) { 311 + renderRecentTab(); 312 + } else { 313 + renderFilesTab(); 314 + } 315 + 316 + // Draw scroll indicator 317 + const int screenHeight = renderer.getScreenHeight(); 318 + const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar 319 + ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight); 320 + 321 + // Draw side button hints (up/down navigation on right side) 322 + // Note: text is rotated 90° CW, so ">" appears as "^" and "<" appears as "v" 323 + renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<"); 324 + 325 + // Draw bottom button hints 326 + const auto labels = mappedInput.mapLabels("« Back", "Open", "<", ">"); 327 + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 328 + 329 + renderer.displayBuffer(); 330 + } 331 + 332 + void MyLibraryActivity::renderRecentTab() const { 333 + const auto pageWidth = renderer.getScreenWidth(); 334 + const int pageItems = getPageItems(); 335 + const int bookCount = static_cast<int>(bookTitles.size()); 336 + 337 + if (bookCount == 0) { 338 + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); 339 + return; 340 + } 341 + 342 + const auto pageStartIndex = selectorIndex / pageItems * pageItems; 343 + 344 + // Draw selection highlight 345 + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, 346 + LINE_HEIGHT); 347 + 348 + // Draw items 349 + for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { 350 + auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); 351 + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), 352 + i != selectorIndex); 353 + } 354 + } 355 + 356 + void MyLibraryActivity::renderFilesTab() const { 357 + const auto pageWidth = renderer.getScreenWidth(); 358 + const int pageItems = getPageItems(); 359 + const int fileCount = static_cast<int>(files.size()); 360 + 361 + if (fileCount == 0) { 362 + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); 363 + return; 364 + } 365 + 366 + const auto pageStartIndex = selectorIndex / pageItems * pageItems; 367 + 368 + // Draw selection highlight 369 + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, 370 + LINE_HEIGHT); 371 + 372 + // Draw items 373 + for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) { 374 + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); 375 + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), 376 + i != selectorIndex); 377 + } 378 + }
+67
src/activities/home/MyLibraryActivity.h
··· 1 + #pragma once 2 + #include <freertos/FreeRTOS.h> 3 + #include <freertos/semphr.h> 4 + #include <freertos/task.h> 5 + 6 + #include <functional> 7 + #include <string> 8 + #include <vector> 9 + 10 + #include "../Activity.h" 11 + 12 + class MyLibraryActivity final : public Activity { 13 + public: 14 + enum class Tab { Recent, Files }; 15 + 16 + private: 17 + TaskHandle_t displayTaskHandle = nullptr; 18 + SemaphoreHandle_t renderingMutex = nullptr; 19 + 20 + Tab currentTab = Tab::Recent; 21 + int selectorIndex = 0; 22 + bool updateRequired = false; 23 + 24 + // Recent tab state 25 + std::vector<std::string> bookTitles; // Display titles for each book 26 + std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing) 27 + 28 + // Files tab state (from FileSelectionActivity) 29 + std::string basepath = "/"; 30 + std::vector<std::string> files; 31 + 32 + // Callbacks 33 + const std::function<void()> onGoHome; 34 + const std::function<void(const std::string& path, Tab fromTab)> onSelectBook; 35 + 36 + // Number of items that fit on a page 37 + int getPageItems() const; 38 + int getCurrentItemCount() const; 39 + int getTotalPages() const; 40 + int getCurrentPage() const; 41 + 42 + // Data loading 43 + void loadRecentBooks(); 44 + void loadFiles(); 45 + size_t findEntry(const std::string& name) const; 46 + 47 + // Rendering 48 + static void taskTrampoline(void* param); 49 + [[noreturn]] void displayTaskLoop(); 50 + void render() const; 51 + void renderRecentTab() const; 52 + void renderFilesTab() const; 53 + 54 + public: 55 + explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 56 + const std::function<void()>& onGoHome, 57 + const std::function<void(const std::string& path, Tab fromTab)>& onSelectBook, 58 + Tab initialTab = Tab::Recent, std::string initialPath = "/") 59 + : Activity("MyLibrary", renderer, mappedInput), 60 + currentTab(initialTab), 61 + basepath(initialPath.empty() ? "/" : std::move(initialPath)), 62 + onGoHome(onGoHome), 63 + onSelectBook(onSelectBook) {} 64 + void onEnter() override; 65 + void onExit() override; 66 + void loop() override; 67 + };
+3 -1
src/activities/reader/EpubReaderActivity.cpp
··· 9 9 #include "CrossPointState.h" 10 10 #include "EpubReaderChapterSelectionActivity.h" 11 11 #include "MappedInputManager.h" 12 + #include "RecentBooksStore.h" 12 13 #include "ScreenComponents.h" 13 14 #include "fontIds.h" 14 15 ··· 74 75 } 75 76 } 76 77 77 - // Save current epub as last opened epub 78 + // Save current epub as last opened epub and add to recent books 78 79 APP_STATE.openEpubPath = epub->getPath(); 79 80 APP_STATE.saveToFile(); 81 + RECENT_BOOKS.addBook(epub->getPath()); 80 82 81 83 // Trigger first update 82 84 updateRequired = true;
-209
src/activities/reader/FileSelectionActivity.cpp
··· 1 - #include "FileSelectionActivity.h" 2 - 3 - #include <GfxRenderer.h> 4 - #include <SDCardManager.h> 5 - 6 - #include "MappedInputManager.h" 7 - #include "fontIds.h" 8 - #include "util/StringUtils.h" 9 - 10 - namespace { 11 - constexpr int PAGE_ITEMS = 23; 12 - constexpr int SKIP_PAGE_MS = 700; 13 - constexpr unsigned long GO_HOME_MS = 1000; 14 - } // namespace 15 - 16 - void sortFileList(std::vector<std::string>& strs) { 17 - std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { 18 - if (str1.back() == '/' && str2.back() != '/') return true; 19 - if (str1.back() != '/' && str2.back() == '/') return false; 20 - return lexicographical_compare( 21 - begin(str1), end(str1), begin(str2), end(str2), 22 - [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); 23 - }); 24 - } 25 - 26 - void FileSelectionActivity::taskTrampoline(void* param) { 27 - auto* self = static_cast<FileSelectionActivity*>(param); 28 - self->displayTaskLoop(); 29 - } 30 - 31 - void FileSelectionActivity::loadFiles() { 32 - files.clear(); 33 - 34 - auto root = SdMan.open(basepath.c_str()); 35 - if (!root || !root.isDirectory()) { 36 - if (root) root.close(); 37 - return; 38 - } 39 - 40 - root.rewindDirectory(); 41 - 42 - char name[500]; 43 - for (auto file = root.openNextFile(); file; file = root.openNextFile()) { 44 - file.getName(name, sizeof(name)); 45 - if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) { 46 - file.close(); 47 - continue; 48 - } 49 - 50 - if (file.isDirectory()) { 51 - files.emplace_back(std::string(name) + "/"); 52 - } else { 53 - auto filename = std::string(name); 54 - if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || 55 - StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt")) { 56 - files.emplace_back(filename); 57 - } 58 - } 59 - file.close(); 60 - } 61 - root.close(); 62 - sortFileList(files); 63 - } 64 - 65 - void FileSelectionActivity::onEnter() { 66 - Activity::onEnter(); 67 - 68 - renderingMutex = xSemaphoreCreateMutex(); 69 - 70 - // basepath is set via constructor parameter (defaults to "/" if not specified) 71 - loadFiles(); 72 - selectorIndex = 0; 73 - 74 - // Trigger first update 75 - updateRequired = true; 76 - 77 - xTaskCreate(&FileSelectionActivity::taskTrampoline, "FileSelectionActivityTask", 78 - 2048, // Stack size 79 - this, // Parameters 80 - 1, // Priority 81 - &displayTaskHandle // Task handle 82 - ); 83 - } 84 - 85 - void FileSelectionActivity::onExit() { 86 - Activity::onExit(); 87 - 88 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 89 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 90 - if (displayTaskHandle) { 91 - vTaskDelete(displayTaskHandle); 92 - displayTaskHandle = nullptr; 93 - } 94 - vSemaphoreDelete(renderingMutex); 95 - renderingMutex = nullptr; 96 - files.clear(); 97 - } 98 - 99 - void FileSelectionActivity::loop() { 100 - // Long press BACK (1s+) goes to root folder 101 - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { 102 - if (basepath != "/") { 103 - basepath = "/"; 104 - loadFiles(); 105 - updateRequired = true; 106 - } 107 - return; 108 - } 109 - 110 - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || 111 - mappedInput.wasReleased(MappedInputManager::Button::Left); 112 - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || 113 - mappedInput.wasReleased(MappedInputManager::Button::Right); 114 - 115 - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 116 - 117 - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 118 - if (files.empty()) { 119 - return; 120 - } 121 - 122 - if (basepath.back() != '/') basepath += "/"; 123 - if (files[selectorIndex].back() == '/') { 124 - basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); 125 - loadFiles(); 126 - selectorIndex = 0; 127 - updateRequired = true; 128 - } else { 129 - onSelect(basepath + files[selectorIndex]); 130 - } 131 - } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 132 - // Short press: go up one directory, or go home if at root 133 - if (mappedInput.getHeldTime() < GO_HOME_MS) { 134 - if (basepath != "/") { 135 - const std::string oldPath = basepath; 136 - 137 - basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); 138 - if (basepath.empty()) basepath = "/"; 139 - loadFiles(); 140 - 141 - const auto pos = oldPath.find_last_of('/'); 142 - const std::string dirName = oldPath.substr(pos + 1) + "/"; 143 - selectorIndex = findEntry(dirName); 144 - 145 - updateRequired = true; 146 - } else { 147 - onGoHome(); 148 - } 149 - } 150 - } else if (prevReleased) { 151 - if (skipPage) { 152 - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + files.size()) % files.size(); 153 - } else { 154 - selectorIndex = (selectorIndex + files.size() - 1) % files.size(); 155 - } 156 - updateRequired = true; 157 - } else if (nextReleased) { 158 - if (skipPage) { 159 - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); 160 - } else { 161 - selectorIndex = (selectorIndex + 1) % files.size(); 162 - } 163 - updateRequired = true; 164 - } 165 - } 166 - 167 - void FileSelectionActivity::displayTaskLoop() { 168 - while (true) { 169 - if (updateRequired) { 170 - updateRequired = false; 171 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 172 - render(); 173 - xSemaphoreGive(renderingMutex); 174 - } 175 - vTaskDelay(10 / portTICK_PERIOD_MS); 176 - } 177 - } 178 - 179 - void FileSelectionActivity::render() const { 180 - renderer.clearScreen(); 181 - 182 - const auto pageWidth = renderer.getScreenWidth(); 183 - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Books", true, EpdFontFamily::BOLD); 184 - 185 - // Help text 186 - const auto labels = mappedInput.mapLabels("« Home", "Open", "", ""); 187 - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 188 - 189 - if (files.empty()) { 190 - renderer.drawText(UI_10_FONT_ID, 20, 60, "No books found"); 191 - renderer.displayBuffer(); 192 - return; 193 - } 194 - 195 - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; 196 - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); 197 - for (size_t i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) { 198 - auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40); 199 - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); 200 - } 201 - 202 - renderer.displayBuffer(); 203 - } 204 - 205 - size_t FileSelectionActivity::findEntry(const std::string& name) const { 206 - for (size_t i = 0; i < files.size(); i++) 207 - if (files[i] == name) return i; 208 - return 0; 209 - }
-40
src/activities/reader/FileSelectionActivity.h
··· 1 - #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 - 6 - #include <functional> 7 - #include <string> 8 - #include <vector> 9 - 10 - #include "../Activity.h" 11 - 12 - class FileSelectionActivity final : public Activity { 13 - TaskHandle_t displayTaskHandle = nullptr; 14 - SemaphoreHandle_t renderingMutex = nullptr; 15 - std::string basepath = "/"; 16 - std::vector<std::string> files; 17 - size_t selectorIndex = 0; 18 - bool updateRequired = false; 19 - const std::function<void(const std::string&)> onSelect; 20 - const std::function<void()> onGoHome; 21 - 22 - static void taskTrampoline(void* param); 23 - [[noreturn]] void displayTaskLoop(); 24 - void render() const; 25 - void loadFiles(); 26 - 27 - size_t findEntry(const std::string& name) const; 28 - 29 - public: 30 - explicit FileSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 31 - const std::function<void(const std::string&)>& onSelect, 32 - const std::function<void()>& onGoHome, std::string initialPath = "/") 33 - : Activity("FileSelection", renderer, mappedInput), 34 - basepath(initialPath.empty() ? "/" : std::move(initialPath)), 35 - onSelect(onSelect), 36 - onGoHome(onGoHome) {} 37 - void onEnter() override; 38 - void onExit() override; 39 - void loop() override; 40 - };
+6 -56
src/activities/reader/ReaderActivity.cpp
··· 2 2 3 3 #include "Epub.h" 4 4 #include "EpubReaderActivity.h" 5 - #include "FileSelectionActivity.h" 6 5 #include "Txt.h" 7 6 #include "TxtReaderActivity.h" 8 7 #include "Xtc.h" ··· 73 72 return nullptr; 74 73 } 75 74 76 - void ReaderActivity::onSelectBookFile(const std::string& path) { 77 - currentBookPath = path; // Track current book path 78 - exitActivity(); 79 - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Loading...")); 80 - 81 - if (isXtcFile(path)) { 82 - // Load XTC file 83 - auto xtc = loadXtc(path); 84 - if (xtc) { 85 - onGoToXtcReader(std::move(xtc)); 86 - } else { 87 - exitActivity(); 88 - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC", 89 - EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); 90 - delay(2000); 91 - onGoToFileSelection(); 92 - } 93 - } else if (isTxtFile(path)) { 94 - // Load TXT file 95 - auto txt = loadTxt(path); 96 - if (txt) { 97 - onGoToTxtReader(std::move(txt)); 98 - } else { 99 - exitActivity(); 100 - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load TXT", 101 - EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); 102 - delay(2000); 103 - onGoToFileSelection(); 104 - } 105 - } else { 106 - // Load EPUB file 107 - auto epub = loadEpub(path); 108 - if (epub) { 109 - onGoToEpubReader(std::move(epub)); 110 - } else { 111 - exitActivity(); 112 - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub", 113 - EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH)); 114 - delay(2000); 115 - onGoToFileSelection(); 116 - } 117 - } 118 - } 119 - 120 - void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) { 121 - exitActivity(); 75 + void ReaderActivity::goToLibrary(const std::string& fromBookPath) { 122 76 // If coming from a book, start in that book's folder; otherwise start from root 123 77 const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); 124 - enterNewActivity(new FileSelectionActivity( 125 - renderer, mappedInput, [this](const std::string& path) { onSelectBookFile(path); }, onGoBack, initialPath)); 78 + onGoToLibrary(initialPath, libraryTab); 126 79 } 127 80 128 81 void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) { ··· 130 83 currentBookPath = epubPath; 131 84 exitActivity(); 132 85 enterNewActivity(new EpubReaderActivity( 133 - renderer, mappedInput, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); }, 134 - [this] { onGoBack(); })); 86 + renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); 135 87 } 136 88 137 89 void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) { ··· 139 91 currentBookPath = xtcPath; 140 92 exitActivity(); 141 93 enterNewActivity(new XtcReaderActivity( 142 - renderer, mappedInput, std::move(xtc), [this, xtcPath] { onGoToFileSelection(xtcPath); }, 143 - [this] { onGoBack(); })); 94 + renderer, mappedInput, std::move(xtc), [this, xtcPath] { goToLibrary(xtcPath); }, [this] { onGoBack(); })); 144 95 } 145 96 146 97 void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) { ··· 148 99 currentBookPath = txtPath; 149 100 exitActivity(); 150 101 enterNewActivity(new TxtReaderActivity( 151 - renderer, mappedInput, std::move(txt), [this, txtPath] { onGoToFileSelection(txtPath); }, 152 - [this] { onGoBack(); })); 102 + renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); })); 153 103 } 154 104 155 105 void ReaderActivity::onEnter() { 156 106 ActivityWithSubactivity::onEnter(); 157 107 158 108 if (initialBookPath.empty()) { 159 - onGoToFileSelection(); // Start from root when entering via Browse 109 + goToLibrary(); // Start from root when entering via Browse 160 110 return; 161 111 } 162 112
+10 -5
src/activities/reader/ReaderActivity.h
··· 2 2 #include <memory> 3 3 4 4 #include "../ActivityWithSubactivity.h" 5 + #include "activities/home/MyLibraryActivity.h" 5 6 6 7 class Epub; 7 8 class Xtc; ··· 9 10 10 11 class ReaderActivity final : public ActivityWithSubactivity { 11 12 std::string initialBookPath; 12 - std::string currentBookPath; // Track current book path for navigation 13 + std::string currentBookPath; // Track current book path for navigation 14 + MyLibraryActivity::Tab libraryTab; // Track which tab to return to 13 15 const std::function<void()> onGoBack; 16 + const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary; 14 17 static std::unique_ptr<Epub> loadEpub(const std::string& path); 15 18 static std::unique_ptr<Xtc> loadXtc(const std::string& path); 16 19 static std::unique_ptr<Txt> loadTxt(const std::string& path); ··· 18 21 static bool isTxtFile(const std::string& path); 19 22 20 23 static std::string extractFolderPath(const std::string& filePath); 21 - void onSelectBookFile(const std::string& path); 22 - void onGoToFileSelection(const std::string& fromBookPath = ""); 24 + void goToLibrary(const std::string& fromBookPath = ""); 23 25 void onGoToEpubReader(std::unique_ptr<Epub> epub); 24 26 void onGoToXtcReader(std::unique_ptr<Xtc> xtc); 25 27 void onGoToTxtReader(std::unique_ptr<Txt> txt); 26 28 27 29 public: 28 30 explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, 29 - const std::function<void()>& onGoBack) 31 + MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack, 32 + const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary) 30 33 : ActivityWithSubactivity("Reader", renderer, mappedInput), 31 34 initialBookPath(std::move(initialBookPath)), 32 - onGoBack(onGoBack) {} 35 + libraryTab(libraryTab), 36 + onGoBack(onGoBack), 37 + onGoToLibrary(onGoToLibrary) {} 33 38 void onEnter() override; 34 39 };
+3 -1
src/activities/reader/XtcReaderActivity.cpp
··· 14 14 #include "CrossPointSettings.h" 15 15 #include "CrossPointState.h" 16 16 #include "MappedInputManager.h" 17 + #include "RecentBooksStore.h" 17 18 #include "XtcReaderChapterSelectionActivity.h" 18 19 #include "fontIds.h" 19 20 ··· 41 42 // Load saved progress 42 43 loadProgress(); 43 44 44 - // Save current XTC as last opened book 45 + // Save current XTC as last opened book and add to recent books 45 46 APP_STATE.openEpubPath = xtc->getPath(); 46 47 APP_STATE.saveToFile(); 48 + RECENT_BOOKS.addBook(xtc->getPath()); 47 49 48 50 // Trigger first update 49 51 updateRequired = true;
+21 -6
src/main.cpp
··· 14 14 #include "CrossPointState.h" 15 15 #include "KOReaderCredentialStore.h" 16 16 #include "MappedInputManager.h" 17 + #include "RecentBooksStore.h" 17 18 #include "activities/boot_sleep/BootActivity.h" 18 19 #include "activities/boot_sleep/SleepActivity.h" 19 20 #include "activities/browser/OpdsBookBrowserActivity.h" 20 21 #include "activities/home/HomeActivity.h" 22 + #include "activities/home/MyLibraryActivity.h" 21 23 #include "activities/network/CrossPointWebServerActivity.h" 22 24 #include "activities/reader/ReaderActivity.h" 23 25 #include "activities/settings/SettingsActivity.h" ··· 211 213 } 212 214 213 215 void onGoHome(); 214 - void onGoToReader(const std::string& initialEpubPath) { 216 + void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab); 217 + void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) { 215 218 exitActivity(); 216 - enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome)); 219 + enterNewActivity( 220 + new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab)); 217 221 } 218 - void onGoToReaderHome() { onGoToReader(std::string()); } 219 - void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); } 222 + void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); } 220 223 221 224 void onGoToFileTransfer() { 222 225 exitActivity(); ··· 228 231 enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); 229 232 } 230 233 234 + void onGoToMyLibrary() { 235 + exitActivity(); 236 + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); 237 + } 238 + 239 + void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) { 240 + exitActivity(); 241 + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, tab, path)); 242 + } 243 + 231 244 void onGoToBrowser() { 232 245 exitActivity(); 233 246 enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); ··· 235 248 236 249 void onGoHome() { 237 250 exitActivity(); 238 - enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, 251 + enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, 239 252 onGoToFileTransfer, onGoToBrowser)); 240 253 } 241 254 ··· 304 317 enterNewActivity(new BootActivity(renderer, mappedInputManager)); 305 318 306 319 APP_STATE.loadFromFile(); 320 + RECENT_BOOKS.loadFromFile(); 321 + 307 322 if (APP_STATE.openEpubPath.empty()) { 308 323 onGoHome(); 309 324 } else { ··· 312 327 APP_STATE.openEpubPath = ""; 313 328 APP_STATE.lastSleepImage = 0; 314 329 APP_STATE.saveToFile(); 315 - onGoToReader(path); 330 + onGoToReader(path, MyLibraryActivity::Tab::Recent); 316 331 } 317 332 318 333 // Ensure we're not still holding the power button before leaving setup