mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 15:47:39 +03:00
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.
This commit is contained in:
parent
0054981eab
commit
8ca01e9ede
@ -41,6 +41,8 @@ This project is **not affiliated with Xteink**; it's built as a community projec
|
|||||||
- [ ] Full UTF support
|
- [ ] Full UTF support
|
||||||
- [x] Screen rotation
|
- [x] Screen rotation
|
||||||
- [x] Bluetooth LE Support
|
- [x] Bluetooth LE Support
|
||||||
|
- [x] Adjustable sleep timer
|
||||||
|
- [x] Set default folder
|
||||||
|
|
||||||
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint.
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance;
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
|
||||||
// Increment this when adding new persisted settings fields
|
// 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";
|
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -40,6 +40,9 @@ bool CrossPointSettings::saveToFile() const {
|
|||||||
serialization::writePod(outputFile, bluetoothEnabled);
|
serialization::writePod(outputFile, bluetoothEnabled);
|
||||||
serialization::writePod(outputFile, useCoverArtPicker);
|
serialization::writePod(outputFile, useCoverArtPicker);
|
||||||
serialization::writePod(outputFile, autoSleepMinutes);
|
serialization::writePod(outputFile, autoSleepMinutes);
|
||||||
|
serialization::writePod(outputFile, refreshInterval);
|
||||||
|
serialization::writePod(outputFile, defaultFolder);
|
||||||
|
serialization::writeString(outputFile, customDefaultFolder);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
|
||||||
@ -92,6 +95,12 @@ bool CrossPointSettings::loadFromFile() {
|
|||||||
if (++settingsRead >= fileSettingsCount) break;
|
if (++settingsRead >= fileSettingsCount) break;
|
||||||
serialization::readPod(inputFile, autoSleepMinutes);
|
serialization::readPod(inputFile, autoSleepMinutes);
|
||||||
if (++settingsRead >= fileSettingsCount) break;
|
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);
|
} while (false);
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <iosfwd>
|
#include <iosfwd>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
class CrossPointSettings {
|
class CrossPointSettings {
|
||||||
private:
|
private:
|
||||||
@ -44,6 +45,9 @@ class CrossPointSettings {
|
|||||||
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
|
||||||
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
|
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
|
// Sleep screen settings
|
||||||
uint8_t sleepScreen = DARK;
|
uint8_t sleepScreen = DARK;
|
||||||
// Status bar settings
|
// Status bar settings
|
||||||
@ -68,6 +72,13 @@ class CrossPointSettings {
|
|||||||
uint8_t useCoverArtPicker = 0;
|
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)
|
// 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
|
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;
|
~CrossPointSettings() = default;
|
||||||
|
|
||||||
@ -91,6 +102,19 @@ class CrossPointSettings {
|
|||||||
return (autoSleepMinutes < 8) ? timeouts[autoSleepMinutes] : timeouts[2];
|
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 saveToFile() const;
|
||||||
bool loadFromFile();
|
bool loadFromFile();
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t STATE_FILE_VERSION = 1;
|
constexpr uint8_t STATE_FILE_VERSION = 2;
|
||||||
constexpr char STATE_FILE[] = "/.crosspoint/state.bin";
|
constexpr char STATE_FILE[] = "/.crosspoint/state.bin";
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ bool CrossPointState::saveToFile() const {
|
|||||||
|
|
||||||
serialization::writePod(outputFile, STATE_FILE_VERSION);
|
serialization::writePod(outputFile, STATE_FILE_VERSION);
|
||||||
serialization::writeString(outputFile, openEpubPath);
|
serialization::writeString(outputFile, openEpubPath);
|
||||||
|
serialization::writeString(outputFile, lastBrowsedFolder);
|
||||||
outputFile.close();
|
outputFile.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -31,14 +32,20 @@ bool CrossPointState::loadFromFile() {
|
|||||||
|
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
serialization::readPod(inputFile, 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);
|
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
serialization::readString(inputFile, openEpubPath);
|
|
||||||
|
|
||||||
inputFile.close();
|
inputFile.close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ class CrossPointState {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
std::string openEpubPath;
|
std::string openEpubPath;
|
||||||
|
std::string lastBrowsedFolder = "/";
|
||||||
~CrossPointState() = default;
|
~CrossPointState() = default;
|
||||||
|
|
||||||
// Get singleton instance
|
// Get singleton instance
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
#include "Bitmap.h"
|
#include "Bitmap.h"
|
||||||
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@ -103,6 +104,8 @@ void CoverArtPickerActivity::loop() {
|
|||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
||||||
if (basepath != "/") {
|
if (basepath != "/") {
|
||||||
basepath = "/";
|
basepath = "/";
|
||||||
|
APP_STATE.lastBrowsedFolder = basepath;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
@ -124,6 +127,8 @@ void CoverArtPickerActivity::loop() {
|
|||||||
if (basepath.back() != '/') basepath += "/";
|
if (basepath.back() != '/') basepath += "/";
|
||||||
if (files[selectorIndex].back() == '/') {
|
if (files[selectorIndex].back() == '/') {
|
||||||
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
||||||
|
APP_STATE.lastBrowsedFolder = basepath;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
@ -135,6 +140,8 @@ void CoverArtPickerActivity::loop() {
|
|||||||
if (basepath != "/") {
|
if (basepath != "/") {
|
||||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||||
if (basepath.empty()) basepath = "/";
|
if (basepath.empty()) basepath = "/";
|
||||||
|
APP_STATE.lastBrowsedFolder = basepath;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int pagesPerRefresh = 15;
|
|
||||||
constexpr unsigned long skipChapterMs = 700;
|
constexpr unsigned long skipChapterMs = 700;
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
constexpr unsigned long goHomeMs = 1000;
|
||||||
constexpr int topPadding = 5;
|
constexpr int topPadding = 5;
|
||||||
@ -378,7 +377,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = pagesPerRefresh;
|
pagesUntilFullRefresh = SETTINGS.getRefreshIntervalPages();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
@ -102,6 +103,8 @@ void FileSelectionActivity::loop() {
|
|||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
||||||
if (basepath != "/") {
|
if (basepath != "/") {
|
||||||
basepath = "/";
|
basepath = "/";
|
||||||
|
APP_STATE.lastBrowsedFolder = basepath;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
@ -123,6 +126,8 @@ void FileSelectionActivity::loop() {
|
|||||||
if (basepath.back() != '/') basepath += "/";
|
if (basepath.back() != '/') basepath += "/";
|
||||||
if (files[selectorIndex].back() == '/') {
|
if (files[selectorIndex].back() == '/') {
|
||||||
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
||||||
|
APP_STATE.lastBrowsedFolder = basepath;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
@ -134,6 +139,8 @@ void FileSelectionActivity::loop() {
|
|||||||
if (basepath != "/") {
|
if (basepath != "/") {
|
||||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||||
if (basepath.empty()) basepath = "/";
|
if (basepath.empty()) basepath = "/";
|
||||||
|
APP_STATE.lastBrowsedFolder = basepath;
|
||||||
|
APP_STATE.saveToFile();
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "ReaderActivity.h"
|
#include "ReaderActivity.h"
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "CrossPointState.h"
|
||||||
#include "CoverArtPickerActivity.h"
|
#include "CoverArtPickerActivity.h"
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
#include "EpubReaderActivity.h"
|
#include "EpubReaderActivity.h"
|
||||||
@ -92,8 +93,22 @@ void ReaderActivity::onSelectBookFile(const std::string& path) {
|
|||||||
|
|
||||||
void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) {
|
void ReaderActivity::onGoToFileSelection(const std::string& fromBookPath) {
|
||||||
exitActivity();
|
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
|
// Check if cover art picker is enabled
|
||||||
if (SETTINGS.useCoverArtPicker) {
|
if (SETTINGS.useCoverArtPicker) {
|
||||||
|
|||||||
@ -11,13 +11,13 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "XtcReaderChapterSelectionActivity.h"
|
#include "XtcReaderChapterSelectionActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int pagesPerRefresh = 15;
|
|
||||||
constexpr unsigned long skipPageMs = 700;
|
constexpr unsigned long skipPageMs = 700;
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
constexpr unsigned long goHomeMs = 1000;
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -266,7 +266,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
// Display BW with conditional refresh based on pagesUntilFullRefresh
|
// Display BW with conditional refresh based on pagesUntilFullRefresh
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = pagesPerRefresh;
|
pagesUntilFullRefresh = SETTINGS.getRefreshIntervalPages();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
@ -346,7 +346,7 @@ void XtcReaderActivity::renderPage() {
|
|||||||
// Display with appropriate refresh
|
// Display with appropriate refresh
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
if (pagesUntilFullRefresh <= 1) {
|
||||||
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
pagesUntilFullRefresh = pagesPerRefresh;
|
pagesUntilFullRefresh = SETTINGS.getRefreshIntervalPages();
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
pagesUntilFullRefresh--;
|
pagesUntilFullRefresh--;
|
||||||
|
|||||||
195
src/activities/settings/FolderPickerActivity.cpp
Normal file
195
src/activities/settings/FolderPickerActivity.cpp
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
#include "FolderPickerActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int PAGE_ITEMS = 23;
|
||||||
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void sortFolderList(std::vector<std::string>& 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<FolderPickerActivity*>(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<std::string> 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();
|
||||||
|
}
|
||||||
39
src/activities/settings/FolderPickerActivity.h
Normal file
39
src/activities/settings/FolderPickerActivity.h
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
|
class FolderPickerActivity final : public Activity {
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
std::string basepath = "/";
|
||||||
|
std::vector<std::string> folders;
|
||||||
|
int selectorIndex = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
unsigned long entryTime = 0;
|
||||||
|
const std::function<void(const std::string&)> onSelect;
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
void loadFolders();
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FolderPickerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::function<void(const std::string&)>& onSelect,
|
||||||
|
const std::function<void()>& 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;
|
||||||
|
};
|
||||||
@ -3,13 +3,14 @@
|
|||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "FolderPickerActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
// Define the static settings list
|
// Define the static settings list
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int settingsCount = 14;
|
constexpr int settingsCount = 17;
|
||||||
const SettingInfo settingsList[settingsCount] = {
|
const SettingInfo settingsList[settingsCount] = {
|
||||||
// Should match with SLEEP_SCREEN_MODE
|
// Should match with SLEEP_SCREEN_MODE
|
||||||
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||||
@ -34,11 +35,20 @@ const SettingInfo settingsList[settingsCount] = {
|
|||||||
{"Bookerly", "Noto Sans", "Open Dyslexic"}},
|
{"Bookerly", "Noto Sans", "Open Dyslexic"}},
|
||||||
{"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}},
|
{"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}},
|
||||||
{"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}},
|
{"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, {}},
|
{"Cover Art Picker", SettingType::TOGGLE, &CrossPointSettings::useCoverArtPicker, {}},
|
||||||
{"Auto Sleep Timeout",
|
{"Auto Sleep Timeout",
|
||||||
SettingType::ENUM,
|
SettingType::ENUM,
|
||||||
&CrossPointSettings::autoSleepMinutes,
|
&CrossPointSettings::autoSleepMinutes,
|
||||||
{"2 min", "5 min", "10 min", "15 min", "20 min", "30 min", "60 min", "Never"}},
|
{"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, {}},
|
{"Bluetooth", SettingType::TOGGLE, &CrossPointSettings::bluetoothEnabled, {}},
|
||||||
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
||||||
};
|
};
|
||||||
@ -140,6 +150,23 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
xSemaphoreGive(renderingMutex);
|
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 {
|
} else {
|
||||||
// Only toggle if it's a toggle type and has a value pointer
|
// 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) {
|
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
||||||
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
||||||
valueText = settingsList[i].enumValues[value];
|
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());
|
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);
|
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), i != selectedSettingIndex);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user