diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp index 616b094b..352e2c26 100644 --- a/lib/FsHelpers/FsHelpers.cpp +++ b/lib/FsHelpers/FsHelpers.cpp @@ -86,4 +86,22 @@ std::string extractFolderPath(const std::string& filePath) { return filePath.substr(0, lastSlash); } +void sanitizePathComponentForFat32(const char* input, char* output, size_t maxLen) { + if (maxLen == 0) { + return; + } + + size_t i = 0; + for (; i < maxLen - 1 && input[i] != '\0'; i++) { + const char c = input[i]; + if (c == '\\' || c == '/' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' || + c == ' ' || (c > 0x00 && c <= 0x1f)) { + output[i] = '-'; + } else { + output[i] = c; + } + } + output[i] = '\0'; +} + } // namespace FsHelpers diff --git a/lib/FsHelpers/FsHelpers.h b/lib/FsHelpers/FsHelpers.h index f8af636a..b70a3dc8 100644 --- a/lib/FsHelpers/FsHelpers.h +++ b/lib/FsHelpers/FsHelpers.h @@ -57,4 +57,10 @@ bool hasMarkdownExtension(std::string_view fileName); std::string extractFolderPath(const std::string& filePath); +/** + * Sanitize a filename/path component for FAT32 in a caller-provided buffer. + * Replaces invalid path characters, spaces, and control characters with '-'. + */ +void sanitizePathComponentForFat32(const char* input, char* output, size_t maxLen); + } // namespace FsHelpers diff --git a/src/activities/Activity.h b/src/activities/Activity.h index cc3fe443..33a44952 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -11,6 +11,7 @@ #include "GfxRenderer.h" #include "MappedInputManager.h" #include "RenderLock.h" +#include "util/ScreenshotInfo.h" class Activity { friend class ActivityManager; @@ -43,6 +44,7 @@ class Activity { virtual bool skipLoopDelay() { return false; } virtual bool preventAutoSleep() { return false; } virtual bool isReaderActivity() const { return false; } + virtual ScreenshotInfo getScreenshotInfo() const { return {}; } // Start a new activity without destroying the current one // Note: requestUpdate() will be invoked automatically once resultHandler finishes diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index 915f956e..e1d5dd21 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -234,6 +234,13 @@ bool ActivityManager::isReaderActivity() const { return currentActivity && curre bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } +ScreenshotInfo ActivityManager::getScreenshotInfo() const { + if (currentActivity) { + return currentActivity->getScreenshotInfo(); + } + return {}; +} + void ActivityManager::requestUpdate(bool immediate) { if (immediate) { if (renderTaskHandle) { diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index bc975e91..a8d737b8 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -11,6 +11,7 @@ #include "GfxRenderer.h" #include "MappedInputManager.h" +#include "util/ScreenshotInfo.h" class Activity; // forward declaration class RenderLock; // forward declaration @@ -99,6 +100,7 @@ class ActivityManager { bool preventAutoSleep() const; bool isReaderActivity() const; bool skipLoopDelay() const; + ScreenshotInfo getScreenshotInfo() const; // If immediate is true, the update will be triggered immediately. // Otherwise, it will be deferred until the end of the current loop iteration. diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5b4d668c..8415020a 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -928,3 +928,24 @@ void EpubReaderActivity::restoreSavedPosition() { } requestUpdate(); } + +ScreenshotInfo EpubReaderActivity::getScreenshotInfo() const { + ScreenshotInfo info; + info.readerType = ScreenshotInfo::ReaderType::Epub; + if (epub) { + snprintf(info.title, sizeof(info.title), "%s", epub->getTitle().c_str()); + info.spineIndex = currentSpineIndex; + } + if (section) { + info.currentPage = section->currentPage + 1; + info.totalPages = section->pageCount; + if (epub && epub->getBookSize() > 0 && section->pageCount > 0) { + const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); + int pct = static_cast(epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f + 0.5f); + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + info.progressPercent = pct; + } + } + return info; +} diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index d786ffed..76a6df26 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -65,4 +65,5 @@ class EpubReaderActivity final : public Activity { void loop() override; void render(RenderLock&& lock) override; bool isReaderActivity() const override { return true; } + ScreenshotInfo getScreenshotInfo() const override; }; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 597d52fc..30886e83 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -546,3 +546,17 @@ void TxtReaderActivity::savePageIndexCache() const { LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages); } + +ScreenshotInfo TxtReaderActivity::getScreenshotInfo() const { + ScreenshotInfo info; + info.readerType = ScreenshotInfo::ReaderType::Txt; + if (txt) { + const std::string t = txt->getTitle(); + snprintf(info.title, sizeof(info.title), "%s", t.c_str()); + } + info.currentPage = currentPage + 1; + info.totalPages = totalPages; + info.progressPercent = totalPages > 0 ? static_cast((currentPage + 1) * 100.0f / totalPages + 0.5f) : 0; + if (info.progressPercent > 100) info.progressPercent = 100; + return info; +} diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 45877a8e..b5c8b0d9 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -49,4 +49,5 @@ class TxtReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + ScreenshotInfo getScreenshotInfo() const override; }; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index a04100f7..b24cee18 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -360,3 +360,21 @@ void XtcReaderActivity::loadProgress() { f.close(); } } + +ScreenshotInfo XtcReaderActivity::getScreenshotInfo() const { + ScreenshotInfo info; + info.readerType = ScreenshotInfo::ReaderType::Xtc; + if (xtc) { + const std::string t = xtc->getTitle(); + snprintf(info.title, sizeof(info.title), "%s", t.c_str()); + const uint32_t pageCount = xtc->getPageCount(); + info.totalPages = pageCount; + // Clamp to last valid page to avoid sentinel value (currentPage == pageCount) + uint32_t clampedPage = (pageCount > 0 && currentPage >= pageCount) ? pageCount - 1 : currentPage; + info.progressPercent = pageCount > 0 ? xtc->calculateProgress(clampedPage) : 0; + info.currentPage = static_cast(clampedPage) + 1; + } else { + info.currentPage = currentPage + 1; + } + return info; +} diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 18effaad..282e8d2c 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -29,4 +29,5 @@ class XtcReaderActivity final : public Activity { void loop() override; void render(RenderLock&&) override; bool isReaderActivity() const override { return true; } + ScreenshotInfo getScreenshotInfo() const override; }; diff --git a/src/util/ScreenshotInfo.h b/src/util/ScreenshotInfo.h new file mode 100644 index 00000000..dfd76fe0 --- /dev/null +++ b/src/util/ScreenshotInfo.h @@ -0,0 +1,12 @@ +#pragma once +#include + +struct ScreenshotInfo { + enum class ReaderType : uint8_t { None, Epub, Txt, Xtc }; + ReaderType readerType = ReaderType::None; + char title[64] = {}; // Sanitized, truncated book title (null-terminated) + int spineIndex = -1; // EPUB only: current spine/chapter index + int currentPage = 0; // 1-based page number + int totalPages = 0; // Total pages in chapter (EPUB) or book (TXT/XTC) + int progressPercent = 0; // 0-100 whole-book progress +}; diff --git a/src/util/ScreenshotUtil.cpp b/src/util/ScreenshotUtil.cpp index a152488d..df8704f7 100644 --- a/src/util/ScreenshotUtil.cpp +++ b/src/util/ScreenshotUtil.cpp @@ -2,26 +2,88 @@ #include #include +#include #include #include #include +#include #include #include "Bitmap.h" // Required for BmpHeader struct definition +#include "activities/Activity.h" + +void ScreenshotUtil::buildFilename(const ScreenshotInfo& info, char* buf, size_t bufSize) { + const unsigned long ts = millis(); + + if (info.readerType == ScreenshotInfo::ReaderType::None || info.title[0] == '\0') { + snprintf(buf, bufSize, "/screenshots/screenshot-%lu.bmp", ts); + return; + } + + char sanitizedTitle[64]; + FsHelpers::sanitizePathComponentForFat32(info.title, sanitizedTitle, sizeof(sanitizedTitle)); + if (sanitizedTitle[0] == '\0') { + snprintf(buf, bufSize, "/screenshots/screenshot-%lu.bmp", ts); + return; + } + + int pct = info.progressPercent; + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + + // Display spine index as 1-based for user-facing filenames + const int chapterNum = info.spineIndex + 1; + + if (info.readerType == ScreenshotInfo::ReaderType::Epub && info.spineIndex >= 0) { + snprintf(buf, bufSize, "/screenshots/%s/%s_ch%d_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, chapterNum, + info.currentPage, pct, ts); + } else { + snprintf(buf, bufSize, "/screenshots/%s/%s_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, info.currentPage, + pct, ts); + } + + // Truncate title if total path exceeds FAT32 limit + if (strlen(buf) > 255) { + size_t titleLen = strlen(sanitizedTitle); + size_t overhead = strlen(buf) - 2 * titleLen; + if (overhead < 255) { + size_t maxTitleLen = (255 - overhead) / 2; + // Walk back to a valid UTF-8 boundary to avoid corrupting multibyte characters + while (maxTitleLen > 0 && (sanitizedTitle[maxTitleLen] & 0xC0) == 0x80) { + maxTitleLen--; + } + sanitizedTitle[maxTitleLen] = '\0'; + if (info.readerType == ScreenshotInfo::ReaderType::Epub && info.spineIndex >= 0) { + snprintf(buf, bufSize, "/screenshots/%s/%s_ch%d_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, chapterNum, + info.currentPage, pct, ts); + } else { + snprintf(buf, bufSize, "/screenshots/%s/%s_p%d_%dpct_%lu.bmp", sanitizedTitle, sanitizedTitle, info.currentPage, + pct, ts); + } + } else { + snprintf(buf, bufSize, "/screenshots/screenshot-%lu.bmp", ts); + } + } +} void ScreenshotUtil::takeScreenshot(GfxRenderer& renderer) { const uint8_t* fb = renderer.getFrameBuffer(); - if (fb) { - String filename_str = "/screenshots/screenshot-" + String(millis()) + ".bmp"; - if (ScreenshotUtil::saveFramebufferAsBmp(filename_str.c_str(), fb, renderer.getDisplayWidth(), - renderer.getDisplayHeight())) { - LOG_DBG("SCR", "Screenshot saved to %s", filename_str.c_str()); - } else { - LOG_ERR("SCR", "Failed to save screenshot"); - } - } else { + if (!fb) { LOG_ERR("SCR", "Framebuffer not available"); + return; + } + + ScreenshotInfo info = activityManager.getScreenshotInfo(); + char filename[256]; + buildFilename(info, filename, sizeof(filename)); + + bool saved = saveFramebufferAsBmp(filename, fb, renderer.getDisplayWidth(), renderer.getDisplayHeight()); + if (saved) { + LOG_DBG("SCR", "Screenshot saved to %s", filename); + } else { + LOG_ERR("SCR", "Failed to save screenshot"); + return; } // Display a border around the screen to indicate a screenshot was taken diff --git a/src/util/ScreenshotUtil.h b/src/util/ScreenshotUtil.h index 96d459e6..995d301f 100644 --- a/src/util/ScreenshotUtil.h +++ b/src/util/ScreenshotUtil.h @@ -1,8 +1,15 @@ #pragma once #include +#include + +#include "ScreenshotInfo.h" + class ScreenshotUtil { public: static void takeScreenshot(GfxRenderer& renderer); static bool saveFramebufferAsBmp(const char* filename, const uint8_t* framebuffer, int width, int height); + + private: + static void buildFilename(const ScreenshotInfo& info, char* buf, size_t bufSize); };