#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(); } } std::string title, author, series, seriesIndex, description, language; 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); } title = epub.getTitle(); author = epub.getAuthor(); series = epub.getSeries(); seriesIndex = epub.getSeriesIndex(); description = normalizeWhitespace(epub.getDescription()); language = epub.getLanguage(); 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, title.empty() ? fileName : title, 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); } title = xtc.getTitle(); 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, title.empty() ? fileName : title, author, thumbW, coverH)) { coverBmpPath = placeholderPath; } } } if (needsBuild) { GUI.fillPopupProgress(renderer, popupRect, 100); } } if (title.empty()) { title = fileName; } buildLayout(title, author, series, seriesIndex, description, language, fileSize); requestUpdate(); } void BookInfoActivity::onExit() { Activity::onExit(); } void BookInfoActivity::buildLayout(const std::string& title, const std::string& author, const std::string& series, const std::string& seriesIndex, const std::string& description, const std::string& language, size_t fileSize) { const int contentW = renderer.getScreenWidth() - MARGIN * 2; fields.reserve(6); 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, title, true, EpdFontFamily::BOLD); addField(tr(STR_AUTHOR), author, false, EpdFontFamily::REGULAR); if (!series.empty()) { std::string seriesStr = series; if (!seriesIndex.empty()) { seriesStr += " #" + seriesIndex; } addField(tr(STR_SERIES), seriesStr, false, EpdFontFamily::REGULAR); } addField(tr(STR_LANGUAGE), language, false, EpdFontFamily::REGULAR); if (fileSize > 0) { addField(tr(STR_FILE_SIZE), formatFileSize(fileSize), false, EpdFontFamily::REGULAR); } addField(tr(STR_DESCRIPTION), 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; }