port: upstream PR #1342 - Book Info screen, richer metadata, safer controls

Ports upstream PR #1342 (feat: Add Book Info screen, richer metadata,
and safer file-browser controls) with mod-specific adaptations:

- Parse and cache series, seriesIndex, description from EPUB OPF
- Bump book.bin cache version to 6 for new metadata fields
- Add BookInfoActivity (new screen) accessible via Right button in FileBrowser
- Add ManageBook menu via Left button in FileBrowser (replaces upstream hidden delete)
- Guard all delete/archive actions with ConfirmationActivity (10 call sites)
- Add inputArmed gating to ConfirmationActivity to prevent accidental confirmation
- Safe deserialization: readString now returns bool with MAX_STRING_LENGTH guard
- Add series field to RecentBooksStore with JSON and binary serialization
- Add i18n keys: STR_BOOK_INFO, STR_AUTHOR, STR_SERIES, STR_FILE_SIZE, etc.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-09 00:39:32 -04:00
parent 255b98bda0
commit 4cf395aee9
129 changed files with 244823 additions and 248138 deletions

View File

@@ -14,6 +14,7 @@
#include <cstring>
#include <vector>
#include "../util/ConfirmationActivity.h"
#include "BookManageMenuActivity.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
@@ -25,7 +26,6 @@
#include "fontIds.h"
#include "util/BookManager.h"
int HomeActivity::getMenuItemCount() const {
int count = 4; // File Browser, Recents, File transfer, Settings
if (!recentBooks.empty()) {
@@ -83,7 +83,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
success = epub.generateThumbBmp(coverHeight);
if (success) {
const std::string thumbPath = epub.getThumbBmpPath(coverHeight);
RECENT_BOOKS.updateBook(book.path, book.title, book.author, thumbPath);
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
book.coverBmpPath = thumbPath;
} else {
const int thumbWidth = static_cast<int>(coverHeight * 0.6);
@@ -95,7 +95,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
success = xtc.generateThumbBmp(coverHeight);
if (success) {
const std::string thumbPath = xtc.getThumbBmpPath(coverHeight);
RECENT_BOOKS.updateBook(book.path, book.title, book.author, thumbPath);
RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, thumbPath);
book.coverBmpPath = thumbPath;
}
}
@@ -301,6 +301,43 @@ void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); }
void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); }
void HomeActivity::executeManageAction(BookManageMenuActivity::Action action, const std::string& capturedPath) {
bool success = false;
switch (action) {
case BookManageMenuActivity::Action::ARCHIVE:
success = BookManager::archiveBook(capturedPath);
break;
case BookManageMenuActivity::Action::UNARCHIVE:
success = BookManager::unarchiveBook(capturedPath);
break;
case BookManageMenuActivity::Action::DELETE:
success = BookManager::deleteBook(capturedPath);
break;
case BookManageMenuActivity::Action::DELETE_CACHE:
success = BookManager::deleteBookCache(capturedPath);
break;
case BookManageMenuActivity::Action::REINDEX:
success = BookManager::reindexBook(capturedPath, false);
break;
case BookManageMenuActivity::Action::REINDEX_FULL:
success = BookManager::reindexBook(capturedPath, true);
break;
}
{
RenderLock lock(*this);
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
}
requestUpdateAndWait();
recentsLoaded = false;
recentsLoading = false;
coverRendered = false;
freeCoverBuffer();
selectorIndex = 0;
firstRenderDone = false;
loadRecentBooks(UITheme::getInstance().getMetrics().homeRecentBooksCount);
requestUpdate();
}
void HomeActivity::openManageMenu(const std::string& bookPath) {
const bool isArchived = BookManager::isArchived(bookPath);
const std::string capturedPath = bookPath;
@@ -314,39 +351,25 @@ void HomeActivity::openManageMenu(const std::string& bookPath) {
}
const auto& menuResult = std::get<MenuResult>(result.data);
auto action = static_cast<BookManageMenuActivity::Action>(menuResult.action);
bool success = false;
switch (action) {
case BookManageMenuActivity::Action::ARCHIVE:
success = BookManager::archiveBook(capturedPath);
break;
case BookManageMenuActivity::Action::UNARCHIVE:
success = BookManager::unarchiveBook(capturedPath);
break;
case BookManageMenuActivity::Action::DELETE:
success = BookManager::deleteBook(capturedPath);
break;
case BookManageMenuActivity::Action::DELETE_CACHE:
success = BookManager::deleteBookCache(capturedPath);
break;
case BookManageMenuActivity::Action::REINDEX:
success = BookManager::reindexBook(capturedPath, false);
break;
case BookManageMenuActivity::Action::REINDEX_FULL:
success = BookManager::reindexBook(capturedPath, true);
break;
if (action == BookManageMenuActivity::Action::DELETE || action == BookManageMenuActivity::Action::ARCHIVE) {
const char* promptKey =
(action == BookManageMenuActivity::Action::DELETE) ? tr(STR_DELETE_BOOK) : tr(STR_ARCHIVE_BOOK);
std::string heading = std::string(promptKey) + "?";
std::string fileName = capturedPath;
const auto slash = capturedPath.rfind('/');
if (slash != std::string::npos) fileName = capturedPath.substr(slash + 1);
startActivityForResult(std::make_unique<ConfirmationActivity>(renderer, mappedInput, heading, fileName),
[this, action, capturedPath](const ActivityResult& confirmResult) {
if (!confirmResult.isCancelled) {
executeManageAction(action, capturedPath);
} else {
requestUpdate();
}
});
} else {
executeManageAction(action, capturedPath);
}
{
RenderLock lock(*this);
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
}
requestUpdateAndWait();
recentsLoaded = false;
recentsLoading = false;
coverRendered = false;
freeCoverBuffer();
selectorIndex = 0;
firstRenderDone = false;
loadRecentBooks(UITheme::getInstance().getMetrics().homeRecentBooksCount);
requestUpdate();
});
}