From 2010b33e5d53646c819bd4206598dbe36ad25a9b Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 8 Mar 2026 11:03:50 +0100 Subject: [PATCH 1/6] Review amendments --- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 30 +++++++++++++++------- lib/Serialization/Serialization.h | 12 ++++++--- src/RecentBooksStore.cpp | 26 +++++++++++++------ src/RecentBooksStore.h | 2 +- src/activities/home/HomeActivity.cpp | 4 +-- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 58ea3225..5651d5ab 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -21,9 +21,8 @@ std::string stripHtml(const std::string& html) { for (size_t i = 0; i < html.size(); ++i) { const char c = html[i]; if (c == '<') { - // Only treat as a tag if followed by a tag-like character; otherwise keep literal '<' - size_t j = i + 1; - while (j < html.size() && html[j] == ' ') ++j; + // Only treat as a tag if immediately followed (no space skip) by a tag-like character + const size_t j = i + 1; if (j < html.size() && (isalpha(static_cast(html[j])) || html[j] == '/' || html[j] == '!' || html[j] == '?')) { inTag = true; @@ -33,7 +32,11 @@ std::string stripHtml(const std::string& html) { result += c; } } else if (c == '>') { - inTag = false; + if (inTag) { + inTag = false; + } else { + result += c; + } } else if (!inTag) { if (c == '&') { // Decode common HTML entities not covered by Expat @@ -196,7 +199,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } if (self->state == IN_METADATA && strcmp(name, "dc:description") == 0) { - self->state = IN_BOOK_DESCRIPTION; + // Only capture the first dc:description element; subsequent ones are alternate/localized variants + if (self->description.empty()) { + self->state = IN_BOOK_DESCRIPTION; + } return; } @@ -254,9 +260,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (strcmp(metaName, "cover") == 0) { self->coverItemId = metaContent; } else if (strcmp(metaName, "calibre:series") == 0 && self->series.empty()) { - self->series = metaContent; + self->series = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); } else if (strcmp(metaName, "calibre:series_index") == 0 && self->seriesIndex.empty()) { - self->seriesIndex = metaContent; + self->seriesIndex = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); } } @@ -457,12 +463,18 @@ void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, } if (self->state == IN_BOOK_SERIES) { - self->series.append(s, len); + if (self->series.size() < MAX_DESCRIPTION_LENGTH) { + const size_t remaining = MAX_DESCRIPTION_LENGTH - self->series.size(); + self->series.append(s, std::min(static_cast(len), remaining)); + } return; } if (self->state == IN_BOOK_SERIES_INDEX) { - self->seriesIndex.append(s, len); + if (self->seriesIndex.size() < MAX_DESCRIPTION_LENGTH) { + const size_t remaining = MAX_DESCRIPTION_LENGTH - self->seriesIndex.size(); + self->seriesIndex.append(s, std::min(static_cast(len), remaining)); + } return; } } diff --git a/lib/Serialization/Serialization.h b/lib/Serialization/Serialization.h index 1308822f..37df1c83 100644 --- a/lib/Serialization/Serialization.h +++ b/lib/Serialization/Serialization.h @@ -36,17 +36,23 @@ static void writeString(FsFile& file, const std::string& s) { file.write(reinterpret_cast(s.data()), len); } -static void readString(std::istream& is, std::string& s) { +constexpr uint32_t MAX_STRING_LENGTH = 4096; + +static bool readString(std::istream& is, std::string& s) { uint32_t len; readPod(is, len); + if (len > MAX_STRING_LENGTH) return false; s.resize(len); is.read(&s[0], len); + return true; } -static void readString(FsFile& file, std::string& s) { +static bool readString(FsFile& file, std::string& s) { uint32_t len; readPod(file, len); + if (len > MAX_STRING_LENGTH) return false; s.resize(len); - file.read(&s[0], len); + file.read(reinterpret_cast(&s[0]), len); + return true; } } // namespace serialization diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 18a98c43..a013db7a 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -41,13 +41,14 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title } void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, - const std::string& coverBmpPath) { + const std::string& series, const std::string& coverBmpPath) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it != recentBooks.end()) { RecentBook& book = *it; book.title = title; book.author = author; + book.series = series; book.coverBmpPath = coverBmpPath; saveToFile(); } @@ -125,15 +126,22 @@ bool RecentBooksStore::loadFromBinaryFile() { recentBooks.reserve(count); for (uint8_t i = 0; i < count; i++) { std::string path; - serialization::readString(inputFile, path); + if (!serialization::readString(inputFile, path)) { + LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); + inputFile.close(); + return false; + } // 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); + if (!serialization::readString(inputFile, title) || !serialization::readString(inputFile, author)) { + LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); + inputFile.close(); + return false; + } recentBooks.push_back({path, title, author, "", ""}); } else { recentBooks.push_back(book); @@ -149,10 +157,12 @@ bool RecentBooksStore::loadFromBinaryFile() { for (uint8_t i = 0; i < count; i++) { std::string path, title, author, coverBmpPath; - serialization::readString(inputFile, path); - serialization::readString(inputFile, title); - serialization::readString(inputFile, author); - serialization::readString(inputFile, coverBmpPath); + if (!serialization::readString(inputFile, path) || !serialization::readString(inputFile, title) || + !serialization::readString(inputFile, author) || !serialization::readString(inputFile, coverBmpPath)) { + LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); + inputFile.close(); + return false; + } // Omit books with missing title (e.g. saved before metadata was available) if (title.empty()) { diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 100bff0d..bd7ad16d 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -36,7 +36,7 @@ class RecentBooksStore { const std::string& coverBmpPath); void updateBook(const std::string& path, const std::string& title, const std::string& author, - const std::string& coverBmpPath); + const std::string& series, const std::string& coverBmpPath); // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 810cb50b..bc2180fa 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -74,7 +74,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); bool success = epub.generateThumbBmp(coverHeight); if (!success) { - RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); + RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); book.coverBmpPath = ""; } coverRendered = false; @@ -91,7 +91,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); bool success = xtc.generateThumbBmp(coverHeight); if (!success) { - RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); + RECENT_BOOKS.updateBook(book.path, book.title, book.author, book.series, ""); book.coverBmpPath = ""; } coverRendered = false; From 42a01446efc16128587c25b5dddd8def4a431404 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 8 Mar 2026 11:27:52 +0100 Subject: [PATCH 2/6] More review changes --- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 21 ++++++++++++++------- lib/Serialization/Serialization.h | 10 ++++++++-- src/RecentBooksStore.cpp | 16 +++++++++------- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 5651d5ab..4818558e 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -267,18 +267,25 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } // EPUB 3 collection metadata: - // Series Name + // Series Name (character data) + // (attribute, some generators) // 1 if (metaProperty) { if (strcmp(metaProperty, "belongs-to-collection") == 0 && self->series.empty()) { - self->series.clear(); - self->state = IN_BOOK_SERIES; - return; + if (metaContent) { + self->series = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); + } else { + self->state = IN_BOOK_SERIES; + return; + } } if (strcmp(metaProperty, "group-position") == 0 && self->seriesIndex.empty()) { - self->seriesIndex.clear(); - self->state = IN_BOOK_SERIES_INDEX; - return; + if (metaContent) { + self->seriesIndex = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); + } else { + self->state = IN_BOOK_SERIES_INDEX; + return; + } } } diff --git a/lib/Serialization/Serialization.h b/lib/Serialization/Serialization.h index 37df1c83..c1be4982 100644 --- a/lib/Serialization/Serialization.h +++ b/lib/Serialization/Serialization.h @@ -41,7 +41,10 @@ constexpr uint32_t MAX_STRING_LENGTH = 4096; static bool readString(std::istream& is, std::string& s) { uint32_t len; readPod(is, len); - if (len > MAX_STRING_LENGTH) return false; + if (len > MAX_STRING_LENGTH) { + is.seekg(len, std::ios::cur); // skip payload to keep stream aligned + return false; + } s.resize(len); is.read(&s[0], len); return true; @@ -50,7 +53,10 @@ static bool readString(std::istream& is, std::string& s) { static bool readString(FsFile& file, std::string& s) { uint32_t len; readPod(file, len); - if (len > MAX_STRING_LENGTH) return false; + if (len > MAX_STRING_LENGTH) { + file.seekCur(len); // skip payload to keep file position aligned + return false; + } s.resize(len); file.read(reinterpret_cast(&s[0]), len); return true; diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index a013db7a..53a0f333 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -122,8 +122,8 @@ bool RecentBooksStore::loadFromBinaryFile() { // Old version, just read paths uint8_t count; serialization::readPod(inputFile, count); - recentBooks.clear(); - recentBooks.reserve(count); + std::vector tmpRecentBooks; + tmpRecentBooks.reserve(count); for (uint8_t i = 0; i < count; i++) { std::string path; if (!serialization::readString(inputFile, path)) { @@ -142,17 +142,18 @@ bool RecentBooksStore::loadFromBinaryFile() { inputFile.close(); return false; } - recentBooks.push_back({path, title, author, "", ""}); + tmpRecentBooks.push_back({path, title, author, "", ""}); } else { - recentBooks.push_back(book); + tmpRecentBooks.push_back(book); } } + recentBooks = std::move(tmpRecentBooks); } else if (version == 3) { uint8_t count; serialization::readPod(inputFile, count); - recentBooks.clear(); - recentBooks.reserve(count); + std::vector tmpRecentBooks; + tmpRecentBooks.reserve(count); uint8_t omitted = 0; for (uint8_t i = 0; i < count; i++) { @@ -170,8 +171,9 @@ bool RecentBooksStore::loadFromBinaryFile() { continue; } - recentBooks.push_back({path, title, author, "", coverBmpPath}); + tmpRecentBooks.push_back({path, title, author, "", coverBmpPath}); } + recentBooks = std::move(tmpRecentBooks); if (omitted > 0) { inputFile.close(); From 9182a0567057eebf551b4f286da5af6c492a4ea7 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 8 Mar 2026 12:14:18 +0100 Subject: [PATCH 3/6] And more... --- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 8 ++++---- lib/Serialization/Serialization.h | 4 +++- src/RecentBooksStore.cpp | 6 ++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 4818558e..8ff97d80 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -260,9 +260,9 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (strcmp(metaName, "cover") == 0) { self->coverItemId = metaContent; } else if (strcmp(metaName, "calibre:series") == 0 && self->series.empty()) { - self->series = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); + self->series = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); } else if (strcmp(metaName, "calibre:series_index") == 0 && self->seriesIndex.empty()) { - self->seriesIndex = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); + self->seriesIndex = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); } } @@ -273,7 +273,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (metaProperty) { if (strcmp(metaProperty, "belongs-to-collection") == 0 && self->series.empty()) { if (metaContent) { - self->series = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); + self->series = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); } else { self->state = IN_BOOK_SERIES; return; @@ -281,7 +281,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } if (strcmp(metaProperty, "group-position") == 0 && self->seriesIndex.empty()) { if (metaContent) { - self->seriesIndex = std::string(metaContent).substr(0, MAX_DESCRIPTION_LENGTH); + self->seriesIndex = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); } else { self->state = IN_BOOK_SERIES_INDEX; return; diff --git a/lib/Serialization/Serialization.h b/lib/Serialization/Serialization.h index c1be4982..e5a885cc 100644 --- a/lib/Serialization/Serialization.h +++ b/lib/Serialization/Serialization.h @@ -54,7 +54,9 @@ static bool readString(FsFile& file, std::string& s) { uint32_t len; readPod(file, len); if (len > MAX_STRING_LENGTH) { - file.seekCur(len); // skip payload to keep file position aligned + if (!file.seekCur(static_cast(len))) { // skip payload to keep file position aligned + return false; + } return false; } s.resize(len); diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 53a0f333..ff811cd8 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -142,8 +142,10 @@ bool RecentBooksStore::loadFromBinaryFile() { inputFile.close(); return false; } - tmpRecentBooks.push_back({path, title, author, "", ""}); - } else { + if (!title.empty()) { + tmpRecentBooks.push_back({path, title, author, "", ""}); + } + } else if (!book.title.empty()) { tmpRecentBooks.push_back(book); } } From 98fb3d65c9bf36765ee2063e6a7126bd0cd7f7f4 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 8 Mar 2026 12:45:14 +0100 Subject: [PATCH 4/6] And even more... --- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 10 ++++++---- src/RecentBooksStore.cpp | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 8ff97d80..963d4fd0 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -260,9 +260,10 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (strcmp(metaName, "cover") == 0) { self->coverItemId = metaContent; } else if (strcmp(metaName, "calibre:series") == 0 && self->series.empty()) { - self->series = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); + self->series = trim(std::string(metaContent, std::min(strlen(metaContent), size_t{MAX_DESCRIPTION_LENGTH}))); } else if (strcmp(metaName, "calibre:series_index") == 0 && self->seriesIndex.empty()) { - self->seriesIndex = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); + self->seriesIndex = + trim(std::string(metaContent, std::min(strlen(metaContent), size_t{MAX_DESCRIPTION_LENGTH}))); } } @@ -273,7 +274,7 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (metaProperty) { if (strcmp(metaProperty, "belongs-to-collection") == 0 && self->series.empty()) { if (metaContent) { - self->series = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); + self->series = trim(std::string(metaContent, std::min(strlen(metaContent), size_t{MAX_DESCRIPTION_LENGTH}))); } else { self->state = IN_BOOK_SERIES; return; @@ -281,7 +282,8 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } if (strcmp(metaProperty, "group-position") == 0 && self->seriesIndex.empty()) { if (metaContent) { - self->seriesIndex = trim(std::string(metaContent)).substr(0, MAX_DESCRIPTION_LENGTH); + self->seriesIndex = + trim(std::string(metaContent, std::min(strlen(metaContent), size_t{MAX_DESCRIPTION_LENGTH}))); } else { self->state = IN_BOOK_SERIES_INDEX; return; diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index ff811cd8..5746cbe7 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -134,19 +134,26 @@ bool RecentBooksStore::loadFromBinaryFile() { // 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; - if (!serialization::readString(inputFile, title) || !serialization::readString(inputFile, author)) { + if (version == 2) { + // v2 always stores title and author after path; consume them regardless + // of whether live metadata was found, to keep the stream aligned. + std::string storedTitle, storedAuthor; + if (!serialization::readString(inputFile, storedTitle) || !serialization::readString(inputFile, storedAuthor)) { LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); inputFile.close(); return false; } + // Prefer live metadata; fall back to stored when live is unavailable. + const std::string& title = !book.title.empty() ? book.title : storedTitle; + const std::string& author = !book.title.empty() ? book.author : storedAuthor; if (!title.empty()) { tmpRecentBooks.push_back({path, title, author, "", ""}); } - } else if (!book.title.empty()) { - tmpRecentBooks.push_back(book); + } else { + // v1: no stored title/author bytes + if (!book.title.empty()) { + tmpRecentBooks.push_back(book); + } } } recentBooks = std::move(tmpRecentBooks); From 8dc82f28af007110b36905d39ec3fbe7c169fedf Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 8 Mar 2026 17:44:10 +0100 Subject: [PATCH 5/6] Use series in recents if present --- src/activities/home/RecentBooksActivity.cpp | 2 +- src/components/themes/BaseTheme.cpp | 6 +++++- src/components/themes/lyra/LyraTheme.cpp | 14 +++++++++++--- src/components/themes/lyra/LyraTheme.h | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 8f9c2e61..2b77fc7b 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -103,7 +103,7 @@ void RecentBooksActivity::render(RenderLock&&) { [this](int index) { return recentBooks[index].title; }, [this](int index) { const auto& book = recentBooks[index]; - if (!book.author.empty() && !book.series.empty()) return book.author + " \u2022 " + book.series; + if (!book.author.empty() && !book.series.empty()) return book.author + "\n" + book.series; if (!book.series.empty()) return book.series; return book.author; }, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 0346ce28..f94cba1f 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -235,8 +235,12 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex); if (rowSubtitle != nullptr) { - // Draw subtitle + // Draw subtitle; if the text is newline-separated (author\nseries), join with • for single-line display std::string subtitleText = rowSubtitle(i); + const auto nl = subtitleText.find('\n'); + if (nl != std::string::npos) { + subtitleText = subtitleText.substr(0, nl) + " \u2022 " + subtitleText.substr(nl + 1); + } auto subtitle = renderer.truncatedText(UI_10_FONT_ID, subtitleText.c_str(), textWidth); renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY + 30, subtitle.c_str(), i != selectedIndex); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index a8bbd837..ad97f6b1 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -318,10 +318,18 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } if (rowSubtitle != nullptr) { - // Draw subtitle std::string subtitleText = rowSubtitle(i); - auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth); - renderer.drawText(SMALL_FONT_ID, textX, itemY + 30, subtitle.c_str(), true); + const auto nl = subtitleText.find('\n'); + if (nl != std::string::npos) { + // Two-line subtitle: first line (author) at +24, second line (series) at +40 + auto line1 = renderer.truncatedText(SMALL_FONT_ID, subtitleText.substr(0, nl).c_str(), rowTextWidth); + renderer.drawText(SMALL_FONT_ID, textX, itemY + 24, line1.c_str(), true); + auto line2 = renderer.truncatedText(SMALL_FONT_ID, subtitleText.substr(nl + 1).c_str(), rowTextWidth); + renderer.drawText(SMALL_FONT_ID, textX, itemY + 40, line2.c_str(), true); + } else { + auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth); + renderer.drawText(SMALL_FONT_ID, textX, itemY + 30, subtitle.c_str(), true); + } } // Draw value diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index 1e953a81..db162f18 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -14,7 +14,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .verticalSpacing = 16, .contentSidePadding = 20, .listRowHeight = 40, - .listWithSubtitleRowHeight = 60, + .listWithSubtitleRowHeight = 68, .menuRowHeight = 64, .menuSpacing = 8, .tabSpacing = 8, From fd71e8780089a2fe7ed1df5f0123db0115c76b83 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 8 Mar 2026 17:58:20 +0100 Subject: [PATCH 6/6] Add info option to recents, too --- src/activities/home/RecentBooksActivity.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 2b77fc7b..55f41ad0 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -1,11 +1,13 @@ #include "RecentBooksActivity.h" +#include #include #include #include #include +#include "BookInfoActivity.h" #include "MappedInputManager.h" #include "RecentBooksStore.h" #include "components/UITheme.h" @@ -59,6 +61,17 @@ void RecentBooksActivity::loop() { onGoHome(); } + if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { + if (!recentBooks.empty() && selectorIndex < static_cast(recentBooks.size())) { + const std::string& path = recentBooks[selectorIndex].path; + if (FsHelpers::hasEpubExtension(path) || FsHelpers::hasXtcExtension(path)) { + startActivityForResult(std::make_unique(renderer, mappedInput, path), + [this](const ActivityResult&) { requestUpdate(); }); + return; + } + } + } + int listSize = static_cast(recentBooks.size()); buttonNavigator.onNextRelease([this, listSize] { @@ -111,8 +124,14 @@ void RecentBooksActivity::render(RenderLock&&) { } // Help text - const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + const bool hasInfo = !recentBooks.empty() && selectorIndex < recentBooks.size() && + (FsHelpers::hasEpubExtension(recentBooks[selectorIndex].path) || + FsHelpers::hasXtcExtension(recentBooks[selectorIndex].path)); + const auto labels = mappedInput.mapLabels(tr(STR_HOME), tr(STR_OPEN), "", hasInfo ? tr(STR_INFO) : ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Side buttons (Up/Down) navigate; show their hints on the side + GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); renderer.displayBuffer(); }