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

@@ -6,6 +6,7 @@
#include <algorithm>
#include "../util/ConfirmationActivity.h"
#include "BookManageMenuActivity.h"
#include "MappedInputManager.h"
#include "RecentBooksStore.h"
@@ -47,6 +48,37 @@ void RecentBooksActivity::onExit() {
recentBooks.clear();
}
void RecentBooksActivity::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));
}
loadRecentBooks();
selectorIndex = 0;
requestUpdate();
}
void RecentBooksActivity::openManageMenu(const std::string& bookPath) {
const bool isArchived = BookManager::isArchived(bookPath);
const std::string capturedPath = bookPath;
@@ -59,34 +91,26 @@ void RecentBooksActivity::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));
}
loadRecentBooks();
selectorIndex = 0;
requestUpdate();
});
}
@@ -156,7 +180,15 @@ void RecentBooksActivity::render(RenderLock&&) {
} else {
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, recentBooks.size(), selectorIndex,
[this](int index) { return recentBooks[index].title; }, [this](int index) { return recentBooks[index].author; },
[this](int index) { return recentBooks[index].title; },
[this](int index) {
const auto& book = recentBooks[index];
if (!book.series.empty() && !book.author.empty()) {
return book.author + " \xE2\x80\xA2 " + book.series;
}
if (!book.series.empty()) return book.series;
return book.author;
},
[this](int index) { return UITheme::getFileIcon(recentBooks[index].path); });
}