From 7e28af02d120d77625bc27e2c3c5a5aff7e63800 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Mon, 15 Dec 2025 19:39:07 +0300 Subject: [PATCH] Enhance TOC parsing and chapter selection logic - Update .gitignore to include additional paths - Refactor Epub::parseContentOpf to improve NCX item retrieval - Modify ContentOpfParser to store media type in ManifestItem - Implement rebuildVisibleSpineIndices in EpubReaderChapterSelectionScreen for better chapter navigation - Adjust rendering logic to handle empty chapter lists gracefully --- .gitignore | 2 + lib/Epub/Epub.cpp | 25 ++++- lib/Epub/Epub/parsers/ContentOpfParser.cpp | 13 ++- lib/Epub/Epub/parsers/ContentOpfParser.h | 9 +- .../EpubReaderChapterSelectionScreen.cpp | 103 +++++++++++++++--- .../EpubReaderChapterSelectionScreen.h | 3 + 6 files changed, 130 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 29bccdd..25b36fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .pio .idea .DS_Store +.vscode +lib/EpdFont/fontsrc diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index f9273a9..0f4b860 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -69,16 +69,31 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) { // Grab data from opfParser into epub title = opfParser.title; + constexpr const char* NCX_MEDIA_TYPE = "application/x-dtbncx+xml"; - if (opfParser.items.count("ncx")) { - tocNcxItem = opfParser.items.at("ncx"); - } else if (opfParser.items.count("ncxtoc")) { - tocNcxItem = opfParser.items.at("ncxtoc"); + for (const auto& [id, manifestItem] : opfParser.items) { + (void)id; + if (manifestItem.mediaType == NCX_MEDIA_TYPE) { + tocNcxItem = manifestItem.href; + break; + } + } + + if (tocNcxItem.empty() && !opfParser.tocNcxPath.empty()) { + tocNcxItem = opfParser.tocNcxPath; + } + + if (tocNcxItem.empty()) { + if (opfParser.items.count("ncx")) { + tocNcxItem = opfParser.items.at("ncx").href; + } else if (opfParser.items.count("ncxtoc")) { + tocNcxItem = opfParser.items.at("ncxtoc").href; + } } for (auto& spineRef : opfParser.spineRefs) { if (opfParser.items.count(spineRef)) { - spine.emplace_back(spineRef, opfParser.items.at(spineRef)); + spine.emplace_back(spineRef, opfParser.items.at(spineRef).href); } } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 1dcdb04..8c70b16 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -96,17 +96,24 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) { std::string itemId; - std::string href; + ManifestItem item; for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], "id") == 0) { itemId = atts[i + 1]; } else if (strcmp(atts[i], "href") == 0) { - href = self->baseContentPath + atts[i + 1]; + item.href = self->baseContentPath + atts[i + 1]; + } else if (strcmp(atts[i], "media-type") == 0) { + item.mediaType = atts[i + 1]; } } - self->items[itemId] = href; + if (!itemId.empty()) { + self->items[itemId] = item; + if (item.mediaType == "application/x-dtbncx+xml" && self->tocNcxPath.empty()) { + self->tocNcxPath = item.href; + } + } return; } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index 5da16bd..67000d9 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -2,6 +2,8 @@ #include #include +#include +#include #include "Epub.h" #include "expat.h" @@ -26,9 +28,14 @@ class ContentOpfParser final : public Print { static void endElement(void* userData, const XML_Char* name); public: + struct ManifestItem { + std::string href; + std::string mediaType; + }; + std::string title; std::string tocNcxPath; - std::map items; + std::map items; std::vector spineRefs; explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize) diff --git a/src/screens/EpubReaderChapterSelectionScreen.cpp b/src/screens/EpubReaderChapterSelectionScreen.cpp index 26688a4..99bff76 100644 --- a/src/screens/EpubReaderChapterSelectionScreen.cpp +++ b/src/screens/EpubReaderChapterSelectionScreen.cpp @@ -13,13 +13,41 @@ void EpubReaderChapterSelectionScreen::taskTrampoline(void* param) { self->displayTaskLoop(); } +void EpubReaderChapterSelectionScreen::rebuildVisibleSpineIndices() { + visibleSpineIndices.clear(); + if (!epub) { + return; + } + + const int spineCount = epub->getSpineItemsCount(); + visibleSpineIndices.reserve(spineCount); + for (int i = 0; i < spineCount; i++) { + if (epub->getTocIndexForSpineIndex(i) != -1) { + visibleSpineIndices.push_back(i); + } + } +} + void EpubReaderChapterSelectionScreen::onEnter() { if (!epub) { return; } renderingMutex = xSemaphoreCreateMutex(); - selectorIndex = currentSpineIndex; + rebuildVisibleSpineIndices(); + + selectorIndex = 0; + if (!visibleSpineIndices.empty()) { + for (size_t i = 0; i < visibleSpineIndices.size(); i++) { + if (visibleSpineIndices[i] == currentSpineIndex) { + selectorIndex = static_cast(i); + break; + } + } + if (selectorIndex >= static_cast(visibleSpineIndices.size())) { + selectorIndex = static_cast(visibleSpineIndices.size()) - 1; + } + } // Trigger first update updateRequired = true; @@ -40,6 +68,7 @@ void EpubReaderChapterSelectionScreen::onExit() { } vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; + visibleSpineIndices.clear(); } void EpubReaderChapterSelectionScreen::handleInput() { @@ -51,22 +80,53 @@ void EpubReaderChapterSelectionScreen::handleInput() { const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS; if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { - onSelectSpineIndex(selectorIndex); - } else if (inputManager.wasPressed(InputManager::BTN_BACK)) { + if (!visibleSpineIndices.empty()) { + if (selectorIndex >= static_cast(visibleSpineIndices.size())) { + selectorIndex = static_cast(visibleSpineIndices.size()) - 1; + } + onSelectSpineIndex(visibleSpineIndices[selectorIndex]); + } + return; + } + + if (inputManager.wasPressed(InputManager::BTN_BACK)) { onGoBack(); - } else if (prevReleased) { + return; + } + + const int chapterCount = static_cast(visibleSpineIndices.size()); + if (chapterCount == 0) { + return; + } + + if (selectorIndex >= chapterCount) { + selectorIndex = chapterCount - 1; + } + + if (prevReleased) { if (skipPage) { - selectorIndex = - ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + epub->getSpineItemsCount()) % epub->getSpineItemsCount(); + const int totalPages = (chapterCount + PAGE_ITEMS - 1) / PAGE_ITEMS; + int currentPage = selectorIndex / PAGE_ITEMS; + currentPage = (currentPage + totalPages - 1) % totalPages; + selectorIndex = currentPage * PAGE_ITEMS; + if (selectorIndex >= chapterCount) { + selectorIndex = chapterCount - 1; + } } else { - selectorIndex = (selectorIndex + epub->getSpineItemsCount() - 1) % epub->getSpineItemsCount(); + selectorIndex = (selectorIndex + chapterCount - 1) % chapterCount; } updateRequired = true; } else if (nextReleased) { if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % epub->getSpineItemsCount(); + const int totalPages = (chapterCount + PAGE_ITEMS - 1) / PAGE_ITEMS; + int currentPage = selectorIndex / PAGE_ITEMS; + currentPage = (currentPage + 1) % totalPages; + selectorIndex = currentPage * PAGE_ITEMS; + if (selectorIndex >= chapterCount) { + selectorIndex = chapterCount - 1; + } } else { - selectorIndex = (selectorIndex + 1) % epub->getSpineItemsCount(); + selectorIndex = (selectorIndex + 1) % chapterCount; } updateRequired = true; } @@ -90,17 +150,28 @@ void EpubReaderChapterSelectionScreen::renderScreen() { const auto pageWidth = renderer.getScreenWidth(); renderer.drawCenteredText(READER_FONT_ID, 10, "Select Chapter", true, BOLD); + const int chapterCount = static_cast(visibleSpineIndices.size()); + if (chapterCount == 0) { + renderer.drawText(UI_FONT_ID, 20, 60, "No chapters available"); + renderer.displayBuffer(); + return; + } + + if (selectorIndex >= chapterCount) { + selectorIndex = chapterCount - 1; + } + const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 + 2, pageWidth - 1, 30); - for (int i = pageStartIndex; i < epub->getSpineItemsCount() && i < pageStartIndex + PAGE_ITEMS; i++) { - const int tocIndex = epub->getTocIndexForSpineIndex(i); + for (int i = pageStartIndex; i < chapterCount && i < pageStartIndex + PAGE_ITEMS; i++) { + const int spineIndex = visibleSpineIndices[i]; + const int tocIndex = epub->getTocIndexForSpineIndex(spineIndex); if (tocIndex == -1) { - renderer.drawText(UI_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, "Unnamed", i != selectorIndex); - } else { - auto item = epub->getTocItem(tocIndex); - renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % PAGE_ITEMS) * 30, item.title.c_str(), - i != selectorIndex); + continue; // Filtered chapters should not reach here, but skip defensively. } + auto item = epub->getTocItem(tocIndex); + renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % PAGE_ITEMS) * 30, item.title.c_str(), + i != selectorIndex); } renderer.displayBuffer(); diff --git a/src/screens/EpubReaderChapterSelectionScreen.h b/src/screens/EpubReaderChapterSelectionScreen.h index 8cac4cb..0ba41f0 100644 --- a/src/screens/EpubReaderChapterSelectionScreen.h +++ b/src/screens/EpubReaderChapterSelectionScreen.h @@ -5,6 +5,7 @@ #include #include +#include #include "Screen.h" @@ -15,11 +16,13 @@ class EpubReaderChapterSelectionScreen final : public Screen { int currentSpineIndex = 0; int selectorIndex = 0; bool updateRequired = false; + std::vector visibleSpineIndices; const std::function onGoBack; const std::function onSelectSpineIndex; static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); + void rebuildVisibleSpineIndices(); void renderScreen(); public: