crosspoint-reader/src/activities/settings/SettingsActivity.cpp

253 lines
10 KiB
C++
Raw Normal View History

#include "SettingsActivity.h"
#include <GfxRenderer.h>
#include <HardwareSerial.h>
2026-01-23 03:54:39 -05:00
#include <builtinFonts/custom/customFonts.h>
#include "CategorySettingsActivity.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
Aleo, Noto Sans, Open Dyslexic fonts (#163) ## Summary * Swap out Bookerly font due to licensing issues, replace default font with Aleo * I did a bunch of searching around for a nice replacement font, and this trumped several other like Literata, Merriwether, Vollkorn, etc * Add Noto Sans, and Open Dyslexic as font options * They can be selected in the settings screen * Add font size options (Small, Medium, Large, Extra Large) * Adjustable in settings * Swap out uses of reader font in headings and replaced with slightly larger Ubuntu font * Replaced PixelArial14 font as it was difficult to track down, replace with Space Grotesk * Remove auto formatting on generated font files * Massively speeds up formatting step now that there is a lot more CPP font source * Include fonts with their licenses in the repo ## Additional Context Line compression setting will follow | Font | Small | Medium | Large | X Large | | --- | --- | --- | --- | --- | | Aleo | ![IMG_5704](https://github.com/user-attachments/assets/7acb054f-ddef-4080-b3c8-590cfaf13115) | ![IMG_5705](https://github.com/user-attachments/assets/d4819036-5c89-486e-92c3-86094fa4d89a) | ![IMG_5706](https://github.com/user-attachments/assets/35caf622-d126-4396-9c3e-f927eba1e1f4) | ![IMG_5707](https://github.com/user-attachments/assets/af32370a-6244-400f-bea9-5c27db040b5b) | | Noto Sans | ![IMG_5708](https://github.com/user-attachments/assets/1f9264a5-c069-4e22-9099-a082bfcaabc5) | ![IMG_5709](https://github.com/user-attachments/assets/ef6b07fe-8d87-403a-b152-05f50b69b78e) | ![IMG_5710](https://github.com/user-attachments/assets/112a5d20-262c-4dc0-b67d-980b237e4607) | ![IMG_5711](https://github.com/user-attachments/assets/d25e0e1d-2ace-450d-96dd-618e4efd4805) | | Open Dyslexic | ![IMG_5712](https://github.com/user-attachments/assets/ead64690-f261-4fae-a4a2-0becd1162e2d) | ![IMG_5713](https://github.com/user-attachments/assets/59d60f7d-5142-4591-96b0-c04e0a4c6436) | ![IMG_5714](https://github.com/user-attachments/assets/bb6652cd-1790-46a3-93ea-2b8f70d0d36d) | ![IMG_5715](https://github.com/user-attachments/assets/496e7eb4-c81a-4232-83e9-9ba9148fdea4) |
2025-12-30 18:21:47 +10:00
#include "fontIds.h"
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace {
2026-01-27 21:40:52 -05:00
// Visibility condition for bezel edge setting (only show when compensation > 0)
bool isBezelCompensationEnabled() { return SETTINGS.bezelCompensation > 0; }
constexpr int displaySettingsCount = 9;
const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
2026-01-22 13:13:12 -05:00
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop", "Actual"}),
feat: Add support to B&W filters to image covers (#476) * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implementation of a new feature in Display options as Image Filter * **What changes are included?** Black & White and Inverted Black & White options are added. Here are some examples: | None | Contrast | Inverted | | --- | --- | --- | | <img alt="image" src="https://github.com/user-attachments/assets/fe02dd9b-f647-41bd-8495-c262f73177c4" /> | <img alt="image" src="https://github.com/user-attachments/assets/2d17747d-3ff6-48a9-b9b9-eb17cccf19cf" /> | <img alt="image" src="https://github.com/user-attachments/assets/792dea50-f003-4634-83fe-77849ca49095" /> | | <img alt="image" src="https://github.com/user-attachments/assets/28395b63-14f8-41e2-886b-8ddf3faeafc4" /> | <img alt="image" src="https://github.com/user-attachments/assets/71a569c8-fc54-4647-ad4c-ec96e220cddb" /> | <img alt="image" src="https://github.com/user-attachments/assets/9139e32c-9175-433e-8372-45fa042d3dc9" /> | * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). I have also tried adding Color inversion, but could not see much difference with that. It might be because my implementation was wrong. --- 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? _** PARTIALLY **_ --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-27 13:21:59 +00:00
SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter,
{"None", "Contrast", "Inverted"}),
SettingInfo::Toggle("High Contrast", &CrossPointSettings::displayContrast),
2026-01-20 00:56:28 +00:00
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar,
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
2026-01-27 21:40:52 -05:00
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Value("Bezel Compensation", &CrossPointSettings::bezelCompensation, {0, 10, 1}),
SettingInfo::Enum("Bezel Edge", &CrossPointSettings::bezelCompensationEdge,
{"Bottom", "Top", "Left", "Right"}, isBezelCompensationEnabled)};
2026-01-23 03:54:39 -05:00
// Helper to get custom font names as a vector
std::vector<std::string> getCustomFontNamesVector() {
std::vector<std::string> names;
#if CUSTOM_FONT_COUNT > 0
for (int i = 0; i < CUSTOM_FONT_COUNT; i++) {
names.push_back(CUSTOM_FONT_NAMES[i]);
}
#else
names.push_back("(none)"); // Placeholder when no custom fonts
#endif
return names;
}
// Visibility condition for custom font settings
bool isCustomFontSelected() { return SETTINGS.fontFamily == CrossPointSettings::CUSTOM_FONT; }
// Font family options - includes "Custom" only if custom fonts are available
std::vector<std::string> getFontFamilyOptions() {
std::vector<std::string> options = {"Bookerly", "Noto Sans"};
#if CUSTOM_FONT_COUNT > 0
options.push_back("Custom");
#endif
return options;
}
constexpr int readerSettingsCount = 11;
const SettingInfo readerSettings[readerSettingsCount] = {
2026-01-23 03:54:39 -05:00
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, getFontFamilyOptions()),
SettingInfo::Enum("Custom Font", &CrossPointSettings::customFontIndex, getCustomFontNamesVector(),
isCustomFontSelected),
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily,
{"Bookerly", "Noto Sans"}, isCustomFontSelected),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
{"Justify", "Left", "Center", "Right"}),
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = {
SettingInfo::Enum(
"Front Button Layout", &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
2026-01-22 12:42:01 -05:00
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn,
{"Ignore", "Sleep", "Page Turn", "Dictionary", "Quick Menu"})};
2026-01-23 03:54:39 -05:00
constexpr int systemSettingsCount = 4;
const SettingInfo systemSettings[systemSettingsCount] = {
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
2026-01-23 03:54:39 -05:00
SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
SettingInfo::Action("Check for updates")};
} // namespace
void SettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<SettingsActivity*>(param);
self->displayTaskLoop();
}
void SettingsActivity::onEnter() {
Activity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Reset selection to first category
selectedCategoryIndex = 0;
// Trigger first update
updateRequired = true;
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void SettingsActivity::onExit() {
ActivityWithSubactivity::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;
2026-01-26 13:56:36 -05:00
vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void SettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
// Handle category selection
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
enterCategory(selectedCategoryIndex);
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
SETTINGS.saveToFile();
onGoHome();
return;
}
// Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
// Move selection up (with wrap-around)
selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
// Move selection down (with wrap around)
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
updateRequired = true;
}
}
void SettingsActivity::enterCategory(int categoryIndex) {
if (categoryIndex < 0 || categoryIndex >= categoryCount) {
return;
}
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
const SettingInfo* settingsList = nullptr;
int settingsCount = 0;
switch (categoryIndex) {
case 0: // Display
settingsList = displaySettings;
settingsCount = displaySettingsCount;
break;
case 1: // Reader
settingsList = readerSettings;
settingsCount = readerSettingsCount;
break;
case 2: // Controls
settingsList = controlsSettings;
settingsCount = controlsSettingsCount;
break;
case 3: // System
settingsList = systemSettings;
settingsCount = systemSettingsCount;
break;
Add connect to Wifi and File Manager Webserver (#41) ## Summary - **What is the goal of this PR?** Implements wireless EPUB file management via a built-in web server, enabling users to upload, browse, organize, and delete EPUB files from any device on the same WiFi network without needing a computer cable connection. - **What changes are included?** - **New Web Server** ([`CrossPointWebServer.cpp`](src/CrossPointWebServer.cpp), [`CrossPointWebServer.h`](src/CrossPointWebServer.h)): - HTTP server on port 80 with a responsive HTML/CSS interface - Home page showing device status (version, IP, free memory) - File Manager with folder navigation and breadcrumb support - EPUB file upload with progress tracking - Folder creation and file/folder deletion - XSS protection via HTML escaping - Hidden system folders (`.` prefixed, "System Volume Information", "XTCache") - **WiFi Screen** ([`WifiScreen.cpp`](src/screens/WifiScreen.cpp), [`WifiScreen.h`](src/screens/WifiScreen.h)): - Network scanning with signal strength indicators - Visual indicators for encrypted (`*`) and saved (`+`) networks - State machine managing: scanning, network selection, password entry, connecting, save/forget prompts - 15-second connection timeout handling - Integration with web server (starts on connect, stops on exit) - **WiFi Credential Storage** ([`WifiCredentialStore.cpp`](src/WifiCredentialStore.cpp), [`WifiCredentialStore.h`](src/WifiCredentialStore.h)): - Persistent storage in `/sd/.crosspoint/wifi.bin` - XOR obfuscation for stored passwords (basic protection against casual reading) - Up to 8 saved networks with add/remove/update operations - **On-Screen Keyboard** ([`OnScreenKeyboard.cpp`](src/screens/OnScreenKeyboard.cpp), [`OnScreenKeyboard.h`](src/screens/OnScreenKeyboard.h)): - Reusable QWERTY keyboard component with shift support - Special keys: Shift, Space, Backspace, Done - Support for password masking mode - **Settings Screen Integration** ([`SettingsScreen.h`](src/screens/SettingsScreen.h)): - Added WiFi action to navigate to the new WiFi screen - **Documentation** ([`docs/webserver.md`](docs/webserver.md)): - Comprehensive user guide covering WiFi setup, web interface usage, file management, troubleshooting, and security notes - See this for more screenshots! - Working "displays the right way in GitHub" on my repo: https://github.com/olearycrew/crosspoint-reader/blob/feature/connect-to-wifi/docs/webserver.md **Video demo** https://github.com/user-attachments/assets/283e32dc-2d9f-4ae2-848e-01f41166a731 ## Additional Context - **Security considerations**: The web server has no authentication—anyone on the same WiFi network can access files. This is documented as a limitation, recommending use only on trusted private networks. Password obfuscation in the credential store is XOR-based, not cryptographically secure. - **Memory implications**: The web server and WiFi stack consume significant memory. The implementation properly cleans up (stops server, disconnects WiFi, sets `WIFI_OFF` mode) when exiting the WiFi screen to free resources. - **Async operations**: Network scanning and connection use async patterns with FreeRTOS tasks to prevent blocking the UI. The display task handles rendering on a dedicated thread with mutex protection. - **Browser compatibility**: The web interface uses standard HTML5/CSS3/JavaScript and is tested to work with all modern browsers on desktop and mobile. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-19 09:05:43 -05:00
}
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
settingsCount, [this] {
exitActivity();
updateRequired = true;
}));
xSemaphoreGive(renderingMutex);
}
void SettingsActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void SettingsActivity::render() const {
renderer.clearScreen();
Rotation Support (#77) • What is the goal of this PR? Implement a horizontal EPUB reading mode so books can be read in landscape orientation (both 90° and 270°), while keeping the rest of the UI in portrait. • What changes are included? ◦ Rendering / Display ▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal, LandscapeFlipped) and made: ▪ drawPixel, drawImage, displayWindow map logical coordinates differently depending on orientation. ▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical dimensions (480×800 in portrait, 800×480 in landscape). ◦ Settings / Configuration ▪ Extended CrossPointSettings with: ▪ landscapeReading (toggle for portrait vs. landscape EPUB reading). ▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal holding directions are supported). ▪ Updated settings serialization/deserialization to persist these fields while remaining backward‑compatible with existing settings files. ▪ Updated SettingsActivity to expose two new toggles: ▪ “Landscape Reading” ▪ “Flip Landscape (swap top/bottom)” ◦ EPUB Reader ▪ In EpubReaderActivity: ▪ On onEnter, set GfxRenderer orientation based on the new settings (Portrait, LandscapeNormal, or LandscapeFlipped). ▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings, etc. continue to render as before. ▪ Adjusted renderStatusBar to position the status bar and battery indicator relative to GfxRenderer::getScreenHeight() instead of hard‑coded Y coordinates, so it stays correctly at the bottom in both portrait and landscape. ◦ EPUB Caching / Layout ▪ Extended Section cache metadata (section.bin) to include the logical screenWidth and screenHeight used when pages were generated; bumped SECTION_FILE_VERSION. ▪ Updated loadCacheMetadata to compare: ▪ font/margins/line compression/extraParagraphSpacing and screen dimensions; mismatches now invalidate and clear the cache. ▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so portrait and landscape caches are kept separate and correctly sized. Additional Context • Cache behavior / migration ◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected as incompatible and their caches cleared and rebuilt once per chapter when first opened after this change. ◦ Within a given orientation, caches will be reused as before. Switching orientation (portrait ↔ landscape) will cause a one‑time re‑index of each chapter in the new orientation. • Scope and risks ◦ Orientation changes are scoped to the EPUB reader; the Home screen, Settings, WiFi selection, sleep screens, and web server UI continue to assume portrait orientation. ◦ The renderer’s orientation is a static/global setting; if future code uses GfxRenderer outside the reader while a reader instance is active, it should be aware that orientation is no longer implicitly fixed. ◦ All drawing primitives now go through orientation‑aware coordinate transforms; any code that previously relied on edge‑case behavior or out‑of‑bounds writes might surface as logged “Outside range” warnings instead. • Testing suggestions / areas to focus on ◦ Verify in hardware: ▪ Portrait mode still renders correctly (boot, home, settings, WiFi, reader). ▪ Landscape reading in both directions: ▪ Landscape Reading = ON, Flip Landscape = OFF. ▪ Landscape Reading = ON, Flip Landscape = ON. ▪ Status bar (page X/Y, % progress, battery icon) is fully visible and aligned at the bottom in all three combinations. ◦ Open the same book: ▪ In portrait first, then switch to landscape and reopen it. ▪ Confirm that: ▪ Old portrait caches are rebuilt once for landscape (you should see the “Indexing…” page). ▪ Progress save/restore still works (resume opens to the correct page in the current orientation). ◦ Ensure grayscale rendering (the secondary pass in EpubReaderActivity::renderContents) still looks correct in both orientations. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
2026-01-27 21:40:52 -05:00
// Bezel compensation offsets
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int bezelBottom = renderer.getBezelOffsetBottom();
// Draw header
2026-01-27 21:40:52 -05:00
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Settings", true, EpdFontFamily::BOLD);
// Draw selection
2026-01-27 21:40:52 -05:00
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedCategoryIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
// Draw all categories
for (int i = 0; i < categoryCount; i++) {
2026-01-27 21:40:52 -05:00
const int categoryY = 60 + bezelTop + i * 30; // 30 pixels between categories
// Draw category name
2026-01-27 21:40:52 -05:00
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, categoryY, categoryNames[i], i != selectedCategoryIndex);
}
// Draw version text above button hints
2026-01-27 21:40:52 -05:00
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
Aleo, Noto Sans, Open Dyslexic fonts (#163) ## Summary * Swap out Bookerly font due to licensing issues, replace default font with Aleo * I did a bunch of searching around for a nice replacement font, and this trumped several other like Literata, Merriwether, Vollkorn, etc * Add Noto Sans, and Open Dyslexic as font options * They can be selected in the settings screen * Add font size options (Small, Medium, Large, Extra Large) * Adjustable in settings * Swap out uses of reader font in headings and replaced with slightly larger Ubuntu font * Replaced PixelArial14 font as it was difficult to track down, replace with Space Grotesk * Remove auto formatting on generated font files * Massively speeds up formatting step now that there is a lot more CPP font source * Include fonts with their licenses in the repo ## Additional Context Line compression setting will follow | Font | Small | Medium | Large | X Large | | --- | --- | --- | --- | --- | | Aleo | ![IMG_5704](https://github.com/user-attachments/assets/7acb054f-ddef-4080-b3c8-590cfaf13115) | ![IMG_5705](https://github.com/user-attachments/assets/d4819036-5c89-486e-92c3-86094fa4d89a) | ![IMG_5706](https://github.com/user-attachments/assets/35caf622-d126-4396-9c3e-f927eba1e1f4) | ![IMG_5707](https://github.com/user-attachments/assets/af32370a-6244-400f-bea9-5c27db040b5b) | | Noto Sans | ![IMG_5708](https://github.com/user-attachments/assets/1f9264a5-c069-4e22-9099-a082bfcaabc5) | ![IMG_5709](https://github.com/user-attachments/assets/ef6b07fe-8d87-403a-b152-05f50b69b78e) | ![IMG_5710](https://github.com/user-attachments/assets/112a5d20-262c-4dc0-b67d-980b237e4607) | ![IMG_5711](https://github.com/user-attachments/assets/d25e0e1d-2ace-450d-96dd-618e4efd4805) | | Open Dyslexic | ![IMG_5712](https://github.com/user-attachments/assets/ead64690-f261-4fae-a4a2-0becd1162e2d) | ![IMG_5713](https://github.com/user-attachments/assets/59d60f7d-5142-4591-96b0-c04e0a4c6436) | ![IMG_5714](https://github.com/user-attachments/assets/bb6652cd-1790-46a3-93ea-2b8f70d0d36d) | ![IMG_5715](https://github.com/user-attachments/assets/496e7eb4-c81a-4232-83e9-9ba9148fdea4) |
2025-12-30 18:21:47 +10:00
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Always use standard refresh for settings screen
renderer.displayBuffer();
}