diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c3866016..ee374b73 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,9 +1,22 @@ ## Summary -* **What is the goal of this PR?** (e.g., Fixes a bug in the user authentication module, Implements the new feature for - file uploading.) +* **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) * **What changes are included?** ## Additional Context -* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). +* Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, + specific areas to focus on). + +--- + +### AI Usage + +While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it +helps set the right context for reviewers. + +Did you use AI tools to help write this code? + +- [ ] Yes +- [ ] Partially +- [ ] No diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be9a6e59..286f14aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,11 @@ name: CI jobs: build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 with: submodules: recursive + - uses: actions/setup-python@v6 with: python-version: '3.14' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8d1c830..df8d6679 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,17 +7,18 @@ on: jobs: build-release: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 with: submodules: recursive + - uses: actions/cache@v5 with: path: | ~/.cache/pip ~/.platformio/.cache key: ${{ runner.os }}-pio + - uses: actions/setup-python@v6 with: python-version: '3.14' diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 1af7e129..70a765ba 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -67,6 +67,7 @@ The Settings screen allows you to configure the device's behavior. There are a f - "Light" - The same default sleep screen, on a white background - "Custom" - Custom images from the SD card, see [Sleep Screen](#36-sleep-screen) below for more information - "Cover" - The book cover image (Note: this is experimental and may not work as expected) + - "Blank" - A blank screen - **Status Bar**: Configure the status bar displayed while reading: - "None" - No status bar - "No Progress" - Show status bar without reading progress diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 234344d7..9c4b058b 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -345,11 +345,14 @@ const std::string& Epub::getAuthor() const { return bookMetadataCache->coreMetadata.author; } -std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } +std::string Epub::getCoverBmpPath(bool cropped) const { + const auto coverFileName = "cover" + cropped ? "_crop" : ""; + return cachePath + "/" + coverFileName + ".bmp"; +} -bool Epub::generateCoverBmp() const { +bool Epub::generateCoverBmp(bool cropped) const { // Already generated, return true - if (SdMan.exists(getCoverBmpPath().c_str())) { + if (SdMan.exists(getCoverBmpPath(cropped).c_str())) { return true; } @@ -381,7 +384,7 @@ bool Epub::generateCoverBmp() const { } FsFile coverBmp; - if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) { + if (!SdMan.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { coverJpg.close(); return false; } @@ -392,7 +395,7 @@ bool Epub::generateCoverBmp() const { if (!success) { Serial.printf("[%lu] [EBP] Failed to generate BMP from JPG cover image\n", millis()); - SdMan.remove(getCoverBmpPath().c_str()); + SdMan.remove(getCoverBmpPath(cropped).c_str()); } Serial.printf("[%lu] [EBP] Generated BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); return success; diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index a6555e7e..047c955a 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -44,8 +44,8 @@ class Epub { const std::string& getPath() const; const std::string& getTitle() const; const std::string& getAuthor() const; - std::string getCoverBmpPath() const; - bool generateCoverBmp() const; + std::string getCoverBmpPath(bool cropped = false) const; + bool generateCoverBmp(bool cropped = false) const; uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr, bool trailingNullByte = false) const; bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index cd8b56f7..1ca9ea74 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -46,6 +46,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, sleepScreenCoverMode); serialization::writeString(outputFile, std::string(opdsServerUrl)); serialization::writePod(outputFile, textAntiAliasing); + serialization::writePod(outputFile, hideBatteryPercentage); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -110,6 +111,8 @@ bool CrossPointSettings::loadFromFile() { } serialization::readPod(inputFile, textAntiAliasing); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, hideBatteryPercentage); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 3a2a3503..d5f91039 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -52,6 +52,12 @@ class CrossPointSettings { // E-ink refresh frequency (pages between full refreshes) enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 }; + // Short power button press actions + enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 }; + + // Hide battery percentage + enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -61,8 +67,8 @@ class CrossPointSettings { // Text rendering settings uint8_t extraParagraphSpacing = 1; uint8_t textAntiAliasing = 1; - // Duration of the power button press - uint8_t shortPwrBtn = 0; + // Short power button click behaviour + uint8_t shortPwrBtn = IGNORE; // EPUB reading orientation settings // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise uint8_t orientation = PORTRAIT; @@ -82,13 +88,17 @@ class CrossPointSettings { uint8_t screenMargin = 5; // OPDS browser settings char opdsServerUrl[128] = ""; + // Hide battery percentage + uint8_t hideBatteryPercentage = HIDE_NEVER; ~CrossPointSettings() = default; // Get singleton instance static CrossPointSettings& getInstance() { return instance; } - uint16_t getPowerButtonDuration() const { return shortPwrBtn ? 10 : 400; } + uint16_t getPowerButtonDuration() const { + return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400; + } int getReaderFontId() const; bool saveToFile() const; diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index 3c359c0e..42b6ef7b 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -8,10 +8,11 @@ #include "Battery.h" #include "fontIds.h" -void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top) { +void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top, + const bool showPercentage) { // Left aligned battery icon and percentage const uint16_t percentage = battery.readPercentage(); - const auto percentageText = std::to_string(percentage) + "%"; + const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : ""; renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str()); // 1 column on left, 2 columns on right, 5 columns of battery body diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index d938beea..150fb0c8 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -7,7 +7,7 @@ class GfxRenderer; class ScreenComponents { public: - static void drawBattery(const GfxRenderer& renderer, int left, int top); + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); /** * Draw a progress bar with percentage text. diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 43e8e60b..7c79fcb1 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -199,6 +199,7 @@ void SleepActivity::renderCoverSleepScreen() const { } std::string coverBmpPath; + bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP; if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") || StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) { @@ -223,12 +224,12 @@ void SleepActivity::renderCoverSleepScreen() const { return renderDefaultSleepScreen(); } - if (!lastEpub.generateCoverBmp()) { + if (!lastEpub.generateCoverBmp(cropped)) { Serial.println("[SLP] Failed to generate cover bmp"); return renderDefaultSleepScreen(); } - coverBmpPath = lastEpub.getCoverBmpPath(); + coverBmpPath = lastEpub.getCoverBmpPath(cropped); } else { return renderDefaultSleepScreen(); } diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 8a363b25..cd355632 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -26,7 +26,7 @@ void OpdsBookBrowserActivity::taskTrampoline(void* param) { } void OpdsBookBrowserActivity::onEnter() { - Activity::onEnter(); + ActivityWithSubactivity::onEnter(); renderingMutex = xSemaphoreCreateMutex(); state = BrowserState::CHECK_WIFI; @@ -50,7 +50,7 @@ void OpdsBookBrowserActivity::onEnter() { } void OpdsBookBrowserActivity::onExit() { - Activity::onExit(); + ActivityWithSubactivity::onExit(); // Turn off WiFi when exiting WiFi.mode(WIFI_OFF); @@ -67,18 +67,28 @@ void OpdsBookBrowserActivity::onExit() { } void OpdsBookBrowserActivity::loop() { - if (subActivity) { - subActivity->loop(); + // Handle WiFi selection subactivity + if (state == BrowserState::WIFI_SELECTION) { + ActivityWithSubactivity::loop(); return; } // Handle error state - Confirm retries, Back goes back or home if (state == BrowserState::ERROR) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - state = BrowserState::LOADING; - statusMessage = "Loading..."; - updateRequired = true; - fetchFeed(currentPath); + // Check if WiFi is still connected + if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { + // WiFi connected - just retry fetching the feed + Serial.printf("[%lu] [OPDS] Retry: WiFi connected, retrying fetch\n", millis()); + state = BrowserState::LOADING; + statusMessage = "Loading..."; + updateRequired = true; + fetchFeed(currentPath); + } else { + // WiFi not connected - launch WiFi selection + Serial.printf("[%lu] [OPDS] Retry: WiFi not connected, launching selection\n", millis()); + launchWifiSelection(); + } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { navigateBack(); } @@ -362,11 +372,20 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { } void OpdsBookBrowserActivity::checkAndConnectWifi() { + // Already connected? Verify connection is valid by checking IP + if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { + state = BrowserState::LOADING; + statusMessage = "Loading..."; + updateRequired = true; + fetchFeed(currentPath); + return; + } + + // Try to connect to default WiFi if available WIFI_STORE.loadFromFile(); const bool hasDefaultSSID = !WIFI_STORE.getDefaultSSID().empty(); - const bool alreadyConnected = (WiFi.status() == WL_CONNECTED); - if (hasDefaultSSID && !alreadyConnected) { + if (hasDefaultSSID) { statusMessage = "Connecting to WiFi..."; updateRequired = true; } @@ -379,5 +398,37 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() { updateRequired = true; fetchFeed(currentPath); }, - [this]() { onGoHome(); }); + [this]() { + // User cancelled WiFi selection - go home + onGoHome(); + }); +} + +void OpdsBookBrowserActivity::launchWifiSelection() { + state = BrowserState::WIFI_SELECTION; + updateRequired = true; + + enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, + [this](const bool connected) { onWifiSelectionComplete(connected); })); +} + +void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { + exitActivity(); + + if (connected) { + Serial.printf("[%lu] [OPDS] WiFi connected via selection, fetching feed\n", millis()); + state = BrowserState::LOADING; + statusMessage = "Loading..."; + updateRequired = true; + fetchFeed(currentPath); + } else { + Serial.printf("[%lu] [OPDS] WiFi selection cancelled/failed\n", millis()); + // Force disconnect to ensure clean state for next retry + // This prevents stale connection status from interfering + WiFi.disconnect(); + WiFi.mode(WIFI_OFF); + state = BrowserState::ERROR; + errorMessage = "WiFi connection failed"; + updateRequired = true; + } } diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index bc702c9e..b08d9c2a 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -13,15 +13,17 @@ /** * Activity for browsing and downloading books from an OPDS server. * Supports navigation through catalog hierarchy and downloading EPUBs. + * When WiFi connection fails, launches WiFi selection to let user connect. */ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { public: enum class BrowserState { - CHECK_WIFI, // Checking WiFi connection - LOADING, // Fetching OPDS feed - BROWSING, // Displaying entries (navigation or books) - DOWNLOADING, // Downloading selected EPUB - ERROR // Error state with message + CHECK_WIFI, // Checking WiFi connection + WIFI_SELECTION, // WiFi selection subactivity is active + LOADING, // Fetching OPDS feed + BROWSING, // Displaying entries (navigation or books) + DOWNLOADING, // Downloading selected EPUB + ERROR // Error state with message }; explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, @@ -54,6 +56,8 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { void render() const; void checkAndConnectWifi(); + void launchWifiSelection(); + void onWifiSelectionComplete(bool connected); void fetchFeed(const std::string& path); void navigateToEntry(const OpdsEntry& entry); void navigateBack(); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index f34283d4..9c519d76 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -7,6 +7,7 @@ #include #include +#include "Battery.h" #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" @@ -332,8 +333,13 @@ void HomeActivity::render() const { const auto labels = mappedInput.mapLabels("", "Confirm", "Up", "Down"); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, "100 %"); - ScreenComponents::drawBattery(renderer, batteryX, 10); + const bool showBatteryPercentage = + SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; + // get percentage so we can align text properly + const uint16_t percentage = battery.readPercentage(); + const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : ""; + const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); + ScreenComponents::drawBattery(renderer, batteryX, 10, showBatteryPercentage); renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index ac0ffd51..f51cf9bf 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -152,6 +152,8 @@ void EpubReaderActivity::loop() { const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || + (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.wasReleased(MappedInputManager::Button::Power)) || mappedInput.wasReleased(MappedInputManager::Button::Right); if (!prevReleased && !nextReleased) { @@ -417,6 +419,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + const bool showBatteryPercentage = + SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; // Position status bar near the bottom of the logical screen, regardless of orientation const auto screenHeight = renderer.getScreenHeight(); @@ -437,7 +441,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in } if (showBattery) { - ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY); + ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage); } if (showChapterTitle) { diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 63f1e5a7..8f3ecb80 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -16,7 +16,9 @@ int EpubReaderChapterSelectionActivity::getPageItems() const { constexpr int lineHeight = 30; const int screenHeight = renderer.getScreenHeight(); - const int availableHeight = screenHeight - startY; + const int endY = screenHeight - lineHeight; + + const int availableHeight = endY - startY; int items = availableHeight / lineHeight; // Ensure we always have at least one item per page to avoid division by zero @@ -134,5 +136,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() { tocIndex != selectorIndex); } + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index c0580cf6..9cdf5c97 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -112,6 +112,8 @@ void XtcReaderActivity::loop() { const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || mappedInput.wasReleased(MappedInputManager::Button::Left); const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || + (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.wasReleased(MappedInputManager::Button::Power)) || mappedInput.wasReleased(MappedInputManager::Button::Right); if (!prevReleased && !nextReleased) { diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index fd732924..b2cfecaa 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -14,7 +14,9 @@ int XtcReaderChapterSelectionActivity::getPageItems() const { constexpr int lineHeight = 30; const int screenHeight = renderer.getScreenHeight(); - const int availableHeight = screenHeight - startY; + const int endY = screenHeight - lineHeight; + + const int availableHeight = endY - startY; int items = availableHeight / lineHeight; if (items < 1) { items = 1; @@ -147,5 +149,8 @@ void XtcReaderChapterSelectionActivity::renderScreen() { renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex); } + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 6aec0b6a..b7b7d393 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -14,15 +14,16 @@ // Define the static settings list namespace { -constexpr int settingsCount = 19; +constexpr int settingsCount = 20; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), + SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), - SettingInfo::Toggle("Short Power Button Click", &CrossPointSettings::shortPwrBtn), + SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"}), SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,