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 21:16:55 +11:00
committed by GitHub
co-authored by Dave Allie
parent c6ddc5d6a0
commit e7ee6ff05e
38 changed files with 787 additions and 551 deletions
+13 -13
View File
@@ -334,7 +334,9 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
return;
}
const int maxRadius = std::min({cornerRadius, width / 2, height / 2});
// Assume if we're not rounding all corners then we are only rounding one side
const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2;
const int maxRadius = std::min({cornerRadius, width / roundedSides, height / roundedSides});
if (maxRadius <= 0) {
fillRectDither(x, y, width, height, color);
return;
@@ -345,10 +347,16 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color);
}
const int verticalHeight = height - 2 * maxRadius - 2;
if (verticalHeight > 0) {
fillRectDither(x, y + maxRadius + 1, maxRadius + 1, verticalHeight, color);
fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color);
const int leftFillTop = y + (roundTopLeft ? (maxRadius + 1) : 0);
const int leftFillBottom = y + height - 1 - (roundBottomLeft ? (maxRadius + 1) : 0);
if (leftFillBottom >= leftFillTop) {
fillRectDither(x, leftFillTop, maxRadius + 1, leftFillBottom - leftFillTop + 1, color);
}
const int rightFillTop = y + (roundTopRight ? (maxRadius + 1) : 0);
const int rightFillBottom = y + height - 1 - (roundBottomRight ? (maxRadius + 1) : 0);
if (rightFillBottom >= rightFillTop) {
fillRectDither(x + width - maxRadius - 1, rightFillTop, maxRadius + 1, rightFillBottom - rightFillTop + 1, color);
}
auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) {
@@ -372,26 +380,18 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
if (roundTopLeft) {
fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color);
} else {
fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color);
}
if (roundTopRight) {
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color);
} else {
fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color);
}
if (roundBottomRight) {
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color);
} else {
fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color);
}
if (roundBottomLeft) {
fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color);
} else {
fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color);
}
}
+1
View File
@@ -290,6 +290,7 @@ enum class StrId : uint16_t {
STR_UI_THEME,
STR_THEME_CLASSIC,
STR_THEME_LYRA,
STR_THEME_LYRA_EXTENDED,
STR_SUNLIGHT_FADING_FIX,
STR_REMAP_FRONT_BUTTONS,
STR_OPDS_BROWSER,
+1
View File
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol"
STR_UI_THEME: "Šablona rozhraní"
STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
STR_OPDS_BROWSER: "Prohlížeč OPDS"
+1
View File
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar"
STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser"
+4 -3
View File
@@ -48,7 +48,7 @@ STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?"
STR_JOIN_NETWORK: "Connexion à un réseau"
STR_CREATE_HOTSPOT: "Créer un point daccès"
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis dautres appareils"
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible sur dautres appareils"
STR_STARTING_HOTSPOT: "Création du point daccès en cours…"
STR_HOTSPOT_MODE: "Mode point daccès"
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi"
@@ -81,7 +81,7 @@ STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu"
STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : Envoyer vers lappareil"
STR_CALIBRE_INSTRUCTION_4: "Gardez cet écran ouvert pendant le transfert"
STR_CALIBRE_INSTRUCTION_4: "4) Gardez cet écran ouvert pendant le transfert"
STR_CAT_DISPLAY: "Affichage"
STR_CAT_READER: "Lecteur"
STR_CAT_CONTROLS: "Commandes"
@@ -215,7 +215,7 @@ STR_NO_SERVER_URL: "Aucune URL serveur configurée"
STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux"
STR_PARSE_FEED_FAILED: "Échec de lanalyse du flux"
STR_NETWORK_PREFIX: "Réseau : "
STR_IP_ADDRESS_PREFIX: "Adresse IP : "
STR_IP_ADDRESS_PREFIX: "IP : "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général"
STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable"
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre"
STR_UI_THEME: "Thème de linterface"
STR_THEME_CLASSIC: "Classique"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil"
STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant"
STR_OPDS_BROWSER: "Navigateur OPDS"
+1
View File
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel"
STR_UI_THEME: "System-Design"
STR_THEME_CLASSIC: "Klassisch"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_OPDS_BROWSER: "OPDS-Browser"
+1
View File
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo"
STR_UI_THEME: "Tema da interface"
STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_OPDS_BROWSER: "Navegador OPDS"
+1
View File
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы"
STR_UI_THEME: "Тема интерфейса"
STR_THEME_CLASSIC: "Классическая"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_OPDS_BROWSER: "OPDS браузер"
+2 -1
View File
@@ -255,7 +255,8 @@ STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro"
STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos"
STR_UI_THEME: "Estilo de pantalla"
STR_THEME_CLASSIC: "Clásico"
STR_THEME_LYRA: "LYRA"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_OPDS_BROWSER: "Navegador opds"
+1
View File
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist"
STR_UI_THEME: "Användargränssnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_OPDS_BROWSER: "OPDS-webbläsare"
+1 -1
View File
@@ -120,7 +120,7 @@ class CrossPointSettings {
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
// UI Theme
enum UI_THEME { CLASSIC = 0, LYRA = 1 };
enum UI_THEME { CLASSIC = 0, LYRA = 1, LYRA_3_COVERS = 2 };
// Sleep screen settings
uint8_t sleepScreen = DARK;
+2 -1
View File
@@ -36,7 +36,8 @@ inline std::vector<SettingInfo> getSettingsList() {
{StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30},
"refreshFrequency", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme,
{StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY),
{StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme",
StrId::STR_CAT_DISPLAY),
SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix",
StrId::STR_CAT_DISPLAY),
@@ -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();
@@ -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);
}
@@ -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), "", "", "");
@@ -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));
@@ -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) : "";
@@ -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;
+26 -23
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.

Some files were not shown because too many files have changed in this diff Show More