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>
This commit is contained in:
Kenneth 2026-01-21 05:38:38 -06:00 committed by Irene Ying
parent 9de96e9812
commit d2b6506d23
15 changed files with 700 additions and 328 deletions

86
src/RecentBooksStore.cpp Normal file
View File

@ -0,0 +1,86 @@
#include "RecentBooksStore.h"
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <Serialization.h>
#include <algorithm>
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<uint8_t>(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;
}

32
src/RecentBooksStore.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include <string>
#include <vector>
class RecentBooksStore {
// Static instance
static RecentBooksStore instance;
std::vector<std::string> 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<std::string>& getBooks() const { return recentBooks; }
// Get the count of recent books
int getCount() const { return static_cast<int>(recentBooks.size()); }
bool saveToFile() const;
bool loadFromFile();
};
// Helper macro to access recent books store
#define RECENT_BOOKS RecentBooksStore::getInstance()

View File

@ -42,6 +42,75 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left,
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
} }
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& 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, 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) { const int height, const size_t current, const size_t total) {
if (total == 0) { if (total == 0) {

View File

@ -2,13 +2,28 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <vector>
class GfxRenderer; class GfxRenderer;
struct TabInfo {
const char* label;
bool selected;
};
class ScreenComponents { class ScreenComponents {
public: public:
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); 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<TabInfo>& 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. * Draw a progress bar with percentage text.
* @param renderer The graphics renderer * @param renderer The graphics renderer

View File

@ -23,7 +23,7 @@ void HomeActivity::taskTrampoline(void* param) {
} }
int HomeActivity::getMenuItemCount() const { int HomeActivity::getMenuItemCount() const {
int count = 3; // Browse files, File transfer, Settings int count = 3; // My Library, File transfer, Settings
if (hasContinueReading) count++; if (hasContinueReading) count++;
if (hasOpdsUrl) count++; if (hasOpdsUrl) count++;
return count; return count;
@ -169,15 +169,15 @@ void HomeActivity::loop() {
// Calculate dynamic indices based on which options are available // Calculate dynamic indices based on which options are available
int idx = 0; int idx = 0;
const int continueIdx = hasContinueReading ? idx++ : -1; const int continueIdx = hasContinueReading ? idx++ : -1;
const int browseFilesIdx = idx++; const int myLibraryIdx = idx++;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1; const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int fileTransferIdx = idx++; const int fileTransferIdx = idx++;
const int settingsIdx = idx; const int settingsIdx = idx;
if (selectorIndex == continueIdx) { if (selectorIndex == continueIdx) {
onContinueReading(); onContinueReading();
} else if (selectorIndex == browseFilesIdx) { } else if (selectorIndex == myLibraryIdx) {
onReaderOpen(); onMyLibraryOpen();
} else if (selectorIndex == opdsLibraryIdx) { } else if (selectorIndex == opdsLibraryIdx) {
onOpdsBrowserOpen(); onOpdsBrowserOpen();
} else if (selectorIndex == fileTransferIdx) { } else if (selectorIndex == fileTransferIdx) {
@ -500,9 +500,9 @@ void HomeActivity::render() {
// --- Bottom menu tiles --- // --- Bottom menu tiles ---
// Build menu items dynamically // Build menu items dynamically
std::vector<const char*> menuItems = {"Browse Files", "File Transfer", "Settings"}; std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"};
if (hasOpdsUrl) { if (hasOpdsUrl) {
// Insert Calibre Library after Browse Files // Insert Calibre Library after My Library
menuItems.insert(menuItems.begin() + 1, "Calibre 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); 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); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
const bool showBatteryPercentage = const bool showBatteryPercentage =

View File

@ -22,7 +22,7 @@ class HomeActivity final : public Activity {
std::string lastBookAuthor; std::string lastBookAuthor;
std::string coverBmpPath; std::string coverBmpPath;
const std::function<void()> onContinueReading; const std::function<void()> onContinueReading;
const std::function<void()> onReaderOpen; const std::function<void()> onMyLibraryOpen;
const std::function<void()> onSettingsOpen; const std::function<void()> onSettingsOpen;
const std::function<void()> onFileTransferOpen; const std::function<void()> onFileTransferOpen;
const std::function<void()> onOpdsBrowserOpen; const std::function<void()> onOpdsBrowserOpen;
@ -37,12 +37,12 @@ class HomeActivity final : public Activity {
public: public:
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onContinueReading, const std::function<void()>& onReaderOpen, const std::function<void()>& onContinueReading, const std::function<void()>& onMyLibraryOpen,
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen, const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
const std::function<void()>& onOpdsBrowserOpen) const std::function<void()>& onOpdsBrowserOpen)
: Activity("Home", renderer, mappedInput), : Activity("Home", renderer, mappedInput),
onContinueReading(onContinueReading), onContinueReading(onContinueReading),
onReaderOpen(onReaderOpen), onMyLibraryOpen(onMyLibraryOpen),
onSettingsOpen(onSettingsOpen), onSettingsOpen(onSettingsOpen),
onFileTransferOpen(onFileTransferOpen), onFileTransferOpen(onFileTransferOpen),
onOpdsBrowserOpen(onOpdsBrowserOpen) {} onOpdsBrowserOpen(onOpdsBrowserOpen) {}

View File

@ -0,0 +1,378 @@
#include "MyLibraryActivity.h"
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <algorithm>
#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<std::string>& 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<int>(bookTitles.size());
}
return static_cast<int>(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<MyLibraryActivity*>(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<int>(bookPaths.size())) {
onSelectBook(bookPaths[selectorIndex], currentTab);
}
} else {
// Files tab
if (!files.empty() && selectorIndex < static_cast<int>(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<int>(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<TabInfo> 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<int>(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<int>(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);
}
}

View File

@ -0,0 +1,67 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#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<std::string> bookTitles; // Display titles for each book
std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing)
// Files tab state (from FileSelectionActivity)
std::string basepath = "/";
std::vector<std::string> files;
// Callbacks
const std::function<void()> onGoHome;
const std::function<void(const std::string& path, Tab fromTab)> 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<void()>& onGoHome,
const std::function<void(const std::string& path, Tab fromTab)>& 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;
};

View File

@ -9,6 +9,7 @@
#include "CrossPointState.h" #include "CrossPointState.h"
#include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderChapterSelectionActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "ScreenComponents.h" #include "ScreenComponents.h"
#include "fontIds.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.openEpubPath = epub->getPath();
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(epub->getPath());
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;

View File

@ -1,209 +0,0 @@
#include "FileSelectionActivity.h"
#include <GfxRenderer.h>
#include <SDCardManager.h>
#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<std::string>& 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<FileSelectionActivity*>(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;
}

View File

@ -1,40 +0,0 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#include "../Activity.h"
class FileSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::string basepath = "/";
std::vector<std::string> files;
size_t selectorIndex = 0;
bool updateRequired = false;
const std::function<void(const std::string&)> onSelect;
const std::function<void()> 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<void(const std::string&)>& onSelect,
const std::function<void()>& 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;
};

View File

@ -2,7 +2,6 @@
#include "Epub.h" #include "Epub.h"
#include "EpubReaderActivity.h" #include "EpubReaderActivity.h"
#include "FileSelectionActivity.h"
#include "Txt.h" #include "Txt.h"
#include "TxtReaderActivity.h" #include "TxtReaderActivity.h"
#include "Xtc.h" #include "Xtc.h"
@ -73,56 +72,10 @@ std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
return nullptr; return nullptr;
} }
void ReaderActivity::onSelectBookFile(const std::string& path) { void ReaderActivity::goToLibrary(const std::string& fromBookPath) {
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();
// If coming from a book, start in that book's folder; otherwise start from root // If coming from a book, start in that book's folder; otherwise start from root
const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath);
enterNewActivity(new FileSelectionActivity( onGoToLibrary(initialPath, libraryTab);
renderer, mappedInput, [this](const std::string& path) { onSelectBookFile(path); }, onGoBack, initialPath));
} }
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) { void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
@ -130,8 +83,7 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
currentBookPath = epubPath; currentBookPath = epubPath;
exitActivity(); exitActivity();
enterNewActivity(new EpubReaderActivity( enterNewActivity(new EpubReaderActivity(
renderer, mappedInput, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); }, renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); }));
[this] { onGoBack(); }));
} }
void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) { void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
@ -139,8 +91,7 @@ void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
currentBookPath = xtcPath; currentBookPath = xtcPath;
exitActivity(); exitActivity();
enterNewActivity(new XtcReaderActivity( enterNewActivity(new XtcReaderActivity(
renderer, mappedInput, std::move(xtc), [this, xtcPath] { onGoToFileSelection(xtcPath); }, renderer, mappedInput, std::move(xtc), [this, xtcPath] { goToLibrary(xtcPath); }, [this] { onGoBack(); }));
[this] { onGoBack(); }));
} }
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) { void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
@ -148,15 +99,14 @@ void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
currentBookPath = txtPath; currentBookPath = txtPath;
exitActivity(); exitActivity();
enterNewActivity(new TxtReaderActivity( enterNewActivity(new TxtReaderActivity(
renderer, mappedInput, std::move(txt), [this, txtPath] { onGoToFileSelection(txtPath); }, renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); }));
[this] { onGoBack(); }));
} }
void ReaderActivity::onEnter() { void ReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter(); ActivityWithSubactivity::onEnter();
if (initialBookPath.empty()) { if (initialBookPath.empty()) {
onGoToFileSelection(); // Start from root when entering via Browse goToLibrary(); // Start from root when entering via Browse
return; return;
} }

View File

@ -2,6 +2,7 @@
#include <memory> #include <memory>
#include "../ActivityWithSubactivity.h" #include "../ActivityWithSubactivity.h"
#include "activities/home/MyLibraryActivity.h"
class Epub; class Epub;
class Xtc; class Xtc;
@ -9,8 +10,10 @@ class Txt;
class ReaderActivity final : public ActivityWithSubactivity { class ReaderActivity final : public ActivityWithSubactivity {
std::string initialBookPath; 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<void()> onGoBack; const std::function<void()> onGoBack;
const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
static std::unique_ptr<Epub> loadEpub(const std::string& path); static std::unique_ptr<Epub> loadEpub(const std::string& path);
static std::unique_ptr<Xtc> loadXtc(const std::string& path); static std::unique_ptr<Xtc> loadXtc(const std::string& path);
static std::unique_ptr<Txt> loadTxt(const std::string& path); static std::unique_ptr<Txt> loadTxt(const std::string& path);
@ -18,17 +21,19 @@ class ReaderActivity final : public ActivityWithSubactivity {
static bool isTxtFile(const std::string& path); static bool isTxtFile(const std::string& path);
static std::string extractFolderPath(const std::string& filePath); static std::string extractFolderPath(const std::string& filePath);
void onSelectBookFile(const std::string& path); void goToLibrary(const std::string& fromBookPath = "");
void onGoToFileSelection(const std::string& fromBookPath = "");
void onGoToEpubReader(std::unique_ptr<Epub> epub); void onGoToEpubReader(std::unique_ptr<Epub> epub);
void onGoToXtcReader(std::unique_ptr<Xtc> xtc); void onGoToXtcReader(std::unique_ptr<Xtc> xtc);
void onGoToTxtReader(std::unique_ptr<Txt> txt); void onGoToTxtReader(std::unique_ptr<Txt> txt);
public: public:
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
const std::function<void()>& onGoBack) MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack,
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary)
: ActivityWithSubactivity("Reader", renderer, mappedInput), : ActivityWithSubactivity("Reader", renderer, mappedInput),
initialBookPath(std::move(initialBookPath)), initialBookPath(std::move(initialBookPath)),
onGoBack(onGoBack) {} libraryTab(libraryTab),
onGoBack(onGoBack),
onGoToLibrary(onGoToLibrary) {}
void onEnter() override; void onEnter() override;
}; };

View File

@ -14,6 +14,7 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "XtcReaderChapterSelectionActivity.h" #include "XtcReaderChapterSelectionActivity.h"
#include "fontIds.h" #include "fontIds.h"
@ -41,9 +42,10 @@ void XtcReaderActivity::onEnter() {
// Load saved progress // Load saved progress
loadProgress(); 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.openEpubPath = xtc->getPath();
APP_STATE.saveToFile(); APP_STATE.saveToFile();
RECENT_BOOKS.addBook(xtc->getPath());
// Trigger first update // Trigger first update
updateRequired = true; updateRequired = true;

View File

@ -14,10 +14,12 @@
#include "CrossPointState.h" #include "CrossPointState.h"
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "RecentBooksStore.h"
#include "activities/boot_sleep/BootActivity.h" #include "activities/boot_sleep/BootActivity.h"
#include "activities/boot_sleep/SleepActivity.h" #include "activities/boot_sleep/SleepActivity.h"
#include "activities/browser/OpdsBookBrowserActivity.h" #include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/home/HomeActivity.h" #include "activities/home/HomeActivity.h"
#include "activities/home/MyLibraryActivity.h"
#include "activities/network/CrossPointWebServerActivity.h" #include "activities/network/CrossPointWebServerActivity.h"
#include "activities/reader/ReaderActivity.h" #include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h" #include "activities/settings/SettingsActivity.h"
@ -211,12 +213,13 @@ void enterDeepSleep(bool fromTimeout) {
} }
void onGoHome(); 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(); 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, MyLibraryActivity::Tab::Recent); }
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
void onGoToFileTransfer() { void onGoToFileTransfer() {
exitActivity(); exitActivity();
@ -228,6 +231,16 @@ void onGoToSettings() {
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); 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() { void onGoToBrowser() {
exitActivity(); exitActivity();
enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome));
@ -235,7 +248,7 @@ void onGoToBrowser() {
void onGoHome() { void onGoHome() {
exitActivity(); exitActivity();
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings, enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings,
onGoToFileTransfer, onGoToBrowser)); onGoToFileTransfer, onGoToBrowser));
} }
@ -304,6 +317,8 @@ void setup() {
enterNewActivity(new BootActivity(renderer, mappedInputManager)); enterNewActivity(new BootActivity(renderer, mappedInputManager));
APP_STATE.loadFromFile(); APP_STATE.loadFromFile();
RECENT_BOOKS.loadFromFile();
if (APP_STATE.openEpubPath.empty()) { if (APP_STATE.openEpubPath.empty()) {
onGoHome(); onGoHome();
} else { } else {
@ -312,7 +327,7 @@ void setup() {
APP_STATE.openEpubPath = ""; APP_STATE.openEpubPath = "";
APP_STATE.lastSleepImage = 0; APP_STATE.lastSleepImage = 0;
APP_STATE.saveToFile(); APP_STATE.saveToFile();
onGoToReader(path); onGoToReader(path, MyLibraryActivity::Tab::Recent);
} }
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup