mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 08:07:40 +03:00
Introduce Tab bar
This commit is contained in:
parent
6401f099f4
commit
e2d2c4a9b0
@ -39,3 +39,72 @@ 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 = 5;
|
||||||
|
|
||||||
|
const int centerX = screenWidth - indicatorWidth / 2 - margin;
|
||||||
|
const int indicatorTop = contentTop + 10;
|
||||||
|
const int indicatorBottom = contentTop + contentHeight - 30;
|
||||||
|
|
||||||
|
// Draw up arrow (triangle pointing up)
|
||||||
|
for (int i = 0; i < arrowSize; ++i) {
|
||||||
|
const int lineWidth = 1 + i * 2;
|
||||||
|
const int startX = centerX - i;
|
||||||
|
renderer.drawLine(startX, indicatorTop + arrowSize - 1 - i, startX + lineWidth - 1, indicatorTop + arrowSize - 1 - i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw down arrow (triangle pointing down)
|
||||||
|
for (int i = 0; i < arrowSize; ++i) {
|
||||||
|
const int lineWidth = 1 + i * 2;
|
||||||
|
const int startX = centerX - 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());
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#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);
|
static void drawBattery(const GfxRenderer& renderer, int left, int top);
|
||||||
|
|
||||||
|
// 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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
|
||||||
#include "ScreenComponents.h"
|
#include "ScreenComponents.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@ -16,9 +15,8 @@ void HomeActivity::taskTrampoline(void* param) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int HomeActivity::getMenuItemCount() const {
|
int HomeActivity::getMenuItemCount() const {
|
||||||
int count = 3; // Base: Browse, File transfer, Settings
|
int count = 3; // Base: My Library, File transfer, Settings
|
||||||
if (hasContinueReading) count++;
|
if (hasContinueReading) count++;
|
||||||
if (hasRecentBooks) count++;
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,9 +28,6 @@ void HomeActivity::onEnter() {
|
|||||||
// Check if we have a book to continue reading
|
// Check if we have a book to continue reading
|
||||||
hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str());
|
hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str());
|
||||||
|
|
||||||
// Check if we have more than one recent book
|
|
||||||
hasRecentBooks = RECENT_BOOKS.getCount() > 1;
|
|
||||||
|
|
||||||
if (hasContinueReading) {
|
if (hasContinueReading) {
|
||||||
// Extract filename from path for display
|
// Extract filename from path for display
|
||||||
lastBookTitle = APP_STATE.openEpubPath;
|
lastBookTitle = APP_STATE.openEpubPath;
|
||||||
@ -95,34 +90,21 @@ void HomeActivity::loop() {
|
|||||||
const int menuCount = getMenuItemCount();
|
const int menuCount = getMenuItemCount();
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
if (hasContinueReading && hasRecentBooks) {
|
if (hasContinueReading) {
|
||||||
// Menu: Continue Reading, Recent Books, Browse, File transfer, Settings
|
// Menu: Continue Reading, My Library, File transfer, Settings
|
||||||
if (selectorIndex == 0) {
|
if (selectorIndex == 0) {
|
||||||
onContinueReading();
|
onContinueReading();
|
||||||
} else if (selectorIndex == 1) {
|
} else if (selectorIndex == 1) {
|
||||||
onRecentBooksOpen();
|
onMyLibraryOpen();
|
||||||
} else if (selectorIndex == 2) {
|
|
||||||
onReaderOpen();
|
|
||||||
} else if (selectorIndex == 3) {
|
|
||||||
onFileTransferOpen();
|
|
||||||
} else if (selectorIndex == 4) {
|
|
||||||
onSettingsOpen();
|
|
||||||
}
|
|
||||||
} else if (hasContinueReading) {
|
|
||||||
// Menu: Continue Reading, Browse, File transfer, Settings
|
|
||||||
if (selectorIndex == 0) {
|
|
||||||
onContinueReading();
|
|
||||||
} else if (selectorIndex == 1) {
|
|
||||||
onReaderOpen();
|
|
||||||
} else if (selectorIndex == 2) {
|
} else if (selectorIndex == 2) {
|
||||||
onFileTransferOpen();
|
onFileTransferOpen();
|
||||||
} else if (selectorIndex == 3) {
|
} else if (selectorIndex == 3) {
|
||||||
onSettingsOpen();
|
onSettingsOpen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Menu: Browse, File transfer, Settings
|
// Menu: My Library, File transfer, Settings
|
||||||
if (selectorIndex == 0) {
|
if (selectorIndex == 0) {
|
||||||
onReaderOpen();
|
onMyLibraryOpen();
|
||||||
} else if (selectorIndex == 1) {
|
} else if (selectorIndex == 1) {
|
||||||
onFileTransferOpen();
|
onFileTransferOpen();
|
||||||
} else if (selectorIndex == 2) {
|
} else if (selectorIndex == 2) {
|
||||||
@ -300,8 +282,8 @@ void HomeActivity::render() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Bottom menu tiles ---
|
// --- Bottom menu tiles ---
|
||||||
// Menu tiles: [Recent books], Browse files, File transfer, Settings
|
// Menu tiles: My Library, File transfer, Settings
|
||||||
const int menuTileCount = hasRecentBooks ? 4 : 3;
|
constexpr int menuTileCount = 3;
|
||||||
const int menuTileWidth = pageWidth - 2 * margin;
|
const int menuTileWidth = pageWidth - 2 * margin;
|
||||||
constexpr int menuTileHeight = 50;
|
constexpr int menuTileHeight = 50;
|
||||||
constexpr int menuSpacing = 10;
|
constexpr int menuSpacing = 10;
|
||||||
@ -314,6 +296,8 @@ void HomeActivity::render() const {
|
|||||||
menuStartY = maxMenuStartY;
|
menuStartY = maxMenuStartY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr const char* menuItems[3] = {"My Library", "File transfer", "Settings"};
|
||||||
|
|
||||||
for (int i = 0; i < menuTileCount; ++i) {
|
for (int i = 0; i < menuTileCount; ++i) {
|
||||||
const int overallIndex = i + (getMenuItemCount() - menuTileCount);
|
const int overallIndex = i + (getMenuItemCount() - menuTileCount);
|
||||||
constexpr int tileX = margin;
|
constexpr int tileX = margin;
|
||||||
@ -326,16 +310,7 @@ void HomeActivity::render() const {
|
|||||||
renderer.drawRect(tileX, tileY, menuTileWidth, menuTileHeight);
|
renderer.drawRect(tileX, tileY, menuTileWidth, menuTileHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine label based on tile index and whether recent books is shown
|
const char* label = menuItems[i];
|
||||||
const char* label;
|
|
||||||
if (hasRecentBooks) {
|
|
||||||
constexpr const char* itemsWithRecent[4] = {"Recent books", "Browse files", "File transfer", "Settings"};
|
|
||||||
label = itemsWithRecent[i];
|
|
||||||
} else {
|
|
||||||
constexpr const char* itemsNoRecent[3] = {"Browse files", "File transfer", "Settings"};
|
|
||||||
label = itemsNoRecent[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
|
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
|
||||||
const int textX = tileX + (menuTileWidth - textWidth) / 2;
|
const int textX = tileX + (menuTileWidth - textWidth) / 2;
|
||||||
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
|
|||||||
@ -13,14 +13,12 @@ class HomeActivity final : public Activity {
|
|||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
bool hasContinueReading = false;
|
bool hasContinueReading = false;
|
||||||
bool hasRecentBooks = false;
|
|
||||||
std::string lastBookTitle;
|
std::string lastBookTitle;
|
||||||
std::string lastBookAuthor;
|
std::string lastBookAuthor;
|
||||||
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()> onRecentBooksOpen;
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
@ -29,15 +27,13 @@ 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()>& onRecentBooksOpen)
|
|
||||||
: Activity("Home", renderer, mappedInput),
|
: Activity("Home", renderer, mappedInput),
|
||||||
onContinueReading(onContinueReading),
|
onContinueReading(onContinueReading),
|
||||||
onReaderOpen(onReaderOpen),
|
onMyLibraryOpen(onMyLibraryOpen),
|
||||||
onSettingsOpen(onSettingsOpen),
|
onSettingsOpen(onSettingsOpen),
|
||||||
onFileTransferOpen(onFileTransferOpen),
|
onFileTransferOpen(onFileTransferOpen) {}
|
||||||
onRecentBooksOpen(onRecentBooksOpen) {}
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
371
src/activities/home/MyLibraryActivity.cpp
Normal file
371
src/activities/home/MyLibraryActivity.cpp
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
#include "MyLibraryActivity.h"
|
||||||
|
|
||||||
|
#include <Epub.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "RecentBooksStore.h"
|
||||||
|
#include "ScreenComponents.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Layout constants
|
||||||
|
constexpr int TAB_BAR_Y = 15;
|
||||||
|
constexpr int CONTENT_START_Y = 60;
|
||||||
|
constexpr int LINE_HEIGHT = 30;
|
||||||
|
constexpr int LEFT_MARGIN = 20;
|
||||||
|
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
|
||||||
|
|
||||||
|
// Timing thresholds
|
||||||
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
|
|
||||||
|
void sortFileList(std::vector<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() {
|
||||||
|
bookTitles.clear();
|
||||||
|
bookPaths.clear();
|
||||||
|
const auto& books = RECENT_BOOKS.getBooks();
|
||||||
|
bookTitles.reserve(books.size());
|
||||||
|
bookPaths.reserve(books.size());
|
||||||
|
|
||||||
|
for (const auto& path : books) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string ext5 = title.length() >= 5 ? title.substr(title.length() - 5) : "";
|
||||||
|
const std::string ext4 = title.length() >= 4 ? title.substr(title.length() - 4) : "";
|
||||||
|
|
||||||
|
// If epub, try to load the metadata for title
|
||||||
|
if (ext5 == ".epub") {
|
||||||
|
Epub epub(path, "/.crosspoint");
|
||||||
|
epub.load(false);
|
||||||
|
if (!epub.getTitle().empty()) {
|
||||||
|
title = std::string(epub.getTitle());
|
||||||
|
}
|
||||||
|
} else if (ext5 == ".xtch") {
|
||||||
|
title.resize(title.length() - 5);
|
||||||
|
} else if (ext4 == ".xtc") {
|
||||||
|
title.resize(title.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
bookTitles.push_back(title);
|
||||||
|
bookPaths.push_back(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyLibraryActivity::loadFiles() {
|
||||||
|
files.clear();
|
||||||
|
|
||||||
|
auto root = SdMan.open(basepath.c_str());
|
||||||
|
if (!root || !root.isDirectory()) {
|
||||||
|
if (root) root.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.rewindDirectory();
|
||||||
|
|
||||||
|
char name[128];
|
||||||
|
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
||||||
|
file.getName(name, sizeof(name));
|
||||||
|
if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) {
|
||||||
|
file.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
files.emplace_back(std::string(name) + "/");
|
||||||
|
} else {
|
||||||
|
auto filename = std::string(name);
|
||||||
|
std::string ext4 = filename.length() >= 4 ? filename.substr(filename.length() - 4) : "";
|
||||||
|
std::string ext5 = filename.length() >= 5 ? filename.substr(filename.length() - 5) : "";
|
||||||
|
if (ext5 == ".epub" || ext5 == ".xtch" || ext4 == ".xtc") {
|
||||||
|
files.emplace_back(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
root.close();
|
||||||
|
sortFileList(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyLibraryActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<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]);
|
||||||
|
}
|
||||||
|
} 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back button
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
if (mappedInput.getHeldTime() < GO_HOME_MS) {
|
||||||
|
if (currentTab == Tab::Files && basepath != "/") {
|
||||||
|
// Go up one directory
|
||||||
|
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||||
|
if (basepath.empty()) basepath = "/";
|
||||||
|
loadFiles();
|
||||||
|
selectorIndex = 0;
|
||||||
|
updateRequired = true;
|
||||||
|
} else {
|
||||||
|
// Go home
|
||||||
|
onGoHome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab switching: Left/Right when selectorIndex == 0
|
||||||
|
if (selectorIndex == 0) {
|
||||||
|
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, Left/Right also work as prev/next
|
||||||
|
const bool prevReleased = upReleased || leftReleased;
|
||||||
|
const bool nextReleased = downReleased || rightReleased;
|
||||||
|
|
||||||
|
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 bottom button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("HOME", "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);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/activities/home/MyLibraryActivity.h
Normal file
65
src/activities/home/MyLibraryActivity.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#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 (from RecentBooksActivity)
|
||||||
|
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)> onSelectBook;
|
||||||
|
|
||||||
|
// Number of items that fit on a page
|
||||||
|
int getPageItems() const;
|
||||||
|
int getCurrentItemCount() const;
|
||||||
|
int getTotalPages() const;
|
||||||
|
int getCurrentPage() const;
|
||||||
|
|
||||||
|
// Data loading
|
||||||
|
void loadRecentBooks();
|
||||||
|
void loadFiles();
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
void renderRecentTab() const;
|
||||||
|
void renderFilesTab() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::function<void()>& onGoHome,
|
||||||
|
const std::function<void(const std::string& path)>& onSelectBook,
|
||||||
|
Tab initialTab = Tab::Recent)
|
||||||
|
: Activity("MyLibrary", renderer, mappedInput),
|
||||||
|
currentTab(initialTab),
|
||||||
|
onGoHome(onGoHome),
|
||||||
|
onSelectBook(onSelectBook) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
11
src/main.cpp
11
src/main.cpp
@ -15,7 +15,7 @@
|
|||||||
#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/home/HomeActivity.h"
|
#include "activities/home/HomeActivity.h"
|
||||||
#include "activities/home/RecentBooksActivity.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,7 +211,6 @@ void onGoToReader(const std::string& initialEpubPath) {
|
|||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome));
|
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome));
|
||||||
}
|
}
|
||||||
void onGoToReaderHome() { onGoToReader(std::string()); }
|
|
||||||
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
|
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath); }
|
||||||
|
|
||||||
void onGoToFileTransfer() {
|
void onGoToFileTransfer() {
|
||||||
@ -224,15 +223,15 @@ void onGoToSettings() {
|
|||||||
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoToRecentBooks() {
|
void onGoToMyLibrary() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
||||||
}
|
}
|
||||||
|
|
||||||
void onGoHome() {
|
void onGoHome() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToReaderHome, onGoToSettings,
|
enterNewActivity(
|
||||||
onGoToFileTransfer, onGoToRecentBooks));
|
new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings, onGoToFileTransfer));
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupDisplayAndFonts() {
|
void setupDisplayAndFonts() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user