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);