mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
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:
parent
73c30748d8
commit
e548bfc0e1
86
src/RecentBooksStore.cpp
Normal file
86
src/RecentBooksStore.cpp
Normal 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
32
src/RecentBooksStore.h
Normal 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()
|
||||
@ -42,6 +42,75 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left,
|
||||
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
|
||||
}
|
||||
|
||||
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<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,
|
||||
const int height, const size_t current, const size_t total) {
|
||||
if (total == 0) {
|
||||
|
||||
@ -2,13 +2,28 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
struct TabInfo {
|
||||
const char* label;
|
||||
bool selected;
|
||||
};
|
||||
|
||||
class ScreenComponents {
|
||||
public:
|
||||
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
|
||||
|
||||
// Draw a horizontal tab bar with underline indicator for selected tab
|
||||
// Returns the height of the tab bar (for positioning content below)
|
||||
static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector<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.
|
||||
* @param renderer The graphics renderer
|
||||
|
||||
@ -23,7 +23,7 @@ void HomeActivity::taskTrampoline(void* param) {
|
||||
}
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
int count = 3; // Browse files, File transfer, Settings
|
||||
int count = 3; // My Library, File transfer, Settings
|
||||
if (hasContinueReading) count++;
|
||||
if (hasOpdsUrl) count++;
|
||||
return count;
|
||||
@ -169,15 +169,15 @@ void HomeActivity::loop() {
|
||||
// Calculate dynamic indices based on which options are available
|
||||
int idx = 0;
|
||||
const int continueIdx = hasContinueReading ? idx++ : -1;
|
||||
const int browseFilesIdx = idx++;
|
||||
const int myLibraryIdx = idx++;
|
||||
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
|
||||
const int fileTransferIdx = idx++;
|
||||
const int settingsIdx = idx;
|
||||
|
||||
if (selectorIndex == continueIdx) {
|
||||
onContinueReading();
|
||||
} else if (selectorIndex == browseFilesIdx) {
|
||||
onReaderOpen();
|
||||
} else if (selectorIndex == myLibraryIdx) {
|
||||
onMyLibraryOpen();
|
||||
} else if (selectorIndex == opdsLibraryIdx) {
|
||||
onOpdsBrowserOpen();
|
||||
} else if (selectorIndex == fileTransferIdx) {
|
||||
@ -500,9 +500,9 @@ void HomeActivity::render() {
|
||||
|
||||
// --- Bottom menu tiles ---
|
||||
// Build menu items dynamically
|
||||
std::vector<const char*> menuItems = {"Browse Files", "File Transfer", "Settings"};
|
||||
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"};
|
||||
if (hasOpdsUrl) {
|
||||
// Insert Calibre Library after Browse Files
|
||||
// Insert Calibre Library after My Library
|
||||
menuItems.insert(menuItems.begin() + 1, "Calibre Library");
|
||||
}
|
||||
|
||||
@ -541,7 +541,7 @@ void HomeActivity::render() {
|
||||
renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected);
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down");
|
||||
const auto labels = mappedInput.mapLabels("", "Select", "Up", "Down");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
const bool showBatteryPercentage =
|
||||
|
||||
@ -22,7 +22,7 @@ class HomeActivity final : public Activity {
|
||||
std::string lastBookAuthor;
|
||||
std::string coverBmpPath;
|
||||
const std::function<void()> onContinueReading;
|
||||
const std::function<void()> onReaderOpen;
|
||||
const std::function<void()> onMyLibraryOpen;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
const std::function<void()> onFileTransferOpen;
|
||||
const std::function<void()> onOpdsBrowserOpen;
|
||||
@ -37,12 +37,12 @@ class HomeActivity final : public Activity {
|
||||
|
||||
public:
|
||||
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()>& onOpdsBrowserOpen)
|
||||
: Activity("Home", renderer, mappedInput),
|
||||
onContinueReading(onContinueReading),
|
||||
onReaderOpen(onReaderOpen),
|
||||
onMyLibraryOpen(onMyLibraryOpen),
|
||||
onSettingsOpen(onSettingsOpen),
|
||||
onFileTransferOpen(onFileTransferOpen),
|
||||
onOpdsBrowserOpen(onOpdsBrowserOpen) {}
|
||||
|
||||
378
src/activities/home/MyLibraryActivity.cpp
Normal file
378
src/activities/home/MyLibraryActivity.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
67
src/activities/home/MyLibraryActivity.h
Normal file
67
src/activities/home/MyLibraryActivity.h
Normal 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;
|
||||
};
|
||||
@ -9,6 +9,7 @@
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "ScreenComponents.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@ -74,9 +75,10 @@ void EpubReaderActivity::onEnter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Save current epub as last opened epub
|
||||
// Save current epub as last opened epub and add to recent books
|
||||
APP_STATE.openEpubPath = epub->getPath();
|
||||
APP_STATE.saveToFile();
|
||||
RECENT_BOOKS.addBook(epub->getPath());
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
};
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
#include "Epub.h"
|
||||
#include "EpubReaderActivity.h"
|
||||
#include "FileSelectionActivity.h"
|
||||
#include "Txt.h"
|
||||
#include "TxtReaderActivity.h"
|
||||
#include "Xtc.h"
|
||||
@ -73,56 +72,10 @@ std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ReaderActivity::onSelectBookFile(const std::string& path) {
|
||||
currentBookPath = path; // Track current book path
|
||||
exitActivity();
|
||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Loading..."));
|
||||
|
||||
if (isXtcFile(path)) {
|
||||
// Load XTC file
|
||||
auto xtc = loadXtc(path);
|
||||
if (xtc) {
|
||||
onGoToXtcReader(std::move(xtc));
|
||||
} else {
|
||||
exitActivity();
|
||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load XTC",
|
||||
EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH));
|
||||
delay(2000);
|
||||
onGoToFileSelection();
|
||||
}
|
||||
} else if (isTxtFile(path)) {
|
||||
// Load TXT file
|
||||
auto txt = loadTxt(path);
|
||||
if (txt) {
|
||||
onGoToTxtReader(std::move(txt));
|
||||
} else {
|
||||
exitActivity();
|
||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load TXT",
|
||||
EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH));
|
||||
delay(2000);
|
||||
onGoToFileSelection();
|
||||
}
|
||||
} else {
|
||||
// Load EPUB file
|
||||
auto epub = loadEpub(path);
|
||||
if (epub) {
|
||||
onGoToEpubReader(std::move(epub));
|
||||
} else {
|
||||
exitActivity();
|
||||
enterNewActivity(new FullScreenMessageActivity(renderer, mappedInput, "Failed to load epub",
|
||||
EpdFontFamily::REGULAR, EInkDisplay::HALF_REFRESH));
|
||||
delay(2000);
|
||||
onGoToFileSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) {
|
||||
exitActivity();
|
||||
void ReaderActivity::goToLibrary(const std::string& fromBookPath) {
|
||||
// If coming from a book, start in that book's folder; otherwise start from root
|
||||
const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath);
|
||||
enterNewActivity(new FileSelectionActivity(
|
||||
renderer, mappedInput, [this](const std::string& path) { onSelectBookFile(path); }, onGoBack, initialPath));
|
||||
onGoToLibrary(initialPath, libraryTab);
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
||||
@ -130,8 +83,7 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
||||
currentBookPath = epubPath;
|
||||
exitActivity();
|
||||
enterNewActivity(new EpubReaderActivity(
|
||||
renderer, mappedInput, std::move(epub), [this, epubPath] { onGoToFileSelection(epubPath); },
|
||||
[this] { onGoBack(); }));
|
||||
renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); }));
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
|
||||
@ -139,8 +91,7 @@ void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
|
||||
currentBookPath = xtcPath;
|
||||
exitActivity();
|
||||
enterNewActivity(new XtcReaderActivity(
|
||||
renderer, mappedInput, std::move(xtc), [this, xtcPath] { onGoToFileSelection(xtcPath); },
|
||||
[this] { onGoBack(); }));
|
||||
renderer, mappedInput, std::move(xtc), [this, xtcPath] { goToLibrary(xtcPath); }, [this] { onGoBack(); }));
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
||||
@ -148,15 +99,14 @@ void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
||||
currentBookPath = txtPath;
|
||||
exitActivity();
|
||||
enterNewActivity(new TxtReaderActivity(
|
||||
renderer, mappedInput, std::move(txt), [this, txtPath] { onGoToFileSelection(txtPath); },
|
||||
[this] { onGoBack(); }));
|
||||
renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); }));
|
||||
}
|
||||
|
||||
void ReaderActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
if (initialBookPath.empty()) {
|
||||
onGoToFileSelection(); // Start from root when entering via Browse
|
||||
goToLibrary(); // Start from root when entering via Browse
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "../ActivityWithSubactivity.h"
|
||||
#include "activities/home/MyLibraryActivity.h"
|
||||
|
||||
class Epub;
|
||||
class Xtc;
|
||||
@ -10,7 +11,9 @@ class Txt;
|
||||
class ReaderActivity final : public ActivityWithSubactivity {
|
||||
std::string initialBookPath;
|
||||
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(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
|
||||
static std::unique_ptr<Epub> loadEpub(const std::string& path);
|
||||
static std::unique_ptr<Xtc> loadXtc(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 std::string extractFolderPath(const std::string& filePath);
|
||||
void onSelectBookFile(const std::string& path);
|
||||
void onGoToFileSelection(const std::string& fromBookPath = "");
|
||||
void goToLibrary(const std::string& fromBookPath = "");
|
||||
void onGoToEpubReader(std::unique_ptr<Epub> epub);
|
||||
void onGoToXtcReader(std::unique_ptr<Xtc> xtc);
|
||||
void onGoToTxtReader(std::unique_ptr<Txt> txt);
|
||||
|
||||
public:
|
||||
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),
|
||||
initialBookPath(std::move(initialBookPath)),
|
||||
onGoBack(onGoBack) {}
|
||||
libraryTab(libraryTab),
|
||||
onGoBack(onGoBack),
|
||||
onGoToLibrary(onGoToLibrary) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "XtcReaderChapterSelectionActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@ -41,9 +42,10 @@ void XtcReaderActivity::onEnter() {
|
||||
// Load saved progress
|
||||
loadProgress();
|
||||
|
||||
// Save current XTC as last opened book
|
||||
// Save current XTC as last opened book and add to recent books
|
||||
APP_STATE.openEpubPath = xtc->getPath();
|
||||
APP_STATE.saveToFile();
|
||||
RECENT_BOOKS.addBook(xtc->getPath());
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
27
src/main.cpp
27
src/main.cpp
@ -14,10 +14,12 @@
|
||||
#include "CrossPointState.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "activities/boot_sleep/BootActivity.h"
|
||||
#include "activities/boot_sleep/SleepActivity.h"
|
||||
#include "activities/browser/OpdsBookBrowserActivity.h"
|
||||
#include "activities/home/HomeActivity.h"
|
||||
#include "activities/home/MyLibraryActivity.h"
|
||||
#include "activities/network/CrossPointWebServerActivity.h"
|
||||
#include "activities/reader/ReaderActivity.h"
|
||||
#include "activities/settings/SettingsActivity.h"
|
||||
@ -211,12 +213,13 @@ void enterDeepSleep() {
|
||||
}
|
||||
|
||||
void onGoHome();
|
||||
void onGoToReader(const std::string& initialEpubPath) {
|
||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab);
|
||||
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
|
||||
exitActivity();
|
||||
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome));
|
||||
enterNewActivity(
|
||||
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab));
|
||||
}
|
||||
void onGoToReaderHome() { onGoToReader(std::string()); }
|
||||
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
|
||||
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
|
||||
|
||||
void onGoToFileTransfer() {
|
||||
exitActivity();
|
||||
@ -228,6 +231,16 @@ void onGoToSettings() {
|
||||
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
||||
}
|
||||
|
||||
void onGoToMyLibrary() {
|
||||
exitActivity();
|
||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
||||
}
|
||||
|
||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
||||
exitActivity();
|
||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, tab, path));
|
||||
}
|
||||
|
||||
void onGoToBrowser() {
|
||||
exitActivity();
|
||||
enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome));
|
||||
@ -235,7 +248,7 @@ void onGoToBrowser() {
|
||||
|
||||
void onGoHome() {
|
||||
exitActivity();
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings,
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings,
|
||||
onGoToFileTransfer, onGoToBrowser));
|
||||
}
|
||||
|
||||
@ -304,6 +317,8 @@ void setup() {
|
||||
enterNewActivity(new BootActivity(renderer, mappedInputManager));
|
||||
|
||||
APP_STATE.loadFromFile();
|
||||
RECENT_BOOKS.loadFromFile();
|
||||
|
||||
if (APP_STATE.openEpubPath.empty()) {
|
||||
onGoHome();
|
||||
} else {
|
||||
@ -312,7 +327,7 @@ void setup() {
|
||||
APP_STATE.openEpubPath = "";
|
||||
APP_STATE.lastSleepImage = 0;
|
||||
APP_STATE.saveToFile();
|
||||
onGoToReader(path);
|
||||
onGoToReader(path, MyLibraryActivity::Tab::Recent);
|
||||
}
|
||||
|
||||
// Ensure we're not still holding the power button before leaving setup
|
||||
|
||||
Loading…
Reference in New Issue
Block a user