Files
crosspoint-reader-mod/src/activities/home/BookInfoActivity.cpp
cottongin 4c62437689 fix: add header bar and fix bottom spacing in BookInfo
Draw the standard header (clock, battery, title) via GUI.drawHeader().
Replace hardcoded MARGIN with theme metrics for content positioning.
Content area now starts below the header and stops above button hints
so the last lines are never obscured.

Made-with: Cursor
2026-03-09 03:17:49 -04:00

320 lines
10 KiB
C++

#include "BookInfoActivity.h"
#include <algorithm>
#include <Bitmap.h>
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Logging.h>
#include <PlaceholderCoverGenerator.h>
#include <Xtc.h>
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
constexpr int LABEL_VALUE_GAP = 4;
constexpr int SECTION_GAP = 14;
constexpr int MAX_WRAPPED_LINES = 60;
constexpr int COVER_GAP = 16;
std::string normalizeWhitespace(const std::string& s) {
std::string out;
out.reserve(s.size());
bool prevSpace = false;
for (const char c : s) {
if (c == '\n' || c == '\r' || c == '\t') {
if (!prevSpace) {
out += ' ';
prevSpace = true;
}
} else {
out += c;
prevSpace = (c == ' ');
}
}
return out;
}
} // namespace
void BookInfoActivity::onEnter() {
Activity::onEnter();
std::string fileName = filePath;
const size_t lastSlash = filePath.rfind('/');
if (lastSlash != std::string::npos) {
fileName = filePath.substr(lastSlash + 1);
}
size_t fileSize = 0;
{
FsFile file;
if (Storage.openFileForRead("BIF", filePath, file)) {
fileSize = file.fileSize();
file.close();
}
}
BookMetadataCache::BookMetadata meta;
if (FsHelpers::hasEpubExtension(fileName)) {
Epub epub(filePath, "/.crosspoint");
bool needsBuild = !epub.load(false, true);
Rect popupRect{};
if (needsBuild) {
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING));
GUI.fillPopupProgress(renderer, popupRect, 10);
epub.load(true, true);
GUI.fillPopupProgress(renderer, popupRect, 50);
}
meta = epub.getMetadata();
meta.description = normalizeWhitespace(meta.description);
const int coverH = renderer.getScreenHeight() * 2 / 5;
if (epub.generateThumbBmp(coverH)) {
coverBmpPath = epub.getThumbBmpPath(coverH);
} else {
const int thumbW = static_cast<int>(coverH * 0.6);
const std::string placeholderPath = epub.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp";
if (PlaceholderCoverGenerator::generate(placeholderPath, meta.title.empty() ? fileName : meta.title, meta.author,
thumbW, coverH)) {
coverBmpPath = placeholderPath;
}
}
if (needsBuild) {
GUI.fillPopupProgress(renderer, popupRect, 100);
}
} else if (FsHelpers::hasXtcExtension(fileName)) {
Xtc xtc(filePath, "/.crosspoint");
bool needsBuild = !Storage.exists(xtc.getCachePath().c_str());
Rect popupRect{};
if (needsBuild) {
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING));
GUI.fillPopupProgress(renderer, popupRect, 10);
}
if (xtc.load()) {
if (needsBuild) {
GUI.fillPopupProgress(renderer, popupRect, 50);
}
meta.title = xtc.getTitle();
meta.author = xtc.getAuthor();
const int coverH = renderer.getScreenHeight() * 2 / 5;
if (xtc.generateThumbBmp(coverH)) {
coverBmpPath = xtc.getThumbBmpPath(coverH);
} else {
const int thumbW = static_cast<int>(coverH * 0.6);
const std::string placeholderPath = xtc.getCachePath() + "/placeholder_" + std::to_string(coverH) + ".bmp";
if (PlaceholderCoverGenerator::generate(placeholderPath, meta.title.empty() ? fileName : meta.title, meta.author,
thumbW, coverH)) {
coverBmpPath = placeholderPath;
}
}
}
if (needsBuild) {
GUI.fillPopupProgress(renderer, popupRect, 100);
}
}
if (meta.title.empty()) {
meta.title = fileName;
}
buildLayout(meta, fileSize);
requestUpdate();
}
void BookInfoActivity::onExit() { Activity::onExit(); }
void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize) {
const auto& metrics = UITheme::getInstance().getMetrics();
const int sidePad = metrics.contentSidePadding;
const int contentW = renderer.getScreenWidth() - sidePad * 2;
fields.reserve(13);
auto addField = [&](const char* label, const std::string& text, bool bold, EpdFontFamily::Style style) {
if (text.empty()) return;
InfoField field;
field.label = label;
field.bold = bold;
field.lines = renderer.wrappedText(UI_12_FONT_ID, text.c_str(), contentW, MAX_WRAPPED_LINES, style);
fields.push_back(std::move(field));
};
addField(nullptr, meta.title, true, EpdFontFamily::BOLD);
addField(tr(STR_AUTHOR), meta.author, false, EpdFontFamily::REGULAR);
if (!meta.series.empty()) {
std::string seriesStr = meta.series;
if (!meta.seriesIndex.empty()) {
seriesStr += " #" + meta.seriesIndex;
}
addField(tr(STR_SERIES), seriesStr, false, EpdFontFamily::REGULAR);
}
addField(tr(STR_PUBLISHER), meta.publisher, false, EpdFontFamily::REGULAR);
addField(tr(STR_DATE), meta.date, false, EpdFontFamily::REGULAR);
addField(tr(STR_SUBJECTS), meta.subjects, false, EpdFontFamily::REGULAR);
if (!meta.rating.empty()) {
int ratingVal = atoi(meta.rating.c_str());
if (ratingVal > 0 && ratingVal <= 10) {
char ratingBuf[8];
snprintf(ratingBuf, sizeof(ratingBuf), "%d / 5", (ratingVal + 1) / 2);
addField(tr(STR_RATING), std::string(ratingBuf), false, EpdFontFamily::REGULAR);
}
}
addField(tr(STR_LANGUAGE), meta.language, false, EpdFontFamily::REGULAR);
addField(tr(STR_ISBN), meta.identifier, false, EpdFontFamily::REGULAR);
addField(tr(STR_CONTRIBUTOR), meta.contributor, false, EpdFontFamily::REGULAR);
if (fileSize > 0) {
addField(tr(STR_FILE_SIZE), formatFileSize(fileSize), false, EpdFontFamily::REGULAR);
}
addField(tr(STR_RIGHTS), meta.rights, false, EpdFontFamily::REGULAR);
addField(tr(STR_DESCRIPTION), meta.description, false, EpdFontFamily::REGULAR);
const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID);
const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID);
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
int h = contentTop;
if (!coverBmpPath.empty()) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
coverDisplayHeight = bitmap.getHeight();
coverDisplayWidth = bitmap.getWidth();
}
file.close();
}
if (coverDisplayHeight > 0) {
h += coverDisplayHeight + COVER_GAP;
}
}
for (const auto& field : fields) {
if (field.label) {
h += lineH10 + LABEL_VALUE_GAP;
}
h += static_cast<int>(field.lines.size()) * lineH12;
h += SECTION_GAP;
}
h += metrics.buttonHintsHeight + metrics.verticalSpacing;
contentHeight = h;
}
void BookInfoActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
ActivityResult r;
r.isCancelled = true;
setResult(std::move(r));
finish();
return;
}
const int pageH = renderer.getScreenHeight();
const int scrollStep = pageH / 3;
if (mappedInput.wasReleased(MappedInputManager::Button::Down) ||
mappedInput.wasReleased(MappedInputManager::Button::PageForward) ||
mappedInput.wasReleased(MappedInputManager::Button::Left)) {
if (scrollOffset + pageH < contentHeight) {
scrollOffset += scrollStep;
requestUpdate();
}
}
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
mappedInput.wasReleased(MappedInputManager::Button::PageBack) ||
mappedInput.wasReleased(MappedInputManager::Button::Right)) {
if (scrollOffset > 0) {
scrollOffset -= scrollStep;
if (scrollOffset < 0) scrollOffset = 0;
requestUpdate();
}
}
}
void BookInfoActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const int pageW = renderer.getScreenWidth();
const int pageH = renderer.getScreenHeight();
const int sidePad = metrics.contentSidePadding;
const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID);
const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID);
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentBottom = pageH - metrics.buttonHintsHeight - metrics.verticalSpacing;
GUI.drawHeader(renderer, Rect(0, metrics.topPadding, pageW, metrics.headerHeight), tr(STR_BOOK_INFO));
int y = contentTop - scrollOffset;
if (!coverBmpPath.empty() && coverDisplayHeight > 0) {
if (y + coverDisplayHeight > contentTop && y < contentBottom) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
const int coverX = (pageW - coverDisplayWidth) / 2;
renderer.drawBitmap1Bit(bitmap, coverX, y, coverDisplayWidth,
std::min(coverDisplayHeight, contentBottom - y));
}
file.close();
}
}
y += coverDisplayHeight + COVER_GAP;
}
for (const auto& field : fields) {
if (y >= contentBottom) break;
if (field.label) {
if (y + lineH10 > contentTop && y < contentBottom) {
renderer.drawText(UI_10_FONT_ID, sidePad, y, field.label, true, EpdFontFamily::BOLD);
}
y += lineH10 + LABEL_VALUE_GAP;
}
const auto style = field.bold ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR;
for (const auto& line : field.lines) {
if (y >= contentBottom) break;
if (y + lineH12 > contentTop) {
renderer.drawText(UI_12_FONT_ID, sidePad, y, line.c_str(), true, style);
}
y += lineH12;
}
y += SECTION_GAP;
}
const bool canScrollDown = scrollOffset + pageH < contentHeight;
const bool canScrollUp = scrollOffset > 0;
const char* downHint = canScrollDown ? tr(STR_DIR_DOWN) : "";
const char* upHint = canScrollUp ? tr(STR_DIR_UP) : "";
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", downHint, upHint);
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
std::string BookInfoActivity::formatFileSize(size_t bytes) {
char buf[32];
if (bytes < 1024) {
snprintf(buf, sizeof(buf), "%u B", static_cast<unsigned>(bytes));
} else if (bytes < 1024 * 1024) {
snprintf(buf, sizeof(buf), "%.1f KB", static_cast<float>(bytes) / 1024.0f);
} else {
snprintf(buf, sizeof(buf), "%.1f MB", static_cast<float>(bytes) / (1024.0f * 1024.0f));
}
return buf;
}