#include "ReaderActivity.h" #include #include #include #include #include #include #include "CrossPointSettings.h" #include "CrossPointState.h" #include "Epub.h" #include "EpubReaderActivity.h" #include "KOReaderCredentialStore.h" #include "MdReaderActivity.h" #include "Txt.h" #include "TxtReaderActivity.h" #include "Xtc.h" #include "XtcReaderActivity.h" #include "activities/util/BmpViewerActivity.h" #include "activities/util/FullScreenMessageActivity.h" #include "components/UITheme.h" #include "fontIds.h" #ifndef DEBUG_MEMORY_CONSUMPTION #define DEBUG_MEMORY_CONSUMPTION 0 #endif namespace { #if DEBUG_MEMORY_CONSUMPTION void logReaderLaunchMemSnapshot(const char* stage) { const uint32_t freeHeap = esp_get_free_heap_size(); const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); LOG_DBG("READER", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap); } #else inline void logReaderLaunchMemSnapshot(const char*) {} #endif } // namespace std::string ReaderActivity::extractFolderPath(const std::string& filePath) { const auto lastSlash = filePath.find_last_of('/'); if (lastSlash == std::string::npos || lastSlash == 0) { return "/"; } return filePath.substr(0, lastSlash); } bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasXtcExtension(path); } bool ReaderActivity::isTxtFile(const std::string& path) { return FsHelpers::hasTxtExtension(path); } bool ReaderActivity::isMdFile(const std::string& path) { return FsHelpers::hasMarkdownExtension(path); } bool ReaderActivity::isImageFile(const std::string& path) { return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path); } std::string ReaderActivity::sidecarCoverPath(const std::string& bookPath) { const auto dot = bookPath.rfind('.'); if (dot == std::string::npos) return ""; const std::string base = bookPath.substr(0, dot); for (const char* ext : {".jpg", ".jpeg", ".png", ".bmp"}) { const std::string candidate = base + ext; if (Storage.exists(candidate.c_str())) { LOG_DBG("SIDECAR", "Found sidecar cover: %s", candidate.c_str()); return candidate; } } return ""; } std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { if (!Storage.exists(path.c_str())) { LOG_ERR("READER", "File does not exist: %s", path.c_str()); return nullptr; } auto epub = std::unique_ptr(new Epub(path, "/.crosspoint")); epub->setSyntheticTocFallbackEnabled(SETTINGS.syntheticTocFallback != 0); if (epub->load(true, SETTINGS.embeddedStyle == 0)) { return epub; } LOG_ERR("READER", "Failed to load epub"); return nullptr; } std::unique_ptr ReaderActivity::loadXtc(const std::string& path) { if (!Storage.exists(path.c_str())) { LOG_ERR("READER", "File does not exist: %s", path.c_str()); return nullptr; } auto xtc = std::unique_ptr(new Xtc(path, "/.crosspoint")); if (xtc->load()) { return xtc; } LOG_ERR("READER", "Failed to load XTC"); return nullptr; } std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { if (!Storage.exists(path.c_str())) { LOG_ERR("READER", "File does not exist: %s", path.c_str()); return nullptr; } auto txt = std::unique_ptr(new Txt(path, "/.crosspoint")); if (txt->load()) { return txt; } LOG_ERR("READER", "Failed to load TXT"); return nullptr; } void ReaderActivity::goToLibrary(const std::string& fromBookPath) { // If coming from a book, start in that book's folder; otherwise start from root auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); activityManager.goToFileBrowser(std::move(initialPath)); } void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { const auto epubPath = epub->getPath(); currentBookPath = epubPath; // Long-press Confirm on RecentBooks/FileBrowser sets autoPullEpubPath so the user can // ask for KOReader sync at open time. Route into the sync activity instead of creating the // reader; sync's resumeReader() will create the reader once the remote position is applied. // Pull-only mode does not need accurate local reader state, so we hand off zeros for spine/page. auto& sync = APP_STATE.koReaderSyncSession; if (!sync.autoPullEpubPath.empty() && sync.autoPullEpubPath == epubPath && KOREADER_STORE.hasCredentials()) { LOG_DBG("READER", "AUTO_PULL on open: %s", epubPath.c_str()); sync.autoPullEpubPath.clear(); // consume the flag sync.active = true; sync.epubPath = epubPath; sync.spineIndex = 0; sync.page = 0; sync.totalPagesInSpine = 0; sync.paragraphIndex = 0; sync.hasParagraphIndex = false; sync.xhtmlSeekHint = 0; sync.intent = KOReaderSyncIntentState::AUTO_PULL; sync.outcome = KOReaderSyncOutcomeState::PENDING; sync.resultSpineIndex = 0; sync.resultPage = 0; sync.resultParagraphIndex = 0; sync.resultHasParagraphIndex = false; sync.exitToHomeAfterSync = false; APP_STATE.saveToFile(); // Drop the loaded Epub before TLS — sync activity will reload it for remote-position // mapping. Holding it here would needlessly inflate the heap during WiFi/TLS work. epub.reset(); activityManager.goToKOReaderSync(); return; } logReaderLaunchMemSnapshot("before_replace_epub_reader"); activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(epub))); } void ReaderActivity::onGoToBmpViewer(const std::string& path) { activityManager.replaceActivity(std::make_unique(renderer, mappedInput, path)); } void ReaderActivity::onGoToXtcReader(std::unique_ptr xtc) { const auto xtcPath = xtc->getPath(); currentBookPath = xtcPath; activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(xtc))); } void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { const auto txtPath = txt->getPath(); currentBookPath = txtPath; activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(txt))); } void ReaderActivity::onGoToMdReader(std::unique_ptr txt) { const auto txtPath = txt->getPath(); currentBookPath = txtPath; activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(txt))); } void ReaderActivity::onEnter() { Activity::onEnter(); logReaderLaunchMemSnapshot("onEnter_begin"); if (initialBookPath.empty()) { goToLibrary(); // Start from root when entering via Browse return; } if (APP_STATE.koReaderSyncSession.active && APP_STATE.koReaderSyncSession.epubPath == initialBookPath) { LOG_DBG("READER", "Opening EPUB with pending KOReader sync outcome=%d", static_cast(APP_STATE.koReaderSyncSession.outcome)); } currentBookPath = initialBookPath; if (isImageFile(initialBookPath)) { onGoToBmpViewer(initialBookPath); } else if (isXtcFile(initialBookPath)) { { RenderLock lock(*this); renderer.clearScreen(); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_LOADING), true, EpdFontFamily::BOLD); renderer.displayBuffer(); } auto xtc = loadXtc(initialBookPath); if (!xtc) { onGoBack(); return; } onGoToXtcReader(std::move(xtc)); } else if (isMdFile(initialBookPath)) { auto txt = loadTxt(initialBookPath); if (!txt) { onGoBack(); return; } onGoToMdReader(std::move(txt)); } else if (isTxtFile(initialBookPath)) { auto txt = loadTxt(initialBookPath); if (!txt) { onGoBack(); return; } onGoToTxtReader(std::move(txt)); } else { auto epub = loadEpub(initialBookPath); if (!epub) { onGoBack(); return; } onGoToEpubReader(std::move(epub)); } } void ReaderActivity::onGoBack() { finish(); }