Files
crosspoint-reader-mod/src/activities/home/BookInfoActivity.cpp

352 lines
12 KiB
C++
Raw Normal View History

#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();
const auto orient = renderer.getOrientation();
isLandscape = orient == GfxRenderer::Orientation::LandscapeClockwise ||
orient == GfxRenderer::Orientation::LandscapeCounterClockwise;
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();
}
}
const auto& metrics = UITheme::getInstance().getMetrics();
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentBottom = renderer.getScreenHeight() - metrics.buttonHintsHeight - metrics.verticalSpacing;
const int coverH = isLandscape ? (contentBottom - contentTop) : (renderer.getScreenHeight() * 2 / 5);
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);
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();
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 pageW = renderer.getScreenWidth();
const int sidePad = metrics.contentSidePadding;
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();
}
}
coverPanelWidth = 0;
if (isLandscape && coverDisplayWidth > 0) {
coverPanelWidth = std::min(coverDisplayWidth + sidePad * 2, pageW * 2 / 5);
}
const int contentW = isLandscape && coverPanelWidth > 0
? pageW - coverPanelWidth - sidePad
: pageW - 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 (!isLandscape && coverDisplayHeight > 0 && !coverBmpPath.empty()) {
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;
if (isLandscape && coverPanelWidth > 0 && !coverBmpPath.empty() && coverDisplayHeight > 0) {
FsFile file;
if (Storage.openFileForRead("BIF", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
const int availH = contentBottom - contentTop;
const int coverX = (coverPanelWidth - coverDisplayWidth) / 2;
renderer.drawBitmap1Bit(bitmap, coverX, contentTop, coverDisplayWidth, availH);
}
file.close();
}
}
const int fieldX = (isLandscape && coverPanelWidth > 0) ? coverPanelWidth : sidePad;
int y = contentTop - scrollOffset;
if (!isLandscape && !coverBmpPath.empty() && coverDisplayHeight > 0) {
if (y + coverDisplayHeight > 0 && 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, coverDisplayHeight);
}
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, fieldX, 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, fieldX, y, line.c_str(), true, style);
}
y += lineH12;
}
y += SECTION_GAP;
}
renderer.fillRect(0, 0, pageW, contentTop, false);
GUI.drawHeader(renderer, Rect(0, metrics.topPadding, pageW, metrics.headerHeight), tr(STR_BOOK_INFO));
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;
}