Compare commits

..

2 Commits

Author SHA1 Message Date
CaptainFrito
e9469b7475
Merge db2a7f4f8d into da4d3b5ea5 2026-01-30 09:34:25 +00:00
CaptainFrito
db2a7f4f8d feat: UI themes, Lyra 2026-01-30 16:34:12 +07:00
13 changed files with 295 additions and 166 deletions

View File

@ -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");

View File

@ -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) {}

View File

@ -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();
}

View File

@ -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;

View 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();
}

View 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;
};

View File

@ -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) {

View File

@ -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;

View File

@ -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();

View File

@ -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);
}

View File

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

View File

@ -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);
}
}

View File

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