diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index c3a30a3b..1cf7e7e0 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -102,7 +102,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) { - self->state = IN_BOOK_TITLE; + // Only capture the first dc:title element; subsequent ones are subtitles + if (self->title.empty()) { + self->state = IN_BOOK_TITLE; + } return; } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 1306c217..b36afc52 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -85,7 +85,9 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const { LOG_DBG("RBS", "Loading recent book: %s", path.c_str()); - // If epub, try to load the metadata for title/author and cover + // If epub, try to load the metadata for title/author and cover. + // Use buildIfMissing=false to avoid heavy epub loading on boot; getTitle()/getAuthor() may be + // blank until the book is opened, and entries with missing title are omitted from recent list. if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { Epub epub(path, "/.crosspoint"); epub.load(false, true); @@ -112,40 +114,35 @@ bool RecentBooksStore::loadFromFile() { uint8_t version; serialization::readPod(inputFile, version); - if (version != RECENT_BOOKS_FILE_VERSION) { - if (version == 1 || version == 2) { - // Old version, just read paths - uint8_t count; - serialization::readPod(inputFile, count); - recentBooks.clear(); - recentBooks.reserve(count); - for (uint8_t i = 0; i < count; i++) { - std::string path; - serialization::readString(inputFile, path); + if (version == 1 || version == 2) { + // Old version, just read paths + uint8_t count; + serialization::readPod(inputFile, count); + recentBooks.clear(); + recentBooks.reserve(count); + for (uint8_t i = 0; i < count; i++) { + std::string path; + serialization::readString(inputFile, path); - // load book to get missing data - RecentBook book = getDataFromBook(path); - if (book.title.empty() && book.author.empty() && version == 2) { - // Fall back to loading what we can from the store - std::string title, author; - serialization::readString(inputFile, title); - serialization::readString(inputFile, author); - recentBooks.push_back({path, title, author, ""}); - } else { - recentBooks.push_back(book); - } + // load book to get missing data + RecentBook book = getDataFromBook(path); + if (book.title.empty() && book.author.empty() && version == 2) { + // Fall back to loading what we can from the store + std::string title, author; + serialization::readString(inputFile, title); + serialization::readString(inputFile, author); + recentBooks.push_back({path, title, author, ""}); + } else { + recentBooks.push_back(book); } - } else { - LOG_ERR("RBS", "Deserialization failed: Unknown version %u", version); - inputFile.close(); - return false; } - } else { + } else if (version == 3) { uint8_t count; serialization::readPod(inputFile, count); recentBooks.clear(); recentBooks.reserve(count); + uint8_t omitted = 0; for (uint8_t i = 0; i < count; i++) { std::string path, title, author, coverBmpPath; @@ -153,8 +150,26 @@ bool RecentBooksStore::loadFromFile() { serialization::readString(inputFile, title); serialization::readString(inputFile, author); serialization::readString(inputFile, coverBmpPath); + + // Omit books with missing title (e.g. saved before metadata was available) + if (title.empty()) { + omitted++; + continue; + } + recentBooks.push_back({path, title, author, coverBmpPath}); } + + if (omitted > 0) { + inputFile.close(); + saveToFile(); + LOG_DBG("RBS", "Omitted %u recent book(s) with missing title", omitted); + return true; + } + } else { + LOG_ERR("RBS", "Deserialization failed: Unknown version %u", version); + inputFile.close(); + return false; } inputFile.close(); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 399dc675..a93fe6b3 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -519,7 +519,8 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // Still have words left, so add ellipsis to last line lines.back().append("..."); - while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { + while (!lines.back().empty() && lines.back().size() > 3 && + renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { // Remove "..." first, then remove one UTF-8 char, then add "..." back lines.back().resize(lines.back().size() - 3); // Remove "..." utf8RemoveLastChar(lines.back()); @@ -540,17 +541,20 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: break; } } + if (i.empty()) continue; // Skip words that couldn't fit even truncated - int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str()); + int newLineWidth = renderer.getTextAdvanceX(UI_12_FONT_ID, currentLine.c_str(), EpdFontFamily::REGULAR); if (newLineWidth > 0) { newLineWidth += spaceWidth; } - newLineWidth += wordWidth; + newLineWidth += renderer.getTextAdvanceX(UI_12_FONT_ID, i.c_str(), EpdFontFamily::REGULAR); if (newLineWidth > maxLineWidth && !currentLine.empty()) { // New line too long, push old line lines.push_back(currentLine); currentLine = i; + } else if (currentLine.empty()) { + currentLine = i; } else { currentLine.append(" ").append(i); } diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 3f0898c4..fd8fdbe7 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include #include +#include #include "RecentBooksStore.h" #include "components/UITheme.h" @@ -483,13 +485,73 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: hPaddingInSelection, cornerRadius, false, false, true, true, Color::LightGray); } - auto title = renderer.truncatedText(UI_12_FONT_ID, book.title.c_str(), textWidth, EpdFontFamily::BOLD); + // Wrap title to up to 3 lines (word-wrap by advance width) + const std::string& lastBookTitle = book.title; + std::vector words; + words.reserve(8); + std::string::size_type wordStart = 0; + std::string::size_type wordEnd = 0; + // find_first_not_of skips leading/interstitial spaces + while ((wordStart = lastBookTitle.find_first_not_of(' ', wordEnd)) != std::string::npos) { + wordEnd = lastBookTitle.find(' ', wordStart); + if (wordEnd == std::string::npos) wordEnd = lastBookTitle.size(); + words.emplace_back(lastBookTitle.substr(wordStart, wordEnd - wordStart)); + } + const int maxLineWidth = textWidth; + const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID, EpdFontFamily::BOLD); + std::vector titleLines; + std::string currentLine; + for (auto& w : words) { + if (titleLines.size() >= 3) { + titleLines.back().append("..."); + while (!titleLines.back().empty() && titleLines.back().size() > 3 && + renderer.getTextWidth(UI_12_FONT_ID, titleLines.back().c_str(), EpdFontFamily::BOLD) > maxLineWidth) { + titleLines.back().resize(titleLines.back().size() - 3); + utf8RemoveLastChar(titleLines.back()); + titleLines.back().append("..."); + } + break; + } + int wordW = renderer.getTextWidth(UI_12_FONT_ID, w.c_str(), EpdFontFamily::BOLD); + while (wordW > maxLineWidth && !w.empty()) { + utf8RemoveLastChar(w); + std::string withE = w + "..."; + wordW = renderer.getTextWidth(UI_12_FONT_ID, withE.c_str(), EpdFontFamily::BOLD); + if (wordW <= maxLineWidth) { + w = withE; + break; + } + } + if (w.empty()) continue; // Skip words that couldn't fit even truncated + int newW = renderer.getTextAdvanceX(UI_12_FONT_ID, currentLine.c_str(), EpdFontFamily::BOLD); + if (newW > 0) newW += spaceWidth; + newW += renderer.getTextAdvanceX(UI_12_FONT_ID, w.c_str(), EpdFontFamily::BOLD); + if (newW > maxLineWidth && !currentLine.empty()) { + titleLines.push_back(currentLine); + currentLine = w; + } else if (currentLine.empty()) { + currentLine = w; + } else { + currentLine.append(" ").append(w); + } + } + if (!currentLine.empty() && titleLines.size() < 3) titleLines.push_back(currentLine); + auto author = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), textWidth); - auto bookTitleHeight = renderer.getTextHeight(UI_12_FONT_ID); - renderer.drawText(UI_12_FONT_ID, tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, - tileY + tileHeight / 2 - bookTitleHeight, title.c_str(), true, EpdFontFamily::BOLD); - renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, - tileY + tileHeight / 2 + 5, author.c_str(), true); + const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int titleBlockHeight = titleLineHeight * static_cast(titleLines.size()); + const int authorHeight = book.author.empty() ? 0 : (renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2); + const int totalBlockHeight = titleBlockHeight + authorHeight; + int titleY = tileY + tileHeight / 2 - totalBlockHeight / 2; + const int textX = tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing; + for (const auto& line : titleLines) { + renderer.drawText(UI_12_FONT_ID, textX, titleY, line.c_str(), true, EpdFontFamily::BOLD); + titleY += titleLineHeight; + } + if (!book.author.empty()) { + titleY += renderer.getLineHeight(UI_10_FONT_ID) / 2; + renderer.drawText(UI_10_FONT_ID, textX, titleY, author.c_str(), true); + } } else { drawEmptyRecents(renderer, rect); }