diff --git a/lib/FsHelpers/FsHelpers.cpp b/lib/FsHelpers/FsHelpers.cpp index 352e2c26..891190fd 100644 --- a/lib/FsHelpers/FsHelpers.cpp +++ b/lib/FsHelpers/FsHelpers.cpp @@ -1,5 +1,6 @@ #include "FsHelpers.h" +#include #include #include #include @@ -42,6 +43,58 @@ std::string normalisePath(const std::string& path) { return result; } +void sortFileList(std::vector& strs) { + std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { + // Directories first + bool isDir1 = str1.back() == '/'; + bool isDir2 = str2.back() == '/'; + if (isDir1 != isDir2) return isDir1; + + // Start naive natural sort + const char* s1 = str1.c_str(); + const char* s2 = str2.c_str(); + + // Iterate while both strings have characters + while (*s1 && *s2) { + // Check if both are at the start of a number + if (isdigit(*s1) && isdigit(*s2)) { + // Skip leading zeros and track them + const char* start1 = s1; + const char* start2 = s2; + while (*s1 == '0') s1++; + while (*s2 == '0') s2++; + + // Count digits to compare lengths first + int len1 = 0, len2 = 0; + while (isdigit(s1[len1])) len1++; + while (isdigit(s2[len2])) len2++; + + // Different length so return smaller integer value + if (len1 != len2) return len1 < len2; + + // Same length so compare digit by digit + for (int i = 0; i < len1; i++) { + if (s1[i] != s2[i]) return s1[i] < s2[i]; + } + + // Numbers equal so advance pointers + s1 += len1; + s2 += len2; + } else { + // Regular case-insensitive character comparison + char c1 = tolower(*s1); + char c2 = tolower(*s2); + if (c1 != c2) return c1 < c2; + s1++; + s2++; + } + } + + // One string is prefix of other + return *s1 == '\0' && *s2 != '\0'; + }); +} + bool checkFileExtension(std::string_view fileName, const char* extension) { const size_t extLen = strlen(extension); if (fileName.length() < extLen) { diff --git a/lib/FsHelpers/FsHelpers.h b/lib/FsHelpers/FsHelpers.h index b70a3dc8..56c2c987 100644 --- a/lib/FsHelpers/FsHelpers.h +++ b/lib/FsHelpers/FsHelpers.h @@ -3,11 +3,14 @@ #include #include +#include namespace FsHelpers { std::string normalisePath(const std::string& path); +void sortFileList(std::vector& strs); + /** * Check if the given filename ends with the specified extension (case-insensitive). */ diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index eb14a242..b6623899 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -293,6 +293,7 @@ STR_UPLOAD: "Upload" STR_BOOK_S_STYLE: "Book's Style" STR_EMBEDDED_STYLE: "Embedded Style" STR_OPDS_SERVER_URL: "OPDS Server URL" +STR_SET_SLEEP_COVER: "Set Cover" STR_FOOTNOTES: "Footnotes" STR_NO_FOOTNOTES: "No footnotes on this page" STR_LINK: "[link]" diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 06efdef7..19f443b0 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -49,6 +49,23 @@ void SleepActivity::renderCustomSleepScreen() const { // Check if we have a /.sleep (preferred) or /sleep directory const char* sleepDir = nullptr; auto dir = Storage.open("/.sleep"); + + // Look for sleep.bmp on the root of the sd card to determine if we should + // render a custom sleep screen instead of the default. + // This takes priority over the /sleep folder. + FsFile file; + if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) { + Bitmap bitmap(file, true); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + LOG_DBG("SLP", "Loading: /sleep.bmp"); + renderBitmapSleepScreen(bitmap); + file.close(); + if (dir) dir.close(); + return; + } + file.close(); + } + if (dir && dir.isDirectory()) { sleepDir = "/.sleep"; } else { @@ -62,26 +79,31 @@ void SleepActivity::renderCustomSleepScreen() const { std::vector files; char name[500]; // collect all valid BMP files - for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { - if (file.isDirectory()) { + for (auto dirFile = dir.openNextFile(); dirFile; dirFile = dir.openNextFile()) { + if (dirFile.isDirectory()) { + dirFile.close(); continue; } - file.getName(name, sizeof(name)); + dirFile.getName(name, sizeof(name)); auto filename = std::string(name); if (filename[0] == '.') { + dirFile.close(); continue; } if (!FsHelpers::hasBmpExtension(filename)) { LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name); + dirFile.close(); continue; } - Bitmap bitmap(file); + Bitmap bitmap(dirFile); if (bitmap.parseHeaders() != BmpReaderError::Ok) { LOG_DBG("SLP", "Skipping invalid BMP file: %s", name); + dirFile.close(); continue; } files.emplace_back(filename); + dirFile.close(); } const auto numFiles = files.size(); if (numFiles > 0) { @@ -97,29 +119,22 @@ void SleepActivity::renderCustomSleepScreen() const { APP_STATE.pushRecentSleep(randomFileIndex); APP_STATE.saveToFile(); const auto filename = std::string(sleepDir) + "/" + files[randomFileIndex]; - FsFile file; - if (Storage.openFileForRead("SLP", filename, file)) { + FsFile randFile; + if (Storage.openFileForRead("SLP", filename, randFile)) { LOG_DBG("SLP", "Randomly loading: %s/%s", sleepDir, files[randomFileIndex].c_str()); delay(100); - Bitmap bitmap(file, true); + Bitmap bitmap(randFile, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); + randFile.close(); + dir.close(); return; } + randFile.close(); } } } - // Look for sleep.bmp on the root of the sd card to determine if we should - // render a custom sleep screen instead of the default. - FsFile file; - if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) { - Bitmap bitmap(file, true); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - LOG_DBG("SLP", "Loading: /sleep.bmp"); - renderBitmapSleepScreen(bitmap); - return; - } - } + if (dir) dir.close(); renderDefaultSleepScreen(); } diff --git a/src/activities/home/FileBrowserActivity.cpp b/src/activities/home/FileBrowserActivity.cpp index c54e320c..3ea626e2 100644 --- a/src/activities/home/FileBrowserActivity.cpp +++ b/src/activities/home/FileBrowserActivity.cpp @@ -18,58 +18,6 @@ namespace { constexpr unsigned long GO_HOME_MS = 1000; } // namespace -void sortFileList(std::vector& strs) { - std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { - // Directories first - bool isDir1 = str1.back() == '/'; - bool isDir2 = str2.back() == '/'; - if (isDir1 != isDir2) return isDir1; - - // Start naive natural sort - const char* s1 = str1.c_str(); - const char* s2 = str2.c_str(); - - // Iterate while both strings have characters - while (*s1 && *s2) { - // Check if both are at the start of a number - if (isdigit(*s1) && isdigit(*s2)) { - // Skip leading zeros and track them - const char* start1 = s1; - const char* start2 = s2; - while (*s1 == '0') s1++; - while (*s2 == '0') s2++; - - // Count digits to compare lengths first - int len1 = 0, len2 = 0; - while (isdigit(s1[len1])) len1++; - while (isdigit(s2[len2])) len2++; - - // Different length so return smaller integer value - if (len1 != len2) return len1 < len2; - - // Same length so compare digit by digit - for (int i = 0; i < len1; i++) { - if (s1[i] != s2[i]) return s1[i] < s2[i]; - } - - // Numbers equal so advance pointers - s1 += len1; - s2 += len2; - } else { - // Regular case-insensitive character comparison - char c1 = tolower(*s1); - char c2 = tolower(*s2); - if (c1 != c2) return c1 < c2; - s1++; - s2++; - } - } - - // One string is prefix of other - return *s1 == '\0' && *s2 != '\0'; - }); -} - void FileBrowserActivity::loadFiles() { files.clear(); @@ -103,7 +51,8 @@ void FileBrowserActivity::loadFiles() { } } } - sortFileList(files); + root.close(); + FsHelpers::sortFileList(files); } void FileBrowserActivity::onEnter() { diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp index 37fa8fe1..4a29086b 100644 --- a/src/activities/util/BmpViewerActivity.cpp +++ b/src/activities/util/BmpViewerActivity.cpp @@ -1,19 +1,67 @@ #include "BmpViewerActivity.h" #include +#include #include #include #include +#include + +#include "CrossPointSettings.h" #include "components/UITheme.h" #include "fontIds.h" BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)) {} +void BmpViewerActivity::loadSiblingImages() { + siblingImages.clear(); + currentImageIndex = -1; + + if (filePath.empty()) return; + + std::string dirPath = FsHelpers::extractFolderPath(filePath); + size_t lastSlash = filePath.find_last_of('/'); + std::string fileName = (lastSlash != std::string::npos) ? filePath.substr(lastSlash + 1) : filePath; + + auto dir = Storage.open(dirPath.c_str()); + if (!dir || !dir.isDirectory()) { + if (dir) dir.close(); + return; + } + + char name[500]; + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + if (!file.isDirectory()) { + file.getName(name, sizeof(name)); + if (name[0] != '.') { + std::string fname(name); + if (fname.length() >= 4 && fname.substr(fname.length() - 4) == ".bmp") { + siblingImages.push_back(fname); + } + } + } + file.close(); + } + dir.close(); + + FsHelpers::sortFileList(siblingImages); + + for (size_t i = 0; i < siblingImages.size(); ++i) { + if (siblingImages[i] == fileName) { + currentImageIndex = static_cast(i); + break; + } + } +} + void BmpViewerActivity::onEnter() { Activity::onEnter(); - // Removed the redundant initial renderer.clearScreen() + + if (siblingImages.empty() && !filePath.empty()) { + loadSiblingImages(); + } FsFile file; @@ -49,7 +97,8 @@ void BmpViewerActivity::onEnter() { } // 4. Prepare Rendering - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SET_SLEEP_COVER), "", ""); + GUI.fillPopupProgress(renderer, popupRect, 50); renderer.clearScreen(); @@ -61,7 +110,7 @@ void BmpViewerActivity::onEnter() { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); // Single pass for non-grayscale images - renderer.displayBuffer(HalDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); } else { // Handle file parsing error @@ -89,6 +138,39 @@ void BmpViewerActivity::onExit() { renderer.displayBuffer(HalDisplay::HALF_REFRESH); } +void BmpViewerActivity::doSetSleepCover() { + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + + bool success = false; + FsFile inFile, outFile; + if (Storage.openFileForRead("BMP", filePath, inFile)) { + if (Storage.openFileForWrite("BMP", "/sleep.bmp", outFile)) { + char buffer[2048]; + int bytesRead; + success = true; + while ((bytesRead = inFile.read(buffer, sizeof(buffer))) > 0) { + if (outFile.write(buffer, bytesRead) != bytesRead) { + success = false; + break; + } + } + outFile.close(); + } + inFile.close(); + } + + if (success) { + SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; + SETTINGS.saveToFile(); + GUI.drawPopup(renderer, tr(STR_DONE)); + } else { + GUI.drawPopup(renderer, tr(STR_FAILED_LOWER)); + } + + delay(1000); + onEnter(); +} + void BmpViewerActivity::loop() { // Keep CPU awake/polling so 1st click works Activity::loop(); @@ -97,4 +179,32 @@ void BmpViewerActivity::loop() { activityManager.goToFileBrowser(filePath); return; } + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + doSetSleepCover(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { + if (siblingImages.size() > 1 && currentImageIndex > 0) { + currentImageIndex--; + std::string dirPath = FsHelpers::extractFolderPath(filePath); + if (dirPath.back() != '/') dirPath += "/"; + filePath = dirPath + siblingImages[currentImageIndex]; + onEnter(); + } + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { + if (siblingImages.size() > 1 && currentImageIndex != -1 && + currentImageIndex < static_cast(siblingImages.size()) - 1) { + currentImageIndex++; + std::string dirPath = FsHelpers::extractFolderPath(filePath); + if (dirPath.back() != '/') dirPath += "/"; + filePath = dirPath + siblingImages[currentImageIndex]; + onEnter(); + } + return; + } } \ No newline at end of file diff --git a/src/activities/util/BmpViewerActivity.h b/src/activities/util/BmpViewerActivity.h index feac448e..f805d375 100644 --- a/src/activities/util/BmpViewerActivity.h +++ b/src/activities/util/BmpViewerActivity.h @@ -15,5 +15,10 @@ class BmpViewerActivity final : public Activity { void loop() override; private: + void loadSiblingImages(); + void doSetSleepCover(); + std::string filePath; + std::vector siblingImages; + int currentImageIndex = -1; }; \ No newline at end of file