From 2f969a93b4ca85ccdd96eb38516675e6762ee7fe Mon Sep 17 00:00:00 2001 From: Jon Stieglitz <57100505+jonstieglitz@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:56:45 -0400 Subject: [PATCH] feat: context-aware screenshot filenames with book title (#1589) ## Summary I love the new screenshot feature but I can tell that soon I'm gonna have a folder full of screenshots and not know what's what. It would be great if the folder could self organize. When a screenshot is taken while reading, the filename would now include the book title, chapter (EPUB), page number, and progress percentage. Example: My-Great-Book_ch3_p5_42pct_12345.bmp * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) * **What changes are included?** ## Additional Context * Non-reader screens keep the existing screenshot-.bmp naming. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< YES >**_ I reviewed all the changes but I don't have any experience with this project so this is my best guess --- lib/FsHelpers/FsHelpers.cpp | 18 +++++ lib/FsHelpers/FsHelpers.h | 6 ++ src/activities/Activity.h | 2 + src/activities/ActivityManager.cpp | 7 ++ src/activities/ActivityManager.h | 2 + src/activities/reader/EpubReaderActivity.cpp | 21 +++++ src/activities/reader/EpubReaderActivity.h | 1 + src/activities/reader/TxtReaderActivity.cpp | 14 ++++ src/activities/reader/TxtReaderActivity.h | 1 + src/activities/reader/XtcReaderActivity.cpp | 18 +++++ src/activities/reader/XtcReaderActivity.h | 1 + src/util/ScreenshotInfo.h | 12 +++ src/util/ScreenshotUtil.cpp | 80 +++++++++++++++++--- src/util/ScreenshotUtil.h | 7 ++ 14 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 src/util/ScreenshotInfo.h 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); };