mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 16:17:38 +03:00
Compare commits
2 Commits
3ea0878189
...
e9469b7475
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9469b7475 | ||
|
|
db2a7f4f8d |
@ -24,7 +24,7 @@ void HomeActivity::taskTrampoline(void* param) {
|
||||
}
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
int count = 3; // My Library, File transfer, Settings
|
||||
int count = 4; // My Library, Recents, File transfer, Settings
|
||||
if (!recentBooks.empty()) {
|
||||
count += recentBooks.size();
|
||||
}
|
||||
@ -69,12 +69,7 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight, PopupCallbacks
|
||||
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
|
||||
Epub epub(path, "/.crosspoint");
|
||||
epub.load(false);
|
||||
// if (!epub.getTitle().empty()) {
|
||||
// lastBookTitle = std::string(epub.getTitle());
|
||||
// }
|
||||
// if (!epub.getAuthor().empty()) {
|
||||
// lastBookAuthor = std::string(epub.getAuthor());
|
||||
// }
|
||||
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
coverBmpPath = epub.getThumbBmpPath(coverHeight);
|
||||
if (!SdMan.exists(coverBmpPath.c_str())) {
|
||||
@ -93,9 +88,6 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight, PopupCallbacks
|
||||
// Handle XTC file
|
||||
Xtc xtc(path, "/.crosspoint");
|
||||
if (xtc.load()) {
|
||||
// if (!xtc.getTitle().empty()) {
|
||||
// lastBookTitle = std::string(xtc.getTitle());
|
||||
// }
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
coverBmpPath = xtc.getThumbBmpPath(coverHeight);
|
||||
if (!SdMan.exists(coverBmpPath.c_str())) {
|
||||
@ -110,16 +102,6 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight, PopupCallbacks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if (lastBookTitle.empty()) {
|
||||
// // Remove extension from title if we don't have metadata
|
||||
// if (StringUtils::checkFileExtension(lastBookFileName, ".xtch")) {
|
||||
// lastBookFileName.resize(lastBookFileName.length() - 5);
|
||||
// } else if (StringUtils::checkFileExtension(lastBookFileName, ".xtc")) {
|
||||
// lastBookFileName.resize(lastBookFileName.length() - 4);
|
||||
// }
|
||||
// lastBookTitle = lastBookFileName;
|
||||
// }
|
||||
}
|
||||
|
||||
recentBooks.push_back(RecentBookWithCover{book, coverBmpPath});
|
||||
@ -227,14 +209,17 @@ void HomeActivity::loop() {
|
||||
int idx = 0;
|
||||
int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
|
||||
const int myLibraryIdx = idx++;
|
||||
const int recentsIdx = idx++;
|
||||
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
|
||||
const int fileTransferIdx = idx++;
|
||||
const int settingsIdx = idx;
|
||||
|
||||
if (selectorIndex < recentBooks.size()) {
|
||||
onSelectBook(recentBooks[selectorIndex].book.path, MyLibraryActivity::Tab::Recent);
|
||||
onSelectBook(recentBooks[selectorIndex].book.path);
|
||||
} else if (menuSelectedIndex == myLibraryIdx) {
|
||||
onMyLibraryOpen();
|
||||
} else if (menuSelectedIndex == recentsIdx) {
|
||||
onRecentsOpen();
|
||||
} else if (menuSelectedIndex == opdsLibraryIdx) {
|
||||
onOpdsBrowserOpen();
|
||||
} else if (menuSelectedIndex == fileTransferIdx) {
|
||||
@ -289,7 +274,7 @@ void HomeActivity::render() {
|
||||
}
|
||||
|
||||
// Build menu items dynamically
|
||||
std::vector<const char*> menuItems = {"Browse Files", "File Transfer", "Settings"};
|
||||
std::vector<const char*> menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"};
|
||||
if (hasOpdsUrl) {
|
||||
// Insert OPDS Browser after My Library
|
||||
menuItems.insert(menuItems.begin() + 1, "OPDS Browser");
|
||||
|
||||
@ -27,8 +27,9 @@ class HomeActivity final : public Activity {
|
||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
||||
std::vector<RecentBookWithCover> recentBooks;
|
||||
const std::function<void(const std::string& path, MyLibraryActivity::Tab fromTab)> onSelectBook;
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onMyLibraryOpen;
|
||||
const std::function<void()> onRecentsOpen;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
const std::function<void()> onFileTransferOpen;
|
||||
const std::function<void()> onOpdsBrowserOpen;
|
||||
@ -43,14 +44,15 @@ class HomeActivity final : public Activity {
|
||||
void loadRecentBooks(int maxBooks, int coverHeight, PopupCallbacks& popupCallbacks);
|
||||
|
||||
public:
|
||||
explicit HomeActivity(
|
||||
GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(const std::string& path, MyLibraryActivity::Tab fromTab)>& onSelectBook,
|
||||
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onSettingsOpen,
|
||||
const std::function<void()>& onFileTransferOpen, const std::function<void()>& onOpdsBrowserOpen)
|
||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(const std::string& path)>& onSelectBook,
|
||||
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onRecentsOpen,
|
||||
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
||||
const std::function<void()>& onOpdsBrowserOpen)
|
||||
: Activity("Home", renderer, mappedInput),
|
||||
onSelectBook(onSelectBook),
|
||||
onMyLibraryOpen(onMyLibraryOpen),
|
||||
onRecentsOpen(onRecentsOpen),
|
||||
onSettingsOpen(onSettingsOpen),
|
||||
onFileTransferOpen(onFileTransferOpen),
|
||||
onOpdsBrowserOpen(onOpdsBrowserOpen) {}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/StringUtils.h"
|
||||
@ -29,20 +28,6 @@ void MyLibraryActivity::taskTrampoline(void* param) {
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void MyLibraryActivity::loadRecentBooks() {
|
||||
recentBooks.clear();
|
||||
const auto& books = RECENT_BOOKS.getBooks();
|
||||
recentBooks.reserve(books.size());
|
||||
|
||||
for (const auto& book : books) {
|
||||
// Skip if file no longer exists
|
||||
if (!SdMan.exists(book.path.c_str())) {
|
||||
continue;
|
||||
}
|
||||
recentBooks.push_back(book);
|
||||
}
|
||||
}
|
||||
|
||||
void MyLibraryActivity::loadFiles() {
|
||||
files.clear();
|
||||
|
||||
@ -83,8 +68,6 @@ void MyLibraryActivity::onEnter() {
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Load data for both tabs
|
||||
loadRecentBooks();
|
||||
loadFiles();
|
||||
|
||||
selectorIndex = 0;
|
||||
@ -115,54 +98,45 @@ void MyLibraryActivity::onExit() {
|
||||
|
||||
void MyLibraryActivity::loop() {
|
||||
// Long press BACK (1s+) 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;
|
||||
}
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
|
||||
basepath != "/") {
|
||||
basepath = "/";
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left);
|
||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right);
|
||||
const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||
const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||
;
|
||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||
|
||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, true, true);
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (currentTab == Tab::Recent) {
|
||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||
Serial.printf("Selected recent book: %s\n", recentBooks[selectorIndex].path.c_str());
|
||||
onSelectBook(recentBooks[selectorIndex].path, currentTab);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (files.empty()) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
onSelectBook(basepath + files[selectorIndex], currentTab);
|
||||
return;
|
||||
}
|
||||
if (basepath.back() != '/') basepath += "/";
|
||||
if (files[selectorIndex].back() == '/') {
|
||||
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
} else {
|
||||
onSelectBook(basepath + files[selectorIndex]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 (currentTab == Tab::Files && basepath != "/") {
|
||||
if (basepath != "/") {
|
||||
const std::string oldPath = basepath;
|
||||
|
||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||
@ -180,19 +154,7 @@ void MyLibraryActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching: Left/Right always control tabs
|
||||
if (leftReleased || rightReleased) {
|
||||
if (currentTab == Tab::Files) {
|
||||
currentTab = Tab::Recent;
|
||||
} else {
|
||||
currentTab = Tab::Files;
|
||||
}
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
int listSize = (currentTab == Tab::Recent) ? static_cast<int>(recentBooks.size()) : static_cast<int>(files.size());
|
||||
int listSize = static_cast<int>(files.size());
|
||||
if (upReleased) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + listSize) % listSize;
|
||||
@ -232,34 +194,19 @@ void MyLibraryActivity::render() const {
|
||||
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str();
|
||||
UITheme::drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName);
|
||||
|
||||
UITheme::drawTabBar(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
|
||||
{{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}, false);
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
|
||||
if (currentTab == Tab::Recent) {
|
||||
// Recent tab
|
||||
if (recentBooks.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No recent books");
|
||||
} else {
|
||||
UITheme::drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
|
||||
[this](int index) { return recentBooks[index].title; }, false, nullptr, false, nullptr);
|
||||
}
|
||||
if (files.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No books found");
|
||||
} else {
|
||||
if (files.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No books found");
|
||||
} else {
|
||||
UITheme::drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
||||
[this](int index) { return files[index]; }, false, nullptr, false, nullptr);
|
||||
}
|
||||
UITheme::drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, files.size(), selectorIndex,
|
||||
[this](int index) { return files[index]; }, false, nullptr, false, nullptr);
|
||||
}
|
||||
|
||||
// Help text
|
||||
const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down");
|
||||
UITheme::drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
UITheme::drawSideButtonHints(renderer, "^", "v");
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
@ -8,29 +8,21 @@
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "RecentBooksStore.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;
|
||||
size_t selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
|
||||
// Recent tab state
|
||||
std::vector<RecentBook> recentBooks;
|
||||
|
||||
// Files tab state (from FileSelectionActivity)
|
||||
// Files state
|
||||
std::string basepath = "/";
|
||||
std::vector<std::string> files;
|
||||
|
||||
// Callbacks
|
||||
const std::function<void(const std::string& path, Tab fromTab)> onSelectBook;
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
@ -38,18 +30,16 @@ class MyLibraryActivity final : public Activity {
|
||||
void render() const;
|
||||
|
||||
// Data loading
|
||||
void loadRecentBooks();
|
||||
void loadFiles();
|
||||
size_t findEntry(const std::string& name) 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 = "/")
|
||||
const std::function<void(const std::string& path)>& onSelectBook,
|
||||
std::string initialPath = "/")
|
||||
: Activity("MyLibrary", renderer, mappedInput),
|
||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
||||
currentTab(initialTab),
|
||||
onSelectBook(onSelectBook),
|
||||
onGoHome(onGoHome) {}
|
||||
void onEnter() override;
|
||||
|
||||
148
src/activities/home/RecentBooksActivity.cpp
Normal file
148
src/activities/home/RecentBooksActivity.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#include "RecentBooksActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
namespace {
|
||||
constexpr int SKIP_PAGE_MS = 700;
|
||||
constexpr unsigned long GO_HOME_MS = 1000;
|
||||
} // namespace
|
||||
|
||||
void RecentBooksActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<RecentBooksActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void RecentBooksActivity::loadRecentBooks() {
|
||||
recentBooks.clear();
|
||||
const auto& books = RECENT_BOOKS.getBooks();
|
||||
recentBooks.reserve(books.size());
|
||||
|
||||
for (const auto& book : books) {
|
||||
// Skip if file no longer exists
|
||||
if (!SdMan.exists(book.path.c_str())) {
|
||||
continue;
|
||||
}
|
||||
recentBooks.push_back(book);
|
||||
}
|
||||
}
|
||||
|
||||
void RecentBooksActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// Load data
|
||||
loadRecentBooks();
|
||||
|
||||
selectorIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&RecentBooksActivity::taskTrampoline, "RecentBooksActivityTask",
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void RecentBooksActivity::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;
|
||||
|
||||
recentBooks.clear();
|
||||
}
|
||||
|
||||
void RecentBooksActivity::loop() {
|
||||
const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Up);
|
||||
;
|
||||
const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Down);
|
||||
|
||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, true, true);
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||
Serial.printf("Selected recent book: %s\n", recentBooks[selectorIndex].path.c_str());
|
||||
onSelectBook(recentBooks[selectorIndex].path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onGoHome();
|
||||
}
|
||||
|
||||
int listSize = static_cast<int>(recentBooks.size());
|
||||
if (upReleased) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + listSize) % listSize;
|
||||
} else {
|
||||
selectorIndex = (selectorIndex + listSize - 1) % listSize;
|
||||
}
|
||||
updateRequired = true;
|
||||
} else if (downReleased) {
|
||||
if (skipPage) {
|
||||
selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % listSize;
|
||||
} else {
|
||||
selectorIndex = (selectorIndex + 1) % listSize;
|
||||
}
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RecentBooksActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void RecentBooksActivity::render() const {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
auto metrics = UITheme::getMetrics();
|
||||
|
||||
UITheme::drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Recent Books");
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
|
||||
|
||||
// Recent tab
|
||||
if (recentBooks.empty()) {
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No recent books");
|
||||
} else {
|
||||
UITheme::drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
|
||||
[this](int index) { return recentBooks[index].title; }, false, nullptr, false, nullptr);
|
||||
}
|
||||
|
||||
// Help text
|
||||
const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down");
|
||||
UITheme::drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
43
src/activities/home/RecentBooksActivity.h
Normal file
43
src/activities/home/RecentBooksActivity.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "RecentBooksStore.h"
|
||||
|
||||
class RecentBooksActivity final : public Activity {
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
|
||||
size_t selectorIndex = 0;
|
||||
bool updateRequired = false;
|
||||
|
||||
// Recent tab state
|
||||
std::vector<RecentBook> recentBooks;
|
||||
|
||||
// Callbacks
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
// Data loading
|
||||
void loadRecentBooks();
|
||||
|
||||
public:
|
||||
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoHome,
|
||||
const std::function<void(const std::string& path)>& onSelectBook)
|
||||
: Activity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
};
|
||||
@ -74,7 +74,7 @@ std::unique_ptr<Txt> ReaderActivity::loadTxt(const std::string& path) {
|
||||
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);
|
||||
onGoToLibrary(initialPath, libraryTab);
|
||||
onGoToLibrary(initialPath);
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
||||
|
||||
@ -10,10 +10,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
|
||||
std::string currentBookPath; // Track current book path for navigation
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void(const std::string&, MyLibraryActivity::Tab)> onGoToLibrary;
|
||||
const std::function<void(const std::string&)> 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);
|
||||
@ -28,11 +27,10 @@ class ReaderActivity final : public ActivityWithSubactivity {
|
||||
|
||||
public:
|
||||
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
|
||||
MyLibraryActivity::Tab libraryTab, const std::function<void()>& onGoBack,
|
||||
const std::function<void(const std::string&, MyLibraryActivity::Tab)>& onGoToLibrary)
|
||||
const std::function<void()>& onGoBack,
|
||||
const std::function<void(const std::string&)>& onGoToLibrary)
|
||||
: ActivityWithSubactivity("Reader", renderer, mappedInput),
|
||||
initialBookPath(std::move(initialBookPath)),
|
||||
libraryTab(libraryTab),
|
||||
onGoBack(onGoBack),
|
||||
onGoToLibrary(onGoToLibrary) {}
|
||||
void onEnter() override;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
|
||||
|
||||
namespace {
|
||||
constexpr int changeTabsMs = 700;
|
||||
constexpr int displaySettingsCount = 7;
|
||||
const SettingInfo displaySettings[displaySettingsCount] = {
|
||||
// Should match with SLEEP_SCREEN_MODE
|
||||
@ -111,7 +112,7 @@ void SettingsActivity::loop() {
|
||||
return;
|
||||
}
|
||||
bool hasChangedCategory = false;
|
||||
|
||||
|
||||
// Handle actions with early return
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
if (selectedSettingIndex == 0) {
|
||||
@ -131,22 +132,27 @@ void SettingsActivity::loop() {
|
||||
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 changeTab = mappedInput.getHeldTime() > changeTabsMs;
|
||||
|
||||
// Handle navigation
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount);
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0;
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
||||
if (upReleased && changeTab) {
|
||||
hasChangedCategory = true;
|
||||
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
||||
} else if (downReleased && changeTab) {
|
||||
hasChangedCategory = true;
|
||||
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
|
||||
updateRequired = true;
|
||||
} else if (upReleased || leftReleased) {
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount);
|
||||
updateRequired = true;
|
||||
} else if (rightReleased || downReleased) {
|
||||
selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
if (hasChangedCategory) {
|
||||
@ -270,8 +276,8 @@ void SettingsActivity::render() const {
|
||||
Rect{0, metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing, pageWidth,
|
||||
pageHeight - (metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.buttonHintsHeight +
|
||||
metrics.verticalSpacing * 2)},
|
||||
settingsCount, selectedSettingIndex - 1, [this](int index) { return std::string(settingsList[index].name); }, false,
|
||||
nullptr, true,
|
||||
settingsCount, selectedSettingIndex - 1, [this](int index) { return std::string(settingsList[index].name); },
|
||||
false, nullptr, true,
|
||||
[this](int i) {
|
||||
const auto& setting = settingsList[i];
|
||||
std::string valueText = "";
|
||||
@ -293,9 +299,8 @@ void SettingsActivity::render() const {
|
||||
metrics.versionTextY, CROSSPOINT_VERSION);
|
||||
|
||||
// Draw help text
|
||||
const auto labels = mappedInput.mapLabels("« Back", selectedSettingIndex == 0 ? "Tab >" : "Toggle", "Up", "Down");
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "Up", "Down");
|
||||
UITheme::drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
UITheme::drawSideButtonHints(renderer, "^", "v");
|
||||
|
||||
// Always use standard refresh for settings screen
|
||||
renderer.displayBuffer();
|
||||
|
||||
@ -89,7 +89,8 @@ void UITheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* tit
|
||||
}
|
||||
}
|
||||
|
||||
void UITheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs, bool selected) {
|
||||
void UITheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) {
|
||||
if (currentTheme != nullptr) {
|
||||
currentTheme->drawTabBar(renderer, rect, tabs, selected);
|
||||
}
|
||||
|
||||
@ -170,7 +170,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
constexpr int margin = 15; // Offset from right edge
|
||||
|
||||
const int centerX = rect.x + rect.width - indicatorWidth / 2 - margin;
|
||||
const int indicatorTop = rect.y + 60; // Offset to avoid overlapping side button hints
|
||||
const int indicatorTop = rect.y; // Offset to avoid overlapping side button hints
|
||||
const int indicatorBottom = rect.y + rect.height - 30;
|
||||
|
||||
// Draw up arrow at top (^) - narrow point at top, wide base at bottom
|
||||
@ -234,7 +234,8 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
}
|
||||
}
|
||||
|
||||
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs, bool selected) {
|
||||
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) {
|
||||
constexpr int underlineHeight = 2; // Height of selection underline
|
||||
constexpr int underlineGap = 4; // Gap between text and underline
|
||||
|
||||
|
||||
@ -93,13 +93,15 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve
|
||||
renderer.fillRoundedRect(currentX, rect.y + 1, textWidth + 2 * hPaddingInSelection, rect.height - 4,
|
||||
cornerRadius, COLOR_BLACK);
|
||||
} else {
|
||||
renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3, COLOR_LIGHT_GRAY);
|
||||
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection, rect.y + rect.height - 3, 2, true);
|
||||
renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3,
|
||||
COLOR_LIGHT_GRAY);
|
||||
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection,
|
||||
rect.y + rect.height - 3, 2, true);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, rect.y + 6, tab.label, !(tab.selected && selected), EpdFontFamily::REGULAR);
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, rect.y + 6, tab.label, !(tab.selected && selected),
|
||||
EpdFontFamily::REGULAR);
|
||||
|
||||
currentX += textWidth + LyraMetrics::values.tabSpacing + 2 * hPaddingInSelection;
|
||||
}
|
||||
@ -116,7 +118,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
|
||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||
if (totalPages > 1) {
|
||||
const int scrollAreaHeight = topHintButtonY - rect.y - LyraMetrics::values.verticalSpacing;
|
||||
const int scrollAreaHeight = rect.height;
|
||||
|
||||
// Draw scroll bar
|
||||
const int scrollBarHeight = (scrollAreaHeight * pageItems) / itemCount;
|
||||
@ -134,8 +136,8 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
(totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
|
||||
if (selectedIndex >= 0) {
|
||||
renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
||||
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius,
|
||||
COLOR_LIGHT_GRAY);
|
||||
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius,
|
||||
COLOR_LIGHT_GRAY);
|
||||
}
|
||||
|
||||
// Draw all items
|
||||
@ -198,8 +200,8 @@ void LyraTheme::drawButtonHints(const GfxRenderer& renderer, const char* btn1, c
|
||||
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
|
||||
renderer.drawText(SMALL_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
||||
} else {
|
||||
renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true, true, false,
|
||||
false, true);
|
||||
renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true,
|
||||
true, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
src/main.cpp
23
src/main.cpp
@ -20,6 +20,7 @@
|
||||
#include "activities/browser/OpdsBookBrowserActivity.h"
|
||||
#include "activities/home/HomeActivity.h"
|
||||
#include "activities/home/MyLibraryActivity.h"
|
||||
#include "activities/home/RecentBooksActivity.h"
|
||||
#include "activities/network/CrossPointWebServerActivity.h"
|
||||
#include "activities/reader/ReaderActivity.h"
|
||||
#include "activities/settings/SettingsActivity.h"
|
||||
@ -204,11 +205,12 @@ void enterDeepSleep() {
|
||||
}
|
||||
|
||||
void onGoHome();
|
||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab);
|
||||
void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
|
||||
void onGoToMyLibraryWithPath(const std::string& path);
|
||||
void onGoToRecentBooks();
|
||||
void onGoToReader(const std::string& initialEpubPath) {
|
||||
exitActivity();
|
||||
enterNewActivity(
|
||||
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab));
|
||||
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome, onGoToMyLibraryWithPath));
|
||||
}
|
||||
|
||||
void onGoToFileTransfer() {
|
||||
@ -226,9 +228,14 @@ void onGoToMyLibrary() {
|
||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
||||
}
|
||||
|
||||
void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
|
||||
void onGoToRecentBooks() {
|
||||
exitActivity();
|
||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, tab, path));
|
||||
enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
|
||||
}
|
||||
|
||||
void onGoToMyLibraryWithPath(const std::string& path) {
|
||||
exitActivity();
|
||||
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path));
|
||||
}
|
||||
|
||||
void onGoToBrowser() {
|
||||
@ -238,8 +245,8 @@ void onGoToBrowser() {
|
||||
|
||||
void onGoHome() {
|
||||
exitActivity();
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary, onGoToSettings,
|
||||
onGoToFileTransfer, onGoToBrowser));
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary, onGoToRecentBooks,
|
||||
onGoToSettings, onGoToFileTransfer, onGoToBrowser));
|
||||
}
|
||||
|
||||
void setupDisplayAndFonts() {
|
||||
@ -320,7 +327,7 @@ void setup() {
|
||||
APP_STATE.openEpubPath = "";
|
||||
APP_STATE.lastSleepImage = 0;
|
||||
APP_STATE.saveToFile();
|
||||
onGoToReader(path, MyLibraryActivity::Tab::Recent);
|
||||
onGoToReader(path);
|
||||
}
|
||||
|
||||
// Ensure we're not still holding the power button before leaving setup
|
||||
|
||||
Loading…
Reference in New Issue
Block a user