#include "BookInfoActivity.h" #include #include #include #include #include #include #include #include #include #include #include "components/UITheme.h" #include "fontIds.h" namespace { constexpr int MARGIN = 20; 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(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(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 int contentW = renderer.getScreenWidth() - MARGIN * 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); int h = MARGIN; 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(field.lines.size()) * lineH12; h += SECTION_GAP; } 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 int pageH = renderer.getScreenHeight(); const int lineH10 = renderer.getLineHeight(UI_10_FONT_ID); const int lineH12 = renderer.getLineHeight(UI_12_FONT_ID); int y = MARGIN - scrollOffset; // Cover image — only draw if at least partially visible if (!coverBmpPath.empty() && coverDisplayHeight > 0) { if (y + coverDisplayHeight > 0 && y < pageH) { FsFile file; if (Storage.openFileForRead("BIF", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { const int coverX = (renderer.getScreenWidth() - coverDisplayWidth) / 2; renderer.drawBitmap1Bit(bitmap, coverX, y, coverDisplayWidth, std::min(coverDisplayHeight, pageH - y)); } file.close(); } } y += coverDisplayHeight + COVER_GAP; } for (const auto& field : fields) { if (y >= pageH) break; if (field.label) { if (y + lineH10 > 0 && y < pageH) { renderer.drawText(UI_10_FONT_ID, MARGIN, 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 >= pageH) break; if (y + lineH12 > 0) { renderer.drawText(UI_12_FONT_ID, MARGIN, 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(bytes)); } else if (bytes < 1024 * 1024) { snprintf(buf, sizeof(buf), "%.1f KB", static_cast(bytes) / 1024.0f); } else { snprintf(buf, sizeof(buf), "%.1f MB", static_cast(bytes) / (1024.0f * 1024.0f)); } return buf; }