From 8ca01e9edebf639da96abf5abd19fba1398c03a4 Mon Sep 17 00:00:00 2001 From: altsysrq Date: Sat, 3 Jan 2026 23:24:21 -0600 Subject: [PATCH] Add adjustable sleep timer and file browser settings Implemented the adjustable sleep timer feature as requested. Updated the settings storage to include refresh interval and default folder preferences. Modified the state file version to accommodate new fields for last browsed folder. --- README.md | 2 + src/CrossPointSettings.cpp | 11 +- src/CrossPointSettings.h | 24 +++ src/CrossPointState.cpp | 15 +- src/CrossPointState.h | 1 + .../reader/CoverArtPickerActivity.cpp | 7 + src/activities/reader/EpubReaderActivity.cpp | 3 +- .../reader/FileSelectionActivity.cpp | 7 + src/activities/reader/ReaderActivity.cpp | 19 +- src/activities/reader/XtcReaderActivity.cpp | 6 +- .../settings/FolderPickerActivity.cpp | 195 ++++++++++++++++++ .../settings/FolderPickerActivity.h | 39 ++++ src/activities/settings/SettingsActivity.cpp | 34 ++- 13 files changed, 350 insertions(+), 13 deletions(-) create mode 100644 src/activities/settings/FolderPickerActivity.cpp create mode 100644 src/activities/settings/FolderPickerActivity.h diff --git a/README.md b/README.md index f56f8f9b..b02d9a1f 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ This project is **not affiliated with Xteink**; it's built as a community projec - [ ] Full UTF support - [x] Screen rotation - [x] Bluetooth LE Support +- [x] Adjustable sleep timer +- [x] Set default folder See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 01c09e8a..16420f3f 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 13; +constexpr uint8_t SETTINGS_COUNT = 16; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -40,6 +40,9 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, bluetoothEnabled); serialization::writePod(outputFile, useCoverArtPicker); serialization::writePod(outputFile, autoSleepMinutes); + serialization::writePod(outputFile, refreshInterval); + serialization::writePod(outputFile, defaultFolder); + serialization::writeString(outputFile, customDefaultFolder); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -92,6 +95,12 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, autoSleepMinutes); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, refreshInterval); + if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, defaultFolder); + if (++settingsRead >= fileSettingsCount) break; + serialization::readString(inputFile, customDefaultFolder); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 6a3315cc..4e9def2f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include class CrossPointSettings { private: @@ -44,6 +45,9 @@ class CrossPointSettings { enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 }; enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 }; + // Default folder options + enum DEFAULT_FOLDER { FOLDER_ROOT = 0, FOLDER_CUSTOM = 1, FOLDER_LAST_USED = 2 }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Status bar settings @@ -68,6 +72,13 @@ class CrossPointSettings { uint8_t useCoverArtPicker = 0; // Auto-sleep timeout (enum index: 0=2min, 1=5min, 2=10min, 3=15min, 4=20min, 5=30min, 6=60min, 7=Never) uint8_t autoSleepMinutes = 1; // Default to 5 minutes + // Screen refresh interval (enum index: 0=1pg, 1=3pg, 2=5pg, 3=10pg, 4=15pg, 5=20pg) + uint8_t refreshInterval = 4; // Default to 15 pages (current behavior) + // Default folder for file browser (enum index: 0=Root, 1=Custom, 2=Last Used) + uint8_t defaultFolder = FOLDER_LAST_USED; // Default to last used (current behavior) + + // Custom default folder path (used when defaultFolder == FOLDER_CUSTOM) + std::string customDefaultFolder = "/books"; ~CrossPointSettings() = default; @@ -91,6 +102,19 @@ class CrossPointSettings { return (autoSleepMinutes < 8) ? timeouts[autoSleepMinutes] : timeouts[2]; } + int getRefreshIntervalPages() const { + // Map enum index to pages: 0=1, 1=3, 2=5, 3=10, 4=15 (default), 5=20 + constexpr int intervals[] = {1, 3, 5, 10, 15, 20}; + return (refreshInterval < 6) ? intervals[refreshInterval] : 15; + } + + const char* getDefaultFolderPath() const { + // Returns the configured default folder path (doesn't handle FOLDER_LAST_USED) + if (defaultFolder == FOLDER_ROOT) return "/"; + if (defaultFolder == FOLDER_CUSTOM) return customDefaultFolder.c_str(); + return "/"; // Fallback + } + bool saveToFile() const; bool loadFromFile(); diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index 31cb2acb..786c3b2e 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -5,7 +5,7 @@ #include namespace { -constexpr uint8_t STATE_FILE_VERSION = 1; +constexpr uint8_t STATE_FILE_VERSION = 2; constexpr char STATE_FILE[] = "/.crosspoint/state.bin"; } // namespace @@ -19,6 +19,7 @@ bool CrossPointState::saveToFile() const { serialization::writePod(outputFile, STATE_FILE_VERSION); serialization::writeString(outputFile, openEpubPath); + serialization::writeString(outputFile, lastBrowsedFolder); outputFile.close(); return true; } @@ -31,14 +32,20 @@ bool CrossPointState::loadFromFile() { uint8_t version; serialization::readPod(inputFile, version); - if (version != STATE_FILE_VERSION) { + if (version == 1) { + // Version 1: only had openEpubPath + serialization::readString(inputFile, openEpubPath); + lastBrowsedFolder = "/"; // Default for old version + } else if (version == STATE_FILE_VERSION) { + // Version 2: has openEpubPath and lastBrowsedFolder + serialization::readString(inputFile, openEpubPath); + serialization::readString(inputFile, lastBrowsedFolder); + } else { Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version); inputFile.close(); return false; } - serialization::readString(inputFile, openEpubPath); - inputFile.close(); return true; } diff --git a/src/CrossPointState.h b/src/CrossPointState.h index f060a0c6..5e1c9686 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -8,6 +8,7 @@ class CrossPointState { public: std::string openEpubPath; + std::string lastBrowsedFolder = "/"; ~CrossPointState() = default; // Get singleton instance diff --git a/src/activities/reader/CoverArtPickerActivity.cpp b/src/activities/reader/CoverArtPickerActivity.cpp index 61019e6b..2470dfd4 100644 --- a/src/activities/reader/CoverArtPickerActivity.cpp +++ b/src/activities/reader/CoverArtPickerActivity.cpp @@ -6,6 +6,7 @@ #include #include "Bitmap.h" +#include "CrossPointState.h" #include "MappedInputManager.h" #include "fontIds.h" @@ -103,6 +104,8 @@ void CoverArtPickerActivity::loop() { if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (basepath != "/") { basepath = "/"; + APP_STATE.lastBrowsedFolder = basepath; + APP_STATE.saveToFile(); loadFiles(); updateRequired = true; } @@ -124,6 +127,8 @@ void CoverArtPickerActivity::loop() { if (basepath.back() != '/') basepath += "/"; if (files[selectorIndex].back() == '/') { basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + APP_STATE.lastBrowsedFolder = basepath; + APP_STATE.saveToFile(); loadFiles(); updateRequired = true; } else { @@ -135,6 +140,8 @@ void CoverArtPickerActivity::loop() { if (basepath != "/") { basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); if (basepath.empty()) basepath = "/"; + APP_STATE.lastBrowsedFolder = basepath; + APP_STATE.saveToFile(); loadFiles(); updateRequired = true; } else { diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index f9ef40c7..8f404f5a 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -13,7 +13,6 @@ #include "fontIds.h" namespace { -constexpr int pagesPerRefresh = 15; constexpr unsigned long skipChapterMs = 700; constexpr unsigned long goHomeMs = 1000; constexpr int topPadding = 5; @@ -378,7 +377,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); - pagesUntilFullRefresh = pagesPerRefresh; + pagesUntilFullRefresh = SETTINGS.getRefreshIntervalPages(); } else { renderer.displayBuffer(); pagesUntilFullRefresh--; diff --git a/src/activities/reader/FileSelectionActivity.cpp b/src/activities/reader/FileSelectionActivity.cpp index 4496af8e..31cf4985 100644 --- a/src/activities/reader/FileSelectionActivity.cpp +++ b/src/activities/reader/FileSelectionActivity.cpp @@ -3,6 +3,7 @@ #include #include +#include "CrossPointState.h" #include "MappedInputManager.h" #include "fontIds.h" @@ -102,6 +103,8 @@ void FileSelectionActivity::loop() { if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (basepath != "/") { basepath = "/"; + APP_STATE.lastBrowsedFolder = basepath; + APP_STATE.saveToFile(); loadFiles(); updateRequired = true; } @@ -123,6 +126,8 @@ void FileSelectionActivity::loop() { if (basepath.back() != '/') basepath += "/"; if (files[selectorIndex].back() == '/') { basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + APP_STATE.lastBrowsedFolder = basepath; + APP_STATE.saveToFile(); loadFiles(); updateRequired = true; } else { @@ -134,6 +139,8 @@ void FileSelectionActivity::loop() { if (basepath != "/") { basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); if (basepath.empty()) basepath = "/"; + APP_STATE.lastBrowsedFolder = basepath; + APP_STATE.saveToFile(); loadFiles(); updateRequired = true; } else { diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 533a417b..720e1501 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -1,6 +1,7 @@ #include "ReaderActivity.h" #include "CrossPointSettings.h" +#include "CrossPointState.h" #include "CoverArtPickerActivity.h" #include "Epub.h" #include "EpubReaderActivity.h" @@ -92,8 +93,22 @@ void ReaderActivity::onSelectBookFile(const std::string& path) { void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) { exitActivity(); - // If coming from a book, start in that book's folder; otherwise start from root - const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); + + // Determine initial path based on default folder setting + std::string initialPath; + if (SETTINGS.defaultFolder == CrossPointSettings::FOLDER_LAST_USED) { + // Use last browsed folder, or fall back to book's folder if coming from a book + if (!fromBookPath.empty()) { + initialPath = extractFolderPath(fromBookPath); + } else if (!APP_STATE.lastBrowsedFolder.empty()) { + initialPath = APP_STATE.lastBrowsedFolder; + } else { + initialPath = "/"; + } + } else { + // Use configured default folder (Root or Books) + initialPath = SETTINGS.getDefaultFolderPath(); + } // Check if cover art picker is enabled if (SETTINGS.useCoverArtPicker) { diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 5f8a74c9..ec918021 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -11,13 +11,13 @@ #include #include +#include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" #include "XtcReaderChapterSelectionActivity.h" #include "fontIds.h" namespace { -constexpr int pagesPerRefresh = 15; constexpr unsigned long skipPageMs = 700; constexpr unsigned long goHomeMs = 1000; } // namespace @@ -266,7 +266,7 @@ void XtcReaderActivity::renderPage() { // Display BW with conditional refresh based on pagesUntilFullRefresh if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); - pagesUntilFullRefresh = pagesPerRefresh; + pagesUntilFullRefresh = SETTINGS.getRefreshIntervalPages(); } else { renderer.displayBuffer(); pagesUntilFullRefresh--; @@ -346,7 +346,7 @@ void XtcReaderActivity::renderPage() { // Display with appropriate refresh if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); - pagesUntilFullRefresh = pagesPerRefresh; + pagesUntilFullRefresh = SETTINGS.getRefreshIntervalPages(); } else { renderer.displayBuffer(); pagesUntilFullRefresh--; diff --git a/src/activities/settings/FolderPickerActivity.cpp b/src/activities/settings/FolderPickerActivity.cpp new file mode 100644 index 00000000..7080b917 --- /dev/null +++ b/src/activities/settings/FolderPickerActivity.cpp @@ -0,0 +1,195 @@ +#include "FolderPickerActivity.h" + +#include +#include + +#include "MappedInputManager.h" +#include "fontIds.h" + +namespace { +constexpr int PAGE_ITEMS = 23; +constexpr unsigned long GO_HOME_MS = 1000; +} // namespace + +void sortFolderList(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 FolderPickerActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void FolderPickerActivity::loadFolders() { + folders.clear(); + selectorIndex = 0; + + // Add option to select current folder + folders.emplace_back("[Select This Folder]"); + + auto root = SdMan.open(basepath.c_str()); + if (!root || !root.isDirectory()) { + if (root) root.close(); + return; + } + + root.rewindDirectory(); + + char name[128]; + for (auto file = root.openNextFile(); file; file = root.openNextFile()) { + file.getName(name, sizeof(name)); + if (name[0] == '.' || strcmp(name, "System Volume Information") == 0) { + file.close(); + continue; + } + + if (file.isDirectory()) { + folders.emplace_back(std::string(name) + "/"); + } + file.close(); + } + root.close(); + // Sort only the actual folders (skip the first item which is "[Select This Folder]") + if (folders.size() > 1) { + std::vector actualFolders(folders.begin() + 1, folders.end()); + sortFolderList(actualFolders); + std::copy(actualFolders.begin(), actualFolders.end(), folders.begin() + 1); + } +} + +void FolderPickerActivity::onEnter() { + Activity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + entryTime = millis(); + + loadFolders(); + selectorIndex = 0; + + // Trigger first update + updateRequired = true; + + xTaskCreate(&FolderPickerActivity::taskTrampoline, "FolderPickerActivityTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void FolderPickerActivity::onExit() { + Activity::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; + folders.clear(); +} + +void FolderPickerActivity::loop() { + // Ignore button presses for 200ms after entry to avoid processing the button that opened this activity + if (millis() - entryTime < 200) { + return; + } + + // Long press BACK (1s+) goes to root folder + if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { + if (basepath != "/") { + basepath = "/"; + loadFolders(); + updateRequired = true; + } + return; + } + + const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || + mappedInput.wasReleased(MappedInputManager::Button::Right); + + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (folders.empty()) { + return; + } + + if (selectorIndex == 0) { + // "[Select This Folder]" option selected + onSelect(basepath); + } else if (selectorIndex < folders.size()) { + // Navigate into the selected folder + if (basepath.back() != '/') basepath += "/"; + basepath += folders[selectorIndex].substr(0, folders[selectorIndex].length() - 1); + loadFolders(); + updateRequired = true; + } + } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + // Short press: go up one directory, or cancel if at root + if (mappedInput.getHeldTime() < GO_HOME_MS) { + if (basepath != "/") { + basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); + if (basepath.empty()) basepath = "/"; + loadFolders(); + updateRequired = true; + } else { + onCancel(); + } + } + } else if (prevReleased && !folders.empty()) { + selectorIndex = (selectorIndex + folders.size() - 1) % folders.size(); + updateRequired = true; + } else if (nextReleased && !folders.empty()) { + selectorIndex = (selectorIndex + 1) % folders.size(); + updateRequired = true; + } +} + +void FolderPickerActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void FolderPickerActivity::render() const { + renderer.clearScreen(); + + const auto pageWidth = renderer.getScreenWidth(); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Choose Default Folder", true, BOLD); + + // Display current path + auto truncatedPath = renderer.truncatedText(SMALL_FONT_ID, basepath.c_str(), pageWidth - 40); + renderer.drawText(SMALL_FONT_ID, 20, 35, truncatedPath.c_str()); + + // Help text + const auto labels = mappedInput.mapLabels("« Cancel", "Select", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + if (folders.empty()) { + renderer.drawText(UI_10_FONT_ID, 20, 60, "No subfolders. Press Select to use this folder."); + renderer.displayBuffer(); + return; + } + + const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; + renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30); + for (int i = pageStartIndex; i < folders.size() && i < pageStartIndex + PAGE_ITEMS; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, folders[i].c_str(), renderer.getScreenWidth() - 40); + renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex); + } + + renderer.displayBuffer(); +} diff --git a/src/activities/settings/FolderPickerActivity.h b/src/activities/settings/FolderPickerActivity.h new file mode 100644 index 00000000..da696f79 --- /dev/null +++ b/src/activities/settings/FolderPickerActivity.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "../Activity.h" + +class FolderPickerActivity final : public Activity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + std::string basepath = "/"; + std::vector folders; + int selectorIndex = 0; + bool updateRequired = false; + unsigned long entryTime = 0; + const std::function onSelect; + const std::function onCancel; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void loadFolders(); + + public: + explicit FolderPickerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onSelect, + const std::function& onCancel, std::string initialPath = "/") + : Activity("FolderPicker", renderer, mappedInput), + basepath(initialPath.empty() ? "/" : std::move(initialPath)), + onSelect(onSelect), + onCancel(onCancel) {} + void onEnter() override; + void onExit() override; + void loop() override; +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index d2a51da7..6567e672 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -3,13 +3,14 @@ #include #include "CrossPointSettings.h" +#include "FolderPickerActivity.h" #include "MappedInputManager.h" #include "OtaUpdateActivity.h" #include "fontIds.h" // Define the static settings list namespace { -constexpr int settingsCount = 14; +constexpr int settingsCount = 17; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, @@ -34,11 +35,20 @@ const SettingInfo settingsList[settingsCount] = { {"Bookerly", "Noto Sans", "Open Dyslexic"}}, {"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}}, {"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}}, + {"Screen Refresh Interval", + SettingType::ENUM, + &CrossPointSettings::refreshInterval, + {"Every page", "Every 3 pages", "Every 5 pages", "Every 10 pages", "Every 15 pages", "Every 20 pages"}}, {"Cover Art Picker", SettingType::TOGGLE, &CrossPointSettings::useCoverArtPicker, {}}, {"Auto Sleep Timeout", SettingType::ENUM, &CrossPointSettings::autoSleepMinutes, {"2 min", "5 min", "10 min", "15 min", "20 min", "30 min", "60 min", "Never"}}, + {"Default Folder", + SettingType::ENUM, + &CrossPointSettings::defaultFolder, + {"Root", "Custom", "Last Used"}}, + {"Choose Custom Folder", SettingType::ACTION, nullptr, {}}, {"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}}, {"Check for updates", SettingType::ACTION, nullptr, {}}, }; @@ -140,6 +150,23 @@ void SettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (std::string(setting.name) == "Choose Custom Folder") { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new FolderPickerActivity( + renderer, mappedInput, + [this](const std::string& path) { + SETTINGS.customDefaultFolder = path; + SETTINGS.saveToFile(); + exitActivity(); + updateRequired = true; + }, + [this] { + exitActivity(); + updateRequired = true; + }, + "/")); // Start from root directory + xSemaphoreGive(renderingMutex); } } else { // Only toggle if it's a toggle type and has a value pointer @@ -189,6 +216,11 @@ void SettingsActivity::render() const { } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); valueText = settingsList[i].enumValues[value]; + } else if (settingsList[i].type == SettingType::ACTION) { + // Show current value for "Choose Custom Folder" + if (std::string(settingsList[i].name) == "Choose Custom Folder") { + valueText = SETTINGS.customDefaultFolder; + } } const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex);