diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 67f65d55..109cd5ba 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -425,6 +425,9 @@ STR_AUTHOR: "Author" STR_SERIES: "Series" STR_FILE_SIZE: "Size" STR_SLEEP_COVER_OVERLAY: "Sleep Screen Info Overlay" +STR_SLEEP_IMAGE_PICK_MODE: "Sleep Image Selection" +STR_RANDOM: "Random" +STR_SEQUENTIAL: "Sequential" STR_OVERLAY_WHITE: "White" STR_OVERLAY_GRAY: "Gray" STR_OVERLAY_BLACK: "Black" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 94d110c1..7b192ac0 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -28,6 +28,7 @@ class CrossPointSettings { SLEEP_SCREEN_MODE_COUNT }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; + enum SLEEP_IMAGE_PICK_MODE { PICK_RANDOM = 0, PICK_SEQUENTIAL = 1, SLEEP_IMAGE_PICK_MODE_COUNT }; enum SLEEP_SCREEN_COVER_FILTER { NO_FILTER = 0, BLACK_AND_WHITE = 1, @@ -177,6 +178,8 @@ class CrossPointSettings { uint8_t sleepScreenCoverFilter = NO_FILTER; // Apply information overlay with reading progress on sleep cover uint8_t sleepCoverOverlay = 0; + // Sleep image pick mode (random vs sequential walk-through) + uint8_t sleepImagePickMode = PICK_RANDOM; // Status bar settings (statusBar retained for migration only) uint8_t statusBar = FULL; uint8_t statusBarChapterPageCount = 1; diff --git a/src/SettingsList.h b/src/SettingsList.h index c8a41249..753b6e4c 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -27,6 +27,8 @@ inline const std::vector& getSettingsList() { StrId::STR_SLEEP_COVER_OVERLAY, &CrossPointSettings::sleepCoverOverlay, {StrId::STR_OVERLAY_OFF, StrId::STR_OVERLAY_WHITE, StrId::STR_OVERLAY_GRAY, StrId::STR_OVERLAY_BLACK}, "sleepCoverOverlay", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_SLEEP_IMAGE_PICK_MODE, &CrossPointSettings::sleepImagePickMode, + {StrId::STR_RANDOM, StrId::STR_SEQUENTIAL}, "sleepImagePickMode", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, {StrId::STR_NEVER, StrId::STR_IN_READER, StrId::STR_ALWAYS}, "hideBatteryPercentage", StrId::STR_CAT_DISPLAY), diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index d5b4a440..05808bb6 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include "../reader/EpubReaderActivity.h" @@ -158,6 +159,70 @@ int pngOverlayDraw(PNGDRAW* pDraw) { return 1; } +// Collects full paths of valid image files from /.sleep and /sleep, with no preference between +// the two directories. BMP files are validated by parsing their headers; invalid BMPs are skipped. +// When allowPng is true, .png files are also accepted (PNG validation happens later at decode time). +std::vector collectSleepImages(bool allowPng) { + std::vector files; + for (const char* sleepDir : {"/.sleep", "/sleep"}) { + auto dir = Storage.open(sleepDir); + if (!dir || !dir.isDirectory()) { + if (dir) dir.close(); + continue; + } + char name[500]; + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (file.isDirectory()) { + file.close(); + continue; + } + file.getName(name, sizeof(name)); + auto filename = std::string(name); + if (filename[0] == '.') { + file.close(); + continue; + } + const bool isBmp = FsHelpers::hasBmpExtension(filename); + const bool isPng = allowPng && FsHelpers::hasPngExtension(filename); + if (!isBmp && !isPng) { + file.close(); + continue; + } + if (isBmp) { + Bitmap bmp(file); + if (bmp.parseHeaders() != BmpReaderError::Ok) { + LOG_DBG("SLP", "Skipping invalid BMP file: %s", name); + file.close(); + continue; + } + } + files.emplace_back(std::string(sleepDir) + "/" + filename); + file.close(); + } + dir.close(); + } + // Sort by full path so the order is deterministic across reboots — required for sequential + // pick mode, harmless for random pick mode. + std::sort(files.begin(), files.end()); + return files; +} + +// Picks the next file index based on the user's pick mode. +// RANDOM: uniform random with single reroll to avoid immediate repeats. +// SEQUENTIAL: advances from APP_STATE.lastSleepImage, wrapping at numFiles. +size_t pickSleepImageIndex(size_t numFiles) { + if (SETTINGS.sleepImagePickMode == CrossPointSettings::SLEEP_IMAGE_PICK_MODE::PICK_SEQUENTIAL) { + const uint8_t last = APP_STATE.lastSleepImage; + if (last == UINT8_MAX || last >= numFiles) return 0; + return (static_cast(last) + 1) % numFiles; + } + size_t idx = random(numFiles); + while (numFiles > 1 && APP_STATE.lastSleepImage != UINT8_MAX && idx == APP_STATE.lastSleepImage) { + idx = random(numFiles); + } + return idx; +} + } // namespace void SleepActivity::onEnter() { @@ -202,78 +267,29 @@ void SleepActivity::renderCustomSleepScreen() const { explicitSleepFile.close(); } - // Check if we have a /.sleep (preferred) or /sleep directory - const char* sleepDir = nullptr; - auto dir = Storage.open("/.sleep"); - if (dir && dir.isDirectory()) { - sleepDir = "/.sleep"; - } else { - if (dir) dir.close(); - dir = Storage.open("/sleep"); - if (dir && dir.isDirectory()) { - sleepDir = "/sleep"; - } - } - - if (sleepDir) { - std::vector files; - char name[500]; - // collect all valid BMP files - for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { - if (file.isDirectory()) { + // Collect valid BMP files from both /.sleep and /sleep directories (no preference between them) + const auto files = collectSleepImages(/*allowPng=*/false); + const auto numFiles = files.size(); + if (numFiles > 0) { + const auto pickedIndex = pickSleepImageIndex(numFiles); + APP_STATE.lastSleepImage = pickedIndex; + APP_STATE.saveToFile(); + const auto& filename = files[pickedIndex]; + FsFile file; + if (Storage.openFileForRead("SLP", filename, file)) { + LOG_DBG("SLP", "Loading sleep image: %s", filename.c_str()); + delay(100); + Bitmap bitmap(file, true); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + const BookOverlayInfo resolvedOverlayInfo = + shouldLoadOverlayInfo ? getBookOverlayInfo(APP_STATE.openEpubPath) : overlayInfo; + renderBitmapSleepScreen(bitmap, resolvedOverlayInfo); file.close(); - continue; + return; } - file.getName(name, sizeof(name)); - auto filename = std::string(name); - if (filename[0] == '.') { - file.close(); - continue; - } - - if (!FsHelpers::hasBmpExtension(filename)) { - LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name); - file.close(); - continue; - } - Bitmap bitmap(file); - if (bitmap.parseHeaders() != BmpReaderError::Ok) { - LOG_DBG("SLP", "Skipping invalid BMP file: %s", name); - file.close(); - continue; - } - files.emplace_back(filename); file.close(); } - const auto numFiles = files.size(); - if (numFiles > 0) { - // Generate a random number between 1 and numFiles - auto randomFileIndex = random(numFiles); - // If we picked the same image as last time, reroll - while (numFiles > 1 && APP_STATE.lastSleepImage != UINT8_MAX && randomFileIndex == APP_STATE.lastSleepImage) { - randomFileIndex = random(numFiles); - } - APP_STATE.lastSleepImage = randomFileIndex; - APP_STATE.saveToFile(); - const auto filename = std::string(sleepDir) + "/" + files[randomFileIndex]; - FsFile file; - if (Storage.openFileForRead("SLP", filename, file)) { - LOG_DBG("SLP", "Randomly loading: %s/%s", sleepDir, files[randomFileIndex].c_str()); - delay(100); - Bitmap bitmap(file, true); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - const BookOverlayInfo resolvedOverlayInfo = - shouldLoadOverlayInfo ? getBookOverlayInfo(APP_STATE.openEpubPath) : overlayInfo; - renderBitmapSleepScreen(bitmap, resolvedOverlayInfo); - file.close(); - dir.close(); - return; - } - file.close(); - } - } } - if (dir) dir.close(); renderDefaultSleepScreen(); } @@ -769,67 +785,22 @@ void SleepActivity::renderOverlaySleepScreen() const { return rc == PNG_SUCCESS; }; - // Try /.sleep/ (preferred) or /sleep/ directory (random selection, same as renderCustomSleepScreen). + // Collect images from both /.sleep and /sleep directories (no preference between them). // Accepts both .bmp and .png files; .bmp headers are validated during the scan. bool overlayDrawn = false; - const char* sleepDir = nullptr; - auto dir = Storage.open("/.sleep"); - if (dir && dir.isDirectory()) { - sleepDir = "/.sleep"; - } else { - if (dir) dir.close(); - dir = Storage.open("/sleep"); - if (dir && dir.isDirectory()) { - sleepDir = "/sleep"; + const auto files = collectSleepImages(/*allowPng=*/true); + const auto numFiles = files.size(); + if (numFiles > 0) { + const auto pickedIndex = pickSleepImageIndex(numFiles); + APP_STATE.lastSleepImage = pickedIndex; + APP_STATE.saveToFile(); + const std::string& selected = files[pickedIndex]; + if (FsHelpers::hasPngExtension(selected)) { + overlayDrawn = tryDrawPngOverlay(selected); + } else { + overlayDrawn = tryDrawOverlay(selected); } } - if (sleepDir) { - std::vector files; - char name[500]; - for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { - if (file.isDirectory()) { - file.close(); - continue; - } - file.getName(name, sizeof(name)); - auto filename = std::string(name); - if (filename[0] == '.') { - file.close(); - continue; - } - const bool isBmp = FsHelpers::checkFileExtension(filename, ".bmp"); - const bool isPng = FsHelpers::checkFileExtension(filename, ".png"); - if (!isBmp && !isPng) { - file.close(); - continue; - } - if (isBmp) { - Bitmap bmp(file); - if (bmp.parseHeaders() != BmpReaderError::Ok) { - file.close(); - continue; - } - } - files.emplace_back(filename); - file.close(); - } - const auto numFiles = files.size(); - if (numFiles > 0) { - auto randomFileIndex = random(numFiles); - while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) { - randomFileIndex = random(numFiles); - } - APP_STATE.lastSleepImage = randomFileIndex; - APP_STATE.saveToFile(); - const std::string selected = std::string(sleepDir) + "/" + files[randomFileIndex]; - if (FsHelpers::checkFileExtension(selected, ".png")) { - overlayDrawn = tryDrawPngOverlay(selected); - } else { - overlayDrawn = tryDrawOverlay(selected); - } - } - } - if (dir) dir.close(); if (!overlayDrawn) { overlayDrawn = tryDrawOverlay("/sleep.bmp");