feat: merge PR #511 - display epub metadata on Recents

Integrates upstream PR #511 changes while preserving local delete/archive
functionality. Key changes:

- Add RecentBook struct with path, title, author fields
- Update RecentBooksStore to store and serialize metadata
- Implement version migration for existing recent.bin files
- Update MyLibraryActivity to display two-line items (title + author)
- Update EpubReaderActivity and XtcReaderActivity to pass metadata

Maintains backwards compatibility with delete/archive feature from
local commit d5a9873.
This commit is contained in:
cottongin
2026-01-23 22:38:59 -05:00
parent 881f866d86
commit 2952d7554c
6 changed files with 112 additions and 67 deletions

View File

@@ -17,6 +17,7 @@ namespace {
constexpr int TAB_BAR_Y = 15;
constexpr int CONTENT_START_Y = 60;
constexpr int LINE_HEIGHT = 30;
constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items
constexpr int LEFT_MARGIN = 20;
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
@@ -40,7 +41,8 @@ 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;
const int lineHeight = (currentTab == Tab::Recent) ? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
int items = availableHeight / lineHeight;
if (items < 1) {
items = 1;
}
@@ -49,7 +51,7 @@ int MyLibraryActivity::getPageItems() const {
int MyLibraryActivity::getCurrentItemCount() const {
if (currentTab == Tab::Recent) {
return static_cast<int>(bookTitles.size());
return static_cast<int>(recentBooks.size());
}
return static_cast<int>(files.size());
}
@@ -67,34 +69,16 @@ int MyLibraryActivity::getCurrentPage() const {
}
void MyLibraryActivity::loadRecentBooks() {
constexpr size_t MAX_RECENT_BOOKS = 20;
bookTitles.clear();
bookPaths.clear();
recentBooks.clear();
const auto& books = RECENT_BOOKS.getBooks();
bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS));
bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS));
for (const auto& path : books) {
// Limit to maximum number of recent books
if (bookTitles.size() >= MAX_RECENT_BOOKS) {
break;
}
recentBooks.reserve(books.size());
for (const auto& book : books) {
// Skip if file no longer exists
if (!SdMan.exists(path.c_str())) {
if (!SdMan.exists(book.path.c_str())) {
continue;
}
// Extract filename from path for display
std::string title = path;
const size_t lastSlash = title.find_last_of('/');
if (lastSlash != std::string::npos) {
title = title.substr(lastSlash + 1);
}
bookTitles.push_back(title);
bookPaths.push_back(path);
recentBooks.push_back(book);
}
}
@@ -177,14 +161,13 @@ void MyLibraryActivity::onExit() {
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
bookTitles.clear();
bookPaths.clear();
recentBooks.clear();
files.clear();
}
bool MyLibraryActivity::isSelectedItemAFile() const {
if (currentTab == Tab::Recent) {
return !bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size());
return !recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size());
} else {
// Files tab - check if it's a file (not a directory)
if (files.empty() || selectorIndex >= static_cast<int>(files.size())) {
@@ -200,8 +183,18 @@ void MyLibraryActivity::openActionMenu() {
}
if (currentTab == Tab::Recent) {
actionTargetPath = bookPaths[selectorIndex];
actionTargetName = bookTitles[selectorIndex];
const auto& book = recentBooks[selectorIndex];
actionTargetPath = book.path;
// Use title if available, otherwise extract from path
if (!book.title.empty()) {
actionTargetName = book.title;
} else {
actionTargetName = book.path;
const size_t lastSlash = actionTargetName.find_last_of('/');
if (lastSlash != std::string::npos) {
actionTargetName = actionTargetName.substr(lastSlash + 1);
}
}
} else {
if (basepath.back() != '/') {
actionTargetPath = basepath + "/" + files[selectorIndex];
@@ -335,8 +328,8 @@ void MyLibraryActivity::loop() {
}
if (currentTab == Tab::Recent) {
if (!bookPaths.empty() && selectorIndex < static_cast<int>(bookPaths.size())) {
onSelectBook(bookPaths[selectorIndex], currentTab);
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
onSelectBook(recentBooks[selectorIndex].path, currentTab);
}
} else {
// Files tab
@@ -475,7 +468,7 @@ void MyLibraryActivity::render() const {
void MyLibraryActivity::renderRecentTab() const {
const auto pageWidth = renderer.getScreenWidth();
const int pageItems = getPageItems();
const int bookCount = static_cast<int>(bookTitles.size());
const int bookCount = static_cast<int>(recentBooks.size());
if (bookCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books");
@@ -485,14 +478,37 @@ void MyLibraryActivity::renderRecentTab() const {
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);
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN, RECENTS_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);
const auto& book = recentBooks[i];
const int y = CONTENT_START_Y + (i % pageItems) * RECENTS_LINE_HEIGHT;
// Line 1: Title
std::string title = book.title;
if (title.empty()) {
// Fallback for older entries or files without metadata
title = book.path;
const size_t lastSlash = title.find_last_of('/');
if (lastSlash != std::string::npos) {
title = title.substr(lastSlash + 1);
}
const size_t dot = title.find_last_of('.');
if (dot != std::string::npos) {
title.resize(dot);
}
}
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedTitle.c_str(), i != selectorIndex);
// Line 2: Author
if (!book.author.empty()) {
auto truncatedAuthor =
renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), i != selectorIndex);
}
}
}

View File

@@ -8,6 +8,7 @@
#include <vector>
#include "../Activity.h"
#include "RecentBooksStore.h"
class MyLibraryActivity final : public Activity {
public:
@@ -32,8 +33,7 @@ class MyLibraryActivity final : public Activity {
bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu
// Recent tab state
std::vector<std::string> bookTitles; // Display titles for each book
std::vector<std::string> bookPaths; // Paths for each visible book (excludes missing)
std::vector<RecentBook> recentBooks;
// Files tab state (from FileSelectionActivity)
std::string basepath = "/";

View File

@@ -82,7 +82,7 @@ void EpubReaderActivity::onEnter() {
// Save current epub as last opened epub and add to recent books
APP_STATE.openEpubPath = epub->getPath();
APP_STATE.saveToFile();
RECENT_BOOKS.addBook(epub->getPath());
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor());
// Trigger first update
updateRequired = true;

View File

@@ -46,7 +46,7 @@ void XtcReaderActivity::onEnter() {
// Save current XTC as last opened book and add to recent books
APP_STATE.openEpubPath = xtc->getPath();
APP_STATE.saveToFile();
RECENT_BOOKS.addBook(xtc->getPath());
RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), "");
// Trigger first update
updateRequired = true;