feat: Lyra screens (#732)

## Summary

Implements Lyra theme for some more Crosspoint screens:

![IMG_7960
Medium](https://github.com/user-attachments/assets/5d97d91d-e5eb-4296-bbf4-917e142d9095)
![IMG_7961
Medium](https://github.com/user-attachments/assets/02d61964-2632-45ff-83c7-48b95882eb9c)
![IMG_7962
Medium](https://github.com/user-attachments/assets/cf42d20f-3a85-4669-b497-1cac4653fa5a)
![IMG_7963
Medium](https://github.com/user-attachments/assets/a8f59c37-db70-407c-a06d-3e40613a0f55)
![IMG_7964
Medium](https://github.com/user-attachments/assets/0fdaac72-077a-48f6-a8c5-1cd806a58937)
![IMG_7965
Medium](https://github.com/user-attachments/assets/5169f037-8ba8-4488-9a8a-06f5146ec1d9)


## Additional Context

- A bit of refactoring for list scrolling logic

---

### 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? _**NO**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
CaptainFrito
2026-02-19 17:16:55 +07:00
committed by GitHub
parent c6ddc5d6a0
commit e7ee6ff05e
38 changed files with 787 additions and 551 deletions

View File

@@ -167,7 +167,6 @@ void MyLibraryActivity::loop() {
}
int listSize = static_cast<int>(files.size());
buttonNavigator.onNextRelease([this, listSize] {
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
requestUpdate();

View File

@@ -169,76 +169,62 @@ void CalibreConnectActivity::loop() {
}
void CalibreConnectActivity::render(Activity::RenderLock&&) {
if (state == CalibreConnectState::SERVER_RUNNING) {
renderer.clearScreen();
renderServerRunning();
renderer.displayBuffer();
return;
}
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CALIBRE_WIRELESS));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == CalibreConnectState::SERVER_STARTING) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CALIBRE_STARTING), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CALIBRE_STARTING));
} else if (state == CalibreConnectState::ERROR) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD);
} else if (state == CalibreConnectState::SERVER_RUNNING) {
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str(), (std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str());
int y = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 4;
const auto heightText12 = renderer.getTextHeight(UI_12_FONT_ID);
renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD);
y += heightText12 + metrics.verticalSpacing * 2;
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_INSTRUCTION_1));
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height, tr(STR_CALIBRE_INSTRUCTION_2));
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 2, tr(STR_CALIBRE_INSTRUCTION_3));
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 3, tr(STR_CALIBRE_INSTRUCTION_4));
y += height * 3 + metrics.verticalSpacing * 4;
renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD);
y += heightText12 + metrics.verticalSpacing * 2;
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
std::string label = tr(STR_CALIBRE_RECEIVING);
if (!currentUploadName.empty()) {
label += ": " + currentUploadName;
label = renderer.truncatedText(SMALL_FONT_ID, label.c_str(), pageWidth - metrics.contentSidePadding * 2,
EpdFontFamily::REGULAR);
}
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, label.c_str());
GUI.drawProgressBar(renderer,
Rect{metrics.contentSidePadding, y + height + metrics.verticalSpacing,
pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
lastProgressReceived, lastProgressTotal);
y += height + metrics.verticalSpacing * 2 + metrics.progressBarHeight;
}
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName;
msg = renderer.truncatedText(SMALL_FONT_ID, msg.c_str(), pageWidth - metrics.contentSidePadding * 2,
EpdFontFamily::REGULAR);
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, msg.c_str());
}
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
renderer.displayBuffer();
}
void CalibreConnectActivity::renderServerRunning() const {
constexpr int LINE_SPACING = 24;
constexpr int SMALL_SPACING = 20;
constexpr int SECTION_SPACING = 40;
constexpr int TOP_PADDING = 14;
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CALIBRE_WIRELESS), true, EpdFontFamily::BOLD);
int y = 55 + TOP_PADDING;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD);
y += LINE_SPACING;
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
}
renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING,
(std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str());
y += LINE_SPACING * 2 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD);
y += LINE_SPACING;
renderer.drawCenteredText(SMALL_FONT_ID, y, tr(STR_CALIBRE_INSTRUCTION_1));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, tr(STR_CALIBRE_INSTRUCTION_2));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, tr(STR_CALIBRE_INSTRUCTION_3));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, tr(STR_CALIBRE_INSTRUCTION_4));
y += SMALL_SPACING * 3 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD);
y += LINE_SPACING;
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
std::string label = tr(STR_CALIBRE_RECEIVING);
if (!currentUploadName.empty()) {
label += ": " + currentUploadName;
if (label.length() > 34) {
label.replace(31, label.length() - 31, "...");
}
}
renderer.drawCenteredText(SMALL_FONT_ID, y, label.c_str());
constexpr int barWidth = 300;
constexpr int barHeight = 16;
constexpr int barX = (480 - barWidth) / 2;
GUI.drawProgressBar(renderer, Rect{barX, y + 22, barWidth, barHeight}, lastProgressReceived, lastProgressTotal);
y += 40;
}
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName;
if (msg.length() > 36) {
msg.replace(33, msg.length() - 33, "...");
}
renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str());
}
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}

View File

@@ -24,6 +24,8 @@ constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
constexpr const char* AP_HOSTNAME = "crosspoint";
constexpr uint8_t AP_CHANNEL = 1;
constexpr uint8_t AP_MAX_CONNECTIONS = 4;
constexpr int QR_CODE_WIDTH = 6 * 33;
constexpr int QR_CODE_HEIGHT = 200;
// DNS server for captive portal (redirects all DNS queries to our IP)
DNSServer* dnsServer = nullptr;
@@ -339,14 +341,24 @@ void CrossPointWebServerActivity::loop() {
void CrossPointWebServerActivity::render(Activity::RenderLock&&) {
// Only render our own UI when server is running
// Subactivities handle their own rendering
if (state == WebServerActivityState::SERVER_RUNNING) {
renderer.clearScreen();
renderServerRunning();
renderer.displayBuffer();
} else if (state == WebServerActivityState::AP_STARTING) {
if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_STARTING_HOTSPOT), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
if (state == WebServerActivityState::SERVER_RUNNING) {
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());
renderServerRunning();
} else {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_STARTING_HOTSPOT));
}
renderer.displayBuffer();
}
}
@@ -374,66 +386,70 @@ void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std
}
void CrossPointWebServerActivity::renderServerRunning() const {
// Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());
int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2;
int height10 = renderer.getLineHeight(UI_10_FONT_ID);
if (isApMode) {
// AP mode display - center the content block
int startY = 55;
// AP mode display
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_CONNECT_WIFI_HINT), true,
EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_HOTSPOT_MODE), true, EpdFontFamily::BOLD);
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, tr(STR_CONNECT_WIFI_HINT));
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, tr(STR_SCAN_QR_WIFI_HINT));
// Show QR code for URL
// Show QR code for Wifi
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig);
// Show network name
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
connectedSSID.c_str());
startY += QR_CODE_HEIGHT + 2 * metrics.verticalSpacing;
startY += 6 * 29 + 3 * LINE_SPACING;
// Show primary URL (hostname)
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_OPEN_URL_HINT), true,
EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/";
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD);
std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/";
// Show QR code for URL
drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl);
// Show IP address as fallback
std::string ipUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + connectedIP + "/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_OPEN_URL_HINT));
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
hostnameUrl.c_str());
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100,
ipUrl.c_str());
} else {
startY += metrics.verticalSpacing * 2;
// STA mode display (original behavior)
// std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_OPEN_URL_HINT), true, EpdFontFamily::BOLD);
startY += height10;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_SCAN_QR_HINT), true, EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
// Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, tr(STR_SCAN_QR_HINT));
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl);
} else {
// STA mode display (original behavior)
const int startY = 65;
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
}
renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str());
std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str());
std::string webInfo = "http://" + connectedIP + "/";
drawQRCode(renderer, (pageWidth - QR_CODE_WIDTH) / 2, startY, webInfo);
startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2;
// Show web server URL prominently
std::string webInfo = "http://" + connectedIP + "/";
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, startY, webInfo.c_str(), true);
startY += height10 + 5;
// Also show hostname URL
std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, tr(STR_OPEN_URL_HINT));
// Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_SCAN_QR_HINT));
renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true);
}
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");

View File

@@ -57,39 +57,24 @@ void NetworkModeSelectionActivity::loop() {
void NetworkModeSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD);
// Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, tr(STR_HOW_CONNECT));
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FILE_TRANSFER));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
// Menu items and descriptions
static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS,
StrId::STR_CREATE_HOTSPOT};
static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC,
StrId::STR_HOTSPOT_DESC};
// Draw menu items centered on screen
constexpr int itemHeight = 50; // Height for each menu item (including description)
const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10;
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
const int itemY = startY + i * itemHeight;
const bool isSelected = (i == selectedIndex);
// Draw selection highlight (black fill) for selected item
if (isSelected) {
renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6);
}
// Draw text: black=false (white text) when selected (on black background)
// black=true (black text) when not selected (on white background)
renderer.drawText(UI_10_FONT_ID, 30, itemY, I18N.get(menuItems[i]), /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, I18N.get(menuDescs[i]), /*black=*/!isSelected);
}
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEM_COUNT), selectedIndex,
[](int index) { return std::string(I18N.get(menuItems[index])); },
[](int index) { return std::string(I18N.get(menuDescs[index])); });
// Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));

View File

@@ -149,13 +149,12 @@ void WifiSelectionActivity::processWifiScanResults() {
networks.push_back(pair.second);
}
// Sort by signal strength (strongest first)
std::sort(networks.begin(), networks.end(),
[](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; });
// Show networks with PW first
// Sort: saved-password networks first, then by signal strength (strongest first)
std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) {
return a.hasSavedPassword && !b.hasSavedPassword;
if (a.hasSavedPassword != b.hasSavedPassword) {
return a.hasSavedPassword;
}
return a.rssi > b.rssi;
});
WiFi.scanDelete();
@@ -194,7 +193,6 @@ void WifiSelectionActivity::selectNetwork(const int index) {
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD),
"", // No initial text
50, // Y position
64, // Max password length
false, // Show password by default (hard keyboard to use)
[this](const std::string& text) {
@@ -455,15 +453,12 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi
return "||||"; // Excellent
}
if (rssi >= -60) {
return "||| "; // Good
return " |||"; // Good
}
if (rssi >= -70) {
return "|| "; // Fair
return " ||"; // Fair
}
if (rssi >= -80) {
return "| "; // Weak
}
return " "; // Very weak
return " |"; // Very weak
}
void WifiSelectionActivity::render(Activity::RenderLock&&) {
@@ -476,6 +471,18 @@ void WifiSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
char countStr[32];
snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size());
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_WIFI_NETWORKS),
countStr);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
cachedMacAddress.c_str());
switch (state) {
case WifiSelectionState::AUTO_CONNECTING:
renderConnecting();
@@ -507,12 +514,10 @@ void WifiSelectionActivity::render(Activity::RenderLock&&) {
}
void WifiSelectionActivity::renderNetworkList() const {
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD);
if (networks.empty()) {
// No networks found or scan failed
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
@@ -520,69 +525,21 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS));
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN));
} else {
// Calculate how many networks we can display
constexpr int startY = 60;
constexpr int lineHeight = 25;
const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight;
// Calculate scroll offset to keep selected item visible
int scrollOffset = 0;
if (selectedNetworkIndex >= maxVisibleNetworks) {
scrollOffset = selectedNetworkIndex - maxVisibleNetworks + 1;
}
// Draw networks
int displayIndex = 0;
for (size_t i = scrollOffset; i < networks.size() && displayIndex < maxVisibleNetworks; i++, displayIndex++) {
const int networkY = startY + displayIndex * lineHeight;
const auto& network = networks[i];
// Draw selection indicator
if (static_cast<int>(i) == selectedNetworkIndex) {
renderer.drawText(UI_10_FONT_ID, 5, networkY, ">");
}
// Draw network name (truncate if too long)
std::string displayName = network.ssid;
if (displayName.length() > 33) {
displayName.replace(30, displayName.length() - 30, "...");
}
renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str());
// Draw signal strength indicator
std::string signalStr = getSignalStrengthIndicator(network.rssi);
renderer.drawText(UI_10_FONT_ID, pageWidth - 90, networkY, signalStr.c_str());
// Draw saved indicator (checkmark) for networks with saved passwords
if (network.hasSavedPassword) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 50, networkY, "+");
}
// Draw lock icon for encrypted networks
if (network.isEncrypted) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 30, networkY, "*");
}
}
// Draw scroll indicators if needed
if (scrollOffset > 0) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^");
}
if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v");
}
// Show network count
char countStr[64];
snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size());
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(networks.size()),
selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr,
[this](int index) {
auto network = networks[index];
return std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") +
getSignalStrengthIndicator(network.rssi);
});
}
// Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, tr(STR_NETWORK_LEGEND));
GUI.drawHelpText(renderer,
Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20},
tr(STR_NETWORK_LEGEND));
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : "";

View File

@@ -45,7 +45,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
ButtonNavigator buttonNavigator;
WifiSelectionState state = WifiSelectionState::SCANNING;
int selectedNetworkIndex = 0;
size_t selectedNetworkIndex = 0;
std::vector<WifiNetworkInfo> networks;
const std::function<void(bool connected)> onComplete;

View File

@@ -95,9 +95,6 @@ void ButtonRemapActivity::loop() {
}
void ButtonRemapActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* {
for (uint8_t i = 0; i < kRoleCount; i++) {
if (tempMapping[i] == hardwareIndex) {
@@ -107,35 +104,41 @@ void ButtonRemapActivity::render(Activity::RenderLock&&) {
return "-";
};
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_REMAP_FRONT_BUTTONS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_REMAP_PROMPT));
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
for (uint8_t i = 0; i < kRoleCount; i++) {
const int y = 70 + i * 30;
const bool isSelected = (i == currentStep);
renderer.clearScreen();
// Highlight the role that is currently being assigned.
if (isSelected) {
renderer.fillRect(0, y - 2, pageWidth - 1, 30);
}
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_REMAP_FRONT_BUTTONS));
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
tr(STR_REMAP_PROMPT));
const char* roleName = getRoleName(i);
renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected);
// Show currently assigned hardware button (or unassigned).
const char* assigned = (tempMapping[i] == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(tempMapping[i]);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected);
}
int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
GUI.drawList(
renderer, Rect{0, topOffset, pageWidth, contentHeight}, kRoleCount, currentStep,
[&](int index) { return getRoleName(static_cast<uint8_t>(index)); }, nullptr, nullptr,
[&](int index) {
uint8_t assignedButton = tempMapping[static_cast<uint8_t>(index)];
return (assignedButton == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(assignedButton);
},
true);
// Temporary warning banner for duplicates.
if (!errorMessage.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true);
GUI.drawHelpText(renderer,
Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20},
errorMessage.c_str());
}
// Provide side button actions at the bottom of the screen (split across two lines).
renderer.drawCenteredText(SMALL_FONT_ID, 250, tr(STR_REMAP_RESET_HINT), true);
renderer.drawCenteredText(SMALL_FONT_ID, 280, tr(STR_REMAP_CANCEL_HINT), true);
GUI.drawHelpText(renderer,
Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20},
tr(STR_REMAP_RESET_HINT));
GUI.drawHelpText(renderer,
Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20},
tr(STR_REMAP_CANCEL_HINT));
// Live preview of logical labels under front buttons.
// This mirrors the on-device front button order: Back, Confirm, Left, Right.

View File

@@ -58,7 +58,7 @@ void CalibreSettingsActivity::handleSelection() {
// OPDS Server URL
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 10,
renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl,
127, // maxLength
false, // not password
[this](const std::string& url) {
@@ -76,7 +76,7 @@ void CalibreSettingsActivity::handleSelection() {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 10,
renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername,
63, // maxLength
false, // not password
[this](const std::string& username) {
@@ -94,7 +94,7 @@ void CalibreSettingsActivity::handleSelection() {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 10,
renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword,
63, // maxLength
false, // not password mode
[this](const std::string& password) {
@@ -114,41 +114,35 @@ void CalibreSettingsActivity::handleSelection() {
void CalibreSettingsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_OPDS_BROWSER));
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
tr(STR_CALIBRE_URL_HINT));
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.tabBarHeight;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS),
static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
nullptr,
[this](int index) {
// Draw status for each setting
if (index == 0) {
return (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string(SETTINGS.opdsServerUrl)
: std::string(tr(STR_NOT_SET));
} else if (index == 1) {
return (strlen(SETTINGS.opdsUsername) > 0) ? std::string(SETTINGS.opdsUsername)
: std::string(tr(STR_NOT_SET));
} else if (index == 2) {
return (strlen(SETTINGS.opdsPassword) > 0) ? std::string("******") : std::string(tr(STR_NOT_SET));
}
return std::string(tr(STR_NOT_SET));
},
true);
// Draw info text about Calibre
renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_CALIBRE_URL_HINT));
// Draw selection highlight
renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30);
// Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) {
const int settingY = 70 + i * 30;
const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected);
// Draw status for each setting
std::string status = std::string("[") + tr(STR_NOT_SET) + "]";
if (i == 0) {
status = (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
} else if (i == 1) {
status = (strlen(SETTINGS.opdsUsername) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
} else if (i == 2) {
status = (strlen(SETTINGS.opdsPassword) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected);
}
// Draw button hints
// Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@@ -23,7 +23,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity {
private:
ButtonNavigator buttonNavigator;
int selectedIndex = 0;
size_t selectedIndex = 0;
const std::function<void()> onBack;
void handleSelection();
};

View File

@@ -19,10 +19,13 @@ void ClearCacheActivity::onEnter() {
void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); }
void ClearCacheActivity::render(Activity::RenderLock&&) {
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CLEAR_READING_CACHE), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLEAR_READING_CACHE));
if (state == WARNING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true);
@@ -38,7 +41,7 @@ void ClearCacheActivity::render(Activity::RenderLock&&) {
}
if (state == CLEARING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE));
renderer.displayBuffer();
return;
}

View File

@@ -90,33 +90,28 @@ void KOReaderAuthActivity::onExit() {
void KOReaderAuthActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_AUTH), true, EpdFontFamily::BOLD);
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str());
} else if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY));
} else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
}
if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_SYNC_READY));
const auto labels = mappedInput.mapLabels(tr(STR_DONE), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
void KOReaderAuthActivity::loop() {

View File

@@ -60,7 +60,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 10,
renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(),
64, // maxLength
false, // not password
[this](const std::string& username) {
@@ -77,7 +77,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 10,
renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(),
64, // maxLength
false, // show characters
[this](const std::string& password) {
@@ -96,7 +96,7 @@ void KOReaderSettingsActivity::handleSelection() {
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 10,
renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl,
128, // maxLength - URLs can be long
false, // not password
[this](const std::string& url) {
@@ -136,43 +136,39 @@ void KOReaderSettingsActivity::handleSelection() {
void KOReaderSettingsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_SYNC));
// Draw selection highlight
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30);
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS),
static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
nullptr,
[this](int index) {
// Draw status for each setting
if (index == 0) {
auto username = KOREADER_STORE.getUsername();
return username.empty() ? std::string(tr(STR_NOT_SET)) : username;
} else if (index == 1) {
return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
} else if (index == 2) {
auto serverUrl = KOREADER_STORE.getServerUrl();
return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl;
} else if (index == 3) {
return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME))
: std::string(tr(STR_BINARY));
} else if (index == 4) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
}
return std::string(tr(STR_NOT_SET));
},
true);
// Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) {
const int settingY = 60 + i * 30;
const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected);
// Draw status for each item
std::string status = "";
if (i == 0) {
status = std::string("[") + (KOREADER_STORE.getUsername().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]";
} else if (i == 1) {
status = std::string("[") + (KOREADER_STORE.getPassword().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]";
} else if (i == 2) {
status =
std::string("[") + (KOREADER_STORE.getServerUrl().empty() ? tr(STR_DEFAULT_VALUE) : tr(STR_CUSTOM)) + "]";
} else if (i == 3) {
status = std::string("[") +
(KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? tr(STR_FILENAME) : tr(STR_BINARY)) +
"]";
} else if (i == 4) {
status = KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected);
}
// Draw button hints
// Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@@ -23,7 +23,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity {
private:
ButtonNavigator buttonNavigator;
int selectedIndex = 0;
size_t selectedIndex = 0;
const std::function<void()> onBack;
void handleSelection();

View File

@@ -84,6 +84,16 @@ void OtaUpdateActivity::render(Activity::RenderLock&&) {
return;
}
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
float updaterProgress = 0;
if (state == UPDATE_IN_PROGRESS) {
LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize());
@@ -95,60 +105,43 @@ void OtaUpdateActivity::render(Activity::RenderLock&&) {
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
}
const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_UPDATE), true, EpdFontFamily::BOLD);
if (state == CHECKING_FOR_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_CHECKING_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, 200, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20, 250, (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
renderer.drawText(UI_10_FONT_ID, 20, 270, (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_CHECKING_UPDATE));
} else if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + metrics.verticalSpacing,
(std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2,
(std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
} else if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATING));
if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 310, tr(STR_UPDATING), true, EpdFontFamily::BOLD);
renderer.drawRect(20, 350, pageWidth - 40, 50);
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
renderer.drawCenteredText(UI_10_FONT_ID, 420,
int y = top + height + metrics.verticalSpacing;
GUI.drawProgressBar(
renderer,
Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
static_cast<int>(updaterProgress * 100), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y,
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
y += height + metrics.verticalSpacing;
renderer.drawCenteredText(
UI_10_FONT_ID, 440,
UI_10_FONT_ID, y,
(std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str());
renderer.displayBuffer();
return;
} else if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD);
} else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
} else if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_POWER_ON_HINT));
}
if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, tr(STR_POWER_ON_HINT));
renderer.displayBuffer();
state = SHUTTING_DOWN;
return;
}
renderer.displayBuffer();
}
void OtaUpdateActivity::loop() {

View File

@@ -218,7 +218,8 @@ void SettingsActivity::render(Activity::RenderLock&&) {
auto metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE));
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE),
CROSSPOINT_VERSION);
std::vector<TabInfo> tabs;
tabs.reserve(categoryCount);
@@ -249,12 +250,8 @@ void SettingsActivity::render(Activity::RenderLock&&) {
valueText = std::to_string(SETTINGS.*(setting.valuePtr));
}
return valueText;
});
// Draw version text
renderer.drawText(SMALL_FONT_ID,
pageWidth - metrics.versionTextRightX - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
metrics.versionTextY, CROSSPOINT_VERSION);
},
true);
// Draw help text
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_TOGGLE), tr(STR_DIR_UP), tr(STR_DIR_DOWN));

View File

@@ -42,7 +42,7 @@ int KeyboardEntryActivity::getRowLength(const int row) const {
case 3:
return 10; // zxcvbnm,./
case 4:
return 10; // shift (2 wide), space (5 wide), backspace (2 wide), OK
return 11; // shift (2 wide), space (5 wide), backspace (2 wide), OK (2 wide)
default:
return 0;
}
@@ -191,17 +191,19 @@ void KeyboardEntryActivity::loop() {
}
void KeyboardEntryActivity::render(Activity::RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen();
// Draw title
renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str());
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
auto metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str());
// Draw input field
const int inputStartY = startY + 22;
int inputEndY = startY + 22;
renderer.drawText(UI_10_FONT_ID, 10, inputStartY, "[");
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int inputStartY =
metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4;
int inputHeight = 0;
std::string displayText;
if (isPassword) {
@@ -216,34 +218,43 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) {
// Render input text across multiple lines
int lineStartIdx = 0;
int lineEndIdx = displayText.length();
int textWidth = 0;
while (true) {
std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx);
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str());
if (textWidth <= pageWidth - 40) {
renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str());
textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str());
if (textWidth <= pageWidth - 2 * metrics.contentSidePadding) {
if (metrics.keyboardCenteredText) {
renderer.drawCenteredText(UI_12_FONT_ID, inputStartY + inputHeight, lineText.c_str());
} else {
renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, inputStartY + inputHeight, lineText.c_str());
}
if (lineEndIdx == displayText.length()) {
break;
}
inputEndY += renderer.getLineHeight(UI_10_FONT_ID);
inputHeight += lineHeight;
lineStartIdx = lineEndIdx;
lineEndIdx = displayText.length();
} else {
lineEndIdx -= 1;
}
}
renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputEndY, "]");
GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, textWidth);
// Draw keyboard - use compact spacing to fit 5 rows on screen
const int keyboardStartY = inputEndY + 25;
constexpr int keyWidth = 18;
constexpr int keyHeight = 18;
constexpr int keySpacing = 3;
const int keyboardStartY = metrics.keyboardBottomAligned
? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing -
(metrics.keyboardKeyHeight + metrics.keyboardKeySpacing) * NUM_ROWS
: inputStartY + inputHeight + metrics.verticalSpacing * 4;
const int keyWidth = metrics.keyboardKeyWidth;
const int keyHeight = metrics.keyboardKeyHeight;
const int keySpacing = metrics.keyboardKeySpacing;
const char* const* layout = shiftState ? keyboardShift : keyboard;
// Calculate left margin to center the longest row (13 keys)
constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
const int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
const int leftMargin = (pageWidth - maxRowWidth) / 2;
for (int row = 0; row < NUM_ROWS; row++) {
@@ -253,45 +264,50 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) {
const int startX = leftMargin;
// Handle bottom row (row 4) specially with proper multi-column keys
if (row == 4) {
if (row == SPECIAL_ROW) {
// Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
// Total: 11 visual columns, but we use logical positions for selection
int currentX = startX;
// SHIFT key (logical col 0, spans 2 key widths)
const bool shiftSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
static constexpr StrId shiftIds[3] = {StrId::STR_KBD_SHIFT, StrId::STR_KBD_SHIFT_CAPS, StrId::STR_KBD_LOCK};
renderItemWithSelector(currentX + 2, rowY, I18N.get(shiftIds[shiftState]), shiftSelected);
currentX += 2 * (keyWidth + keySpacing);
const bool shiftSelected = (selectedRow == SPECIAL_ROW && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
const int shiftWidth = SPACE_COL - SHIFT_COL;
const int shiftXWidth = shiftWidth * (keyWidth + keySpacing);
GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, shiftXWidth, keyHeight}, shiftString[shiftState],
shiftSelected);
currentX += shiftXWidth;
// Space bar (logical cols 2-6, spans 5 key widths)
const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
const int spaceTextWidth = renderer.getTextWidth(UI_10_FONT_ID, "_____");
const int spaceXWidth = 5 * (keyWidth + keySpacing);
const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2;
renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected);
const bool spaceSelected =
(selectedRow == SPECIAL_ROW && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
const int spaceWidth = BACKSPACE_COL - SPACE_COL;
const int spaceXWidth = spaceWidth * (keyWidth + keySpacing);
GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, spaceXWidth, keyHeight}, "_____", spaceSelected);
currentX += spaceXWidth;
// Backspace key (logical col 7, spans 2 key widths)
const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL);
renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected);
currentX += 2 * (keyWidth + keySpacing);
const bool bsSelected = (selectedRow == SPECIAL_ROW && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL);
const int backspaceWidth = DONE_COL - BACKSPACE_COL;
const int backspaceXWidth = backspaceWidth * (keyWidth + keySpacing);
GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, backspaceXWidth, keyHeight}, "<-", bsSelected);
currentX += backspaceXWidth;
// OK button (logical col 9, spans 2 key widths)
const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL);
renderItemWithSelector(currentX + 2, rowY, tr(STR_OK_BUTTON), okSelected);
const bool okSelected = (selectedRow == SPECIAL_ROW && selectedCol >= DONE_COL);
const int okWidth = getRowLength(row) - DONE_COL;
const int okXWidth = okWidth * (keyWidth + keySpacing);
GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, okXWidth, keyHeight}, tr(STR_OK_BUTTON), okSelected);
} else {
// Regular rows: render each key individually
for (int col = 0; col < getRowLength(row); col++) {
// Get the character to display
const char c = layout[row][col];
std::string keyLabel(1, c);
const int charWidth = renderer.getTextWidth(UI_10_FONT_ID, keyLabel.c_str());
const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2;
const int keyX = startX + col * (keyWidth + keySpacing);
const bool isSelected = row == selectedRow && col == selectedCol;
renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected);
GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, keyLabel.c_str(), isSelected);
}
}
}
@@ -301,17 +317,7 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) {
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Draw side button hints for Up/Down navigation
GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawSideButtonHints(renderer, ">", "<");
renderer.displayBuffer();
}
void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item,
const bool isSelected) const {
if (isSelected) {
const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, item);
renderer.drawText(UI_10_FONT_ID, x - 6, y, "[");
renderer.drawText(UI_10_FONT_ID, x + itemWidth, y, "]");
}
renderer.drawText(UI_10_FONT_ID, x, y, item);
}

View File

@@ -31,20 +31,18 @@ class KeyboardEntryActivity : public Activity {
* @param mappedInput Reference to MappedInputManager for handling input
* @param title Title to display above the keyboard
* @param initialText Initial text to show in the input field
* @param startY Y position to start rendering the keyboard
* @param maxLength Maximum length of input text (0 for unlimited)
* @param isPassword If true, display asterisks instead of actual characters
* @param onComplete Callback invoked when input is complete
* @param onCancel Callback invoked when input is cancelled
*/
explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
std::string title = "Enter Text", std::string initialText = "", const int startY = 10,
std::string title = "Enter Text", std::string initialText = "",
const size_t maxLength = 0, const bool isPassword = false,
OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr)
: Activity("KeyboardEntry", renderer, mappedInput),
title(std::move(title)),
text(std::move(initialText)),
startY(startY),
maxLength(maxLength),
isPassword(isPassword),
onComplete(std::move(onComplete)),
@@ -58,7 +56,6 @@ class KeyboardEntryActivity : public Activity {
private:
std::string title;
int startY;
std::string text;
size_t maxLength;
bool isPassword;
@@ -91,5 +88,4 @@ class KeyboardEntryActivity : public Activity {
char getSelectedChar() const;
void handleKeyPress();
int getRowLength(int row) const;
void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const;
};