#include "FileSelectionScreen.h" #include #include void caseInsensitiveSort(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { return lexicographical_compare( begin(str1), end(str1), begin(str2), end(str2), [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); }); } void FileSelectionScreen::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); } void FileSelectionScreen::loadFiles() { files.clear(); selectorIndex = 0; auto root = SD.open(basepath.c_str()); for (File file = root.openNextFile(); file; file = root.openNextFile()) { auto filename = std::string(file.name()); if (filename[0] == '.') { file.close(); continue; } if (file.isDirectory()) { files.emplace_back(filename + "/"); } else if (filename.substr(filename.length() - 5) == ".epub") { files.emplace_back(filename); } file.close(); } root.close(); caseInsensitiveSort(files); } void FileSelectionScreen::onEnter() { renderingMutex = xSemaphoreCreateMutex(); basepath = "/"; loadFiles(); selectorIndex = 0; // Trigger first update updateRequired = true; xTaskCreate(&FileSelectionScreen::taskTrampoline, "FileSelectionScreenTask", 1024, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle ); } void FileSelectionScreen::onExit() { // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); displayTaskHandle = nullptr; } vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; files.clear(); } void FileSelectionScreen::handleInput() { const bool prevPressed = inputManager.wasPressed(InputManager::BTN_UP) || inputManager.wasPressed(InputManager::BTN_LEFT); const bool nextPressed = inputManager.wasPressed(InputManager::BTN_DOWN) || inputManager.wasPressed(InputManager::BTN_RIGHT); if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { if (files.empty()) { return; } if (basepath.back() != '/') basepath += "/"; if (files[selectorIndex].back() == '/') { basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); loadFiles(); updateRequired = true; } else { onSelect(basepath + files[selectorIndex]); } } else if (inputManager.wasPressed(InputManager::BTN_BACK) && basepath != "/") { basepath = basepath.substr(0, basepath.rfind('/')); if (basepath.empty()) basepath = "/"; loadFiles(); updateRequired = true; } else if (prevPressed) { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); updateRequired = true; } else if (nextPressed) { selectorIndex = (selectorIndex + 1) % files.size(); updateRequired = true; } } void FileSelectionScreen::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); render(); xSemaphoreGive(renderingMutex); } vTaskDelay(10 / portTICK_PERIOD_MS); } } void FileSelectionScreen::render() const { renderer.clearScreen(); const auto pageWidth = renderer.getPageWidth(); const auto titleWidth = renderer.getTextWidth("CrossPoint Reader", BOLD); renderer.drawText((pageWidth - titleWidth) / 2, 0, "CrossPoint Reader", 1, BOLD); if (files.empty()) { renderer.drawUiText(10, 50, "No EPUBs found"); } else { // Draw selection renderer.fillRect(0, 50 + selectorIndex * 30 + 2, pageWidth - 1, 30); for (size_t i = 0; i < files.size(); i++) { const auto file = files[i]; renderer.drawUiText(10, 50 + i * 30, file.c_str(), i == selectorIndex ? 0 : 1); } } renderer.flushDisplay(); }