Selectable UI themes and a Popular source
Themes (Phosphor, Pink, Amber, Ice, Terminal) restyle the menu, boot page and toasts live from the menu; all but Terminal use readable Montserrat text. Source gains Popular: the top-rated listings walked in rank order from page 1, alternating providers when mixed.
This commit is contained in:
1 parent
1ea7b13bc7
commit
3bdfcba6fb
9 files changed
+171
-47
No files matched your search
@@ -63,10 +63,15 @@ Entries:
|
||||
saving asks for one of the 50 slots (it jumps to the first empty one; picking a used slot overwrites it).
|
||||
* **Favourites** — the saved cameras; click one to play it. A saved camera shows a
|
||||
yellow *FAV n* marker while it plays.
|
||||
* **Source** — *Random* draws from the sites, *Favourites* cycles the saved slots.
|
||||
* **Source** — *Random* draws random listing pages, *Popular* walks the sites'
|
||||
top-rated listings in rank order from the top, *Favourites* cycles the saved
|
||||
slots.
|
||||
* **Provider** — *insecam*, *opentopia* or *Mixed* (a random site per batch).
|
||||
* **Time per camera**, **Listing** (mixed / top rated / newest), **Screen
|
||||
brightness**, **Ring brightness**, **Caption**.
|
||||
* **Theme** — the look of the menu, boot page and pop-ups, applied as you
|
||||
cycle: *Phosphor* (green), *Pink*, *Amber*, *Ice*, and *Terminal* (green with
|
||||
an 8×16 pixel font). The camera caption keeps its own neutral style.
|
||||
* **Forget a favourite**, **Wi-Fi setup** (reboots into the hotspot), **Power
|
||||
off**, the board name with its IP, and the power source.
|
||||
|
||||
|
||||
+2
-1
@@ -38,6 +38,7 @@ void app_main(void)
|
||||
|
||||
display_init();
|
||||
ui_init();
|
||||
ui_set_theme(settings.theme);
|
||||
ui_status("insecam widget", "starting");
|
||||
display_set_backlight(settings.lcd_brightness);
|
||||
|
||||
@@ -58,7 +59,7 @@ void app_main(void)
|
||||
if (!net_wait_connected(20000)) run_portal();
|
||||
}
|
||||
ESP_LOGI(TAG, "online at %s", net_ip_str());
|
||||
ui_status("insecam widget", "online " "\xE2\x80\xA2" " finding cameras\n\nknob: next / previous\nclick: pause hold: menu");
|
||||
ui_status("insecam widget", "online - finding cameras\n\nturn: next / previous\nclick: pause\nhold: menu");
|
||||
leds_set_mode(LED_SPIN, 0x2060ff);
|
||||
slideshow_start();
|
||||
}
|
||||
+12
-4
@@ -20,7 +20,7 @@ static const char *TAG = "menu";
|
||||
|
||||
typedef enum { VIEW_TOP, VIEW_FAV_PLAY, VIEW_FAV_SAVE, VIEW_FAV_FORGET } view_t;
|
||||
enum { ROW_FAV_TOGGLE, ROW_FAVS, ROW_SOURCE, ROW_PROVIDER, ROW_INTERVAL, ROW_LISTING, ROW_LCD, ROW_RING,
|
||||
ROW_CAPTION, ROW_FORGET, ROW_WIFI, ROW_POWER, ROW_IP, ROW_BATTERY, ROW_CLOSE, TOP_ROWS };
|
||||
ROW_CAPTION, ROW_THEME, ROW_FORGET, ROW_WIFI, ROW_POWER, ROW_IP, ROW_BATTERY, ROW_CLOSE, TOP_ROWS };
|
||||
|
||||
static ui_menu_row_t rows[TOP_ROWS > FAV_SLOTS + 1 ? TOP_ROWS : FAV_SLOTS + 1];
|
||||
static char values[TOP_ROWS][20];
|
||||
@@ -43,15 +43,17 @@ static int build_top(const cam_info_t *current, int fav_slot)
|
||||
rows[ROW_FAV_TOGGLE] = (ui_menu_row_t){fav_slot >= 0 ? "Remove from favourites" : "Save as favourite",
|
||||
values[ROW_FAV_TOGGLE], current == NULL};
|
||||
rows[ROW_FAVS] = (ui_menu_row_t){"Favourites", values[ROW_FAVS], false};
|
||||
rows[ROW_SOURCE] = (ui_menu_row_t){"Source", settings.source == SOURCE_FAVORITES ? "Favourites" : "Random", false};
|
||||
rows[ROW_SOURCE] = (ui_menu_row_t){"Source", settings.source == SOURCE_FAVORITES ? "Favourites" :
|
||||
settings.source == SOURCE_POPULAR ? "Popular" : "Random", false};
|
||||
rows[ROW_PROVIDER] = (ui_menu_row_t){"Provider",
|
||||
settings.provider == PROVIDER_PREF_INSECAM ? "insecam" : settings.provider == PROVIDER_PREF_OPENTOPIA ? "opentopia" : "Mixed",
|
||||
settings.source == SOURCE_FAVORITES};
|
||||
rows[ROW_INTERVAL] = (ui_menu_row_t){"Time per camera", values[ROW_INTERVAL], false};
|
||||
rows[ROW_LISTING] = (ui_menu_row_t){"Listing", listing_name(settings.listing), false};
|
||||
rows[ROW_LISTING] = (ui_menu_row_t){"Listing", listing_name(settings.listing), settings.source == SOURCE_POPULAR};
|
||||
rows[ROW_LCD] = (ui_menu_row_t){"Screen brightness", values[ROW_LCD], false};
|
||||
rows[ROW_RING] = (ui_menu_row_t){"Ring brightness", values[ROW_RING], false};
|
||||
rows[ROW_CAPTION] = (ui_menu_row_t){"Caption", settings.caption ? "Shown" : "Hidden", false};
|
||||
rows[ROW_THEME] = (ui_menu_row_t){"Theme", ui_theme_name(settings.theme), false};
|
||||
rows[ROW_FORGET] = (ui_menu_row_t){"Forget a favourite", NULL, fav_count() == 0};
|
||||
rows[ROW_WIFI] = (ui_menu_row_t){"Wi-Fi setup", NULL, false};
|
||||
rows[ROW_POWER] = (ui_menu_row_t){"Power off", "knob wakes", false};
|
||||
@@ -179,10 +181,16 @@ menu_result_t menu_run(const cam_info_t *current, int *fav_slot_out)
|
||||
case ROW_FAVS: view = VIEW_FAV_PLAY; sel = top = 0; break;
|
||||
case ROW_FORGET: view = VIEW_FAV_FORGET; sel = top = 0; break;
|
||||
case ROW_SOURCE:
|
||||
settings.source = settings.source == SOURCE_RANDOM ? SOURCE_FAVORITES : SOURCE_RANDOM;
|
||||
settings.source = settings.source == SOURCE_RANDOM ? SOURCE_POPULAR :
|
||||
settings.source == SOURCE_POPULAR ? SOURCE_FAVORITES : SOURCE_RANDOM;
|
||||
if (settings.source == SOURCE_FAVORITES && fav_count() == 0) ui_toast("No favourites saved yet", 1500);
|
||||
settings_save();
|
||||
break;
|
||||
case ROW_THEME:
|
||||
settings.theme = (settings.theme + 1) % ui_theme_count();
|
||||
ui_set_theme(settings.theme);
|
||||
settings_save();
|
||||
break;
|
||||
case ROW_LISTING:
|
||||
settings.listing = (settings.listing + 1) % 3;
|
||||
settings_save();
|
||||
|
||||
+5
-2
@@ -1,10 +1,12 @@
|
||||
#include "settings.h"
|
||||
#include "ui.h"
|
||||
#include "sdkconfig.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -43,12 +45,13 @@ void settings_init(void)
|
||||
size_t len = sizeof stored;
|
||||
if (nvs_get_blob(nvs, "settings", &stored, &len) == ESP_OK && len >= 7 && len <= sizeof stored) {
|
||||
memcpy(&settings, &stored, len);
|
||||
if (len < sizeof settings) settings.provider = PROVIDER_PREF_MIXED; // pre-provider blob: padding, not a choice
|
||||
if (len <= offsetof(settings_t, provider)) settings.provider = PROVIDER_PREF_MIXED; // pre-provider blob
|
||||
settings.version = SETTINGS_VERSION;
|
||||
if (settings.interval_s < 5 || settings.interval_s > 300) settings.interval_s = CONFIG_WIDGET_INTERVAL_S;
|
||||
if (settings.lcd_brightness > 100) settings.lcd_brightness = 100;
|
||||
if (settings.listing > LISTING_NEW) settings.listing = LISTING_MIXED;
|
||||
if (settings.source > SOURCE_FAVORITES) settings.source = SOURCE_RANDOM;
|
||||
if (settings.source > SOURCE_POPULAR) settings.source = SOURCE_RANDOM;
|
||||
if (settings.theme >= ui_theme_count()) settings.theme = 0;
|
||||
if (settings.provider > PROVIDER_PREF_MIXED) settings.provider = PROVIDER_PREF_MIXED;
|
||||
}
|
||||
favs = heap_caps_calloc(FAV_SLOTS, sizeof *favs, MALLOC_CAP_SPIRAM);
|
||||
|
||||
+2
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
// Persistent settings (NVS namespace "widget"); Kconfig supplies first-boot defaults.
|
||||
typedef enum { LISTING_MIXED = 0, LISTING_RATING, LISTING_NEW } listing_pref_t;
|
||||
typedef enum { SOURCE_RANDOM = 0, SOURCE_FAVORITES } source_pref_t;
|
||||
typedef enum { SOURCE_RANDOM = 0, SOURCE_FAVORITES, SOURCE_POPULAR } source_pref_t;
|
||||
typedef enum { PROVIDER_PREF_INSECAM = 0, PROVIDER_PREF_OPENTOPIA, PROVIDER_PREF_MIXED } provider_pref_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -17,6 +17,7 @@ typedef struct {
|
||||
uint8_t caption; // 1 = caption shown
|
||||
uint8_t provider; // provider_pref_t (appended; older blobs load with the default)
|
||||
uint8_t version; // SETTINGS_VERSION; lets us tell a pre-provider blob apart
|
||||
uint8_t theme; // ui theme index
|
||||
} settings_t;
|
||||
#define SETTINGS_VERSION 2
|
||||
|
||||
|
||||
+14
-4
@@ -46,6 +46,8 @@ typedef enum { LEAVE_NEXT, LEAVE_PREV, LEAVE_ERROR, LEAVE_MENU } leave_t;
|
||||
static cam_info_t page_cams[PAGE_ITEMS];
|
||||
static int page_count, page_next;
|
||||
static int total_pages[2][2] = {{287, 287}, {162, 162}}; // [provider][listing]
|
||||
static int popular_page[2] = {1, 1}; // next rated page per provider in Popular mode
|
||||
static int popular_turn;
|
||||
|
||||
static cam_info_t history[HISTORY_LEN];
|
||||
static int hist_len, hist_pos = -1;
|
||||
@@ -88,18 +90,26 @@ static void refill_queue(void)
|
||||
{
|
||||
int failures = 0;
|
||||
while (1) {
|
||||
insecam_listing_t listing = pick_listing();
|
||||
bool popular = settings.source == SOURCE_POPULAR;
|
||||
insecam_listing_t listing = popular ? INSECAM_BY_RATING : pick_listing();
|
||||
provider_t provider = settings.provider == PROVIDER_PREF_INSECAM ? PROVIDER_INSECAM :
|
||||
settings.provider == PROVIDER_PREF_OPENTOPIA ? PROVIDER_OPENTOPIA :
|
||||
popular ? ((popular_turn++ & 1) ? PROVIDER_OPENTOPIA : PROVIDER_INSECAM) :
|
||||
(esp_random() & 1) ? PROVIDER_OPENTOPIA : PROVIDER_INSECAM;
|
||||
int page = 1 + esp_random() % total_pages[provider][listing];
|
||||
int page;
|
||||
if (popular) { // walk the top-rated listing from the top, in rank order
|
||||
if (popular_page[provider] > total_pages[provider][listing]) popular_page[provider] = 1;
|
||||
page = popular_page[provider]++;
|
||||
} else {
|
||||
page = 1 + esp_random() % total_pages[provider][listing];
|
||||
}
|
||||
int n = 0;
|
||||
esp_err_t err = provider == PROVIDER_OPENTOPIA
|
||||
? opentopia_fetch_page(listing, page, page_cams, PAGE_ITEMS, &n, &total_pages[provider][listing])
|
||||
: insecam_fetch_page(listing, page, page_cams, PAGE_ITEMS, &n, &total_pages[provider][listing]);
|
||||
if (err == ESP_OK && n > 0) {
|
||||
// shuffle so the six don't always play in site order
|
||||
for (int i = n - 1; i > 0; i--) {
|
||||
// shuffle so the six don't always play in site order (Popular keeps rank order)
|
||||
for (int i = popular ? 0 : n - 1; i > 0; i--) {
|
||||
int j = esp_random() % (i + 1);
|
||||
cam_info_t t = page_cams[i];
|
||||
page_cams[i] = page_cams[j];
|
||||
|
||||
@@ -4,10 +4,41 @@
|
||||
#include "lvgl.h"
|
||||
#include <string.h>
|
||||
|
||||
static lv_obj_t *scr_status, *lbl_title, *lbl_msg;
|
||||
// Themes for the "system" pages (menu, status, toast). The camera page keeps its own look.
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint32_t bg, text, dim, sel_bg, sel_text, edit;
|
||||
const lv_font_t *title_font, *row_font, *hint_font;
|
||||
} theme_t;
|
||||
|
||||
static const theme_t themes[] = {
|
||||
{"Phosphor", 0x000000, 0x3ddc84, 0x1f7a4a, 0x3ddc84, 0x04140b, 0xffb000,
|
||||
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
|
||||
{"Pink", 0x140812, 0xff6ab8, 0x8c3a6c, 0xff6ab8, 0x1e0716, 0xfff06a,
|
||||
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
|
||||
{"Amber", 0x000000, 0xffb000, 0x7a5200, 0xffb000, 0x1a1000, 0x3ddc84,
|
||||
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
|
||||
{"Ice", 0x03070d, 0xa6dcff, 0x3a6e91, 0x2fb8ff, 0x03070d, 0xffb000,
|
||||
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
|
||||
{"Terminal", 0x000000, 0x3ddc84, 0x1f7a4a, 0x3ddc84, 0x04140b, 0xffb000,
|
||||
&lv_font_unscii_16, &lv_font_unscii_16, &lv_font_unscii_8},
|
||||
};
|
||||
static const theme_t *th = &themes[0];
|
||||
|
||||
static lv_obj_t *scr_status, *status_hdr, *lbl_title, *lbl_msg, *status_cursor;
|
||||
static lv_obj_t *scr_cam, *canvas, *overlay, *lbl_place, *lbl_meta, *lbl_batt, *badge, *marker, *bar, *toast;
|
||||
static lv_obj_t *scr_menu, *menu_title, *menu_row[UI_MENU_ROWS], *menu_label[UI_MENU_ROWS], *menu_value[UI_MENU_ROWS];
|
||||
static lv_obj_t *scr_menu, *menu_title, *menu_cursor, *menu_rule, *menu_hint, *menu_row[UI_MENU_ROWS], *menu_label[UI_MENU_ROWS], *menu_value[UI_MENU_ROWS];
|
||||
static lv_obj_t *prev_screen;
|
||||
static lv_timer_t *cursor_timer;
|
||||
static char menu_line[UI_MENU_ROWS][64];
|
||||
|
||||
static void cursor_blink_cb(lv_timer_t *t)
|
||||
{
|
||||
static bool on;
|
||||
on = !on;
|
||||
lv_obj_set_hidden(menu_cursor, !on);
|
||||
lv_obj_set_hidden(status_cursor, !on);
|
||||
}
|
||||
static lv_timer_t *toast_timer;
|
||||
static bool overlay_on = true;
|
||||
|
||||
@@ -41,15 +72,19 @@ void ui_init(void)
|
||||
|
||||
// --- status page ---
|
||||
scr_status = plain_screen();
|
||||
lbl_title = label(scr_status, &lv_font_montserrat_24, 0xffffff);
|
||||
status_hdr = label(scr_status, th->hint_font, th->dim);
|
||||
lv_label_set_text(status_hdr, "T-EMBED // ESP32-S3 // insecam widget");
|
||||
lv_obj_set_pos(status_hdr, 12, 10);
|
||||
lbl_title = label(scr_status, th->title_font, th->text);
|
||||
lv_obj_set_width(lbl_title, LCD_H_RES - 24);
|
||||
lv_obj_set_style_text_align(lbl_title, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 34);
|
||||
lbl_msg = label(scr_status, &lv_font_montserrat_14, 0xb8c0cc);
|
||||
lv_obj_set_pos(lbl_title, 12, 30);
|
||||
lbl_msg = label(scr_status, th->row_font, th->text);
|
||||
lv_obj_set_width(lbl_msg, LCD_H_RES - 24);
|
||||
lv_obj_set_style_text_align(lbl_msg, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_label_set_long_mode(lbl_msg, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_align(lbl_msg, LV_ALIGN_TOP_MID, 0, 80);
|
||||
lv_obj_set_pos(lbl_msg, 12, 62);
|
||||
status_cursor = label(scr_status, th->row_font, th->text);
|
||||
lv_label_set_text(status_cursor, "_");
|
||||
lv_obj_align(status_cursor, LV_ALIGN_BOTTOM_LEFT, 12, -10);
|
||||
|
||||
// --- camera page ---
|
||||
scr_cam = plain_screen();
|
||||
@@ -102,36 +137,44 @@ void ui_init(void)
|
||||
lv_obj_set_hidden(marker, true);
|
||||
|
||||
// toast lives on the top layer so it shows over every page
|
||||
toast = label(lv_layer_top(), &lv_font_montserrat_14, 0xffffff);
|
||||
lv_obj_set_style_bg_color(toast, lv_color_hex(0x202830), 0);
|
||||
toast = label(lv_layer_top(), th->row_font, th->text);
|
||||
lv_obj_set_style_bg_color(toast, lv_color_hex(th->bg), 0);
|
||||
lv_obj_set_style_bg_opa(toast, LV_OPA_90, 0);
|
||||
lv_obj_set_style_radius(toast, 6, 0);
|
||||
lv_obj_set_style_border_color(toast, lv_color_hex(th->text), 0);
|
||||
lv_obj_set_style_border_width(toast, 1, 0);
|
||||
lv_obj_set_style_radius(toast, 0, 0);
|
||||
lv_obj_set_style_pad_all(toast, 8, 0);
|
||||
lv_obj_align(toast, LV_ALIGN_CENTER, 0, -10);
|
||||
lv_obj_set_hidden(toast, true);
|
||||
|
||||
// --- menu page ---
|
||||
// --- menu page: green phosphor terminal ---
|
||||
scr_menu = plain_screen();
|
||||
lv_obj_t *titlebar = lv_obj_create(scr_menu);
|
||||
lv_obj_remove_style_all(titlebar);
|
||||
lv_obj_set_size(titlebar, LCD_H_RES, 22);
|
||||
lv_obj_set_style_bg_color(titlebar, lv_color_hex(0x1c2430), 0);
|
||||
lv_obj_set_style_bg_opa(titlebar, LV_OPA_COVER, 0);
|
||||
menu_title = label(titlebar, &lv_font_montserrat_14, 0x9fb3c8);
|
||||
lv_obj_align(menu_title, LV_ALIGN_LEFT_MID, 8, 0);
|
||||
menu_title = label(scr_menu, th->title_font, th->text);
|
||||
lv_obj_set_pos(menu_title, 6, 2);
|
||||
menu_cursor = label(scr_menu, th->title_font, th->text);
|
||||
lv_label_set_text(menu_cursor, "_");
|
||||
menu_rule = lv_obj_create(scr_menu);
|
||||
lv_obj_remove_style_all(menu_rule);
|
||||
lv_obj_set_size(menu_rule, LCD_H_RES - 12, 1);
|
||||
lv_obj_set_pos(menu_rule, 6, 23);
|
||||
lv_obj_set_style_bg_opa(menu_rule, LV_OPA_COVER, 0);
|
||||
for (int i = 0; i < UI_MENU_ROWS; i++) {
|
||||
menu_row[i] = lv_obj_create(scr_menu);
|
||||
lv_obj_remove_style_all(menu_row[i]);
|
||||
lv_obj_set_size(menu_row[i], LCD_H_RES, 24);
|
||||
lv_obj_set_pos(menu_row[i], 0, 24 + i * 24);
|
||||
lv_obj_set_size(menu_row[i], LCD_H_RES - 12, 20);
|
||||
lv_obj_set_pos(menu_row[i], 6, 26 + i * 21);
|
||||
lv_obj_set_style_bg_opa(menu_row[i], LV_OPA_COVER, 0);
|
||||
menu_label[i] = label(menu_row[i], &lv_font_montserrat_14, 0xffffff);
|
||||
lv_obj_set_width(menu_label[i], 190);
|
||||
lv_label_set_long_mode(menu_label[i], LV_LABEL_LONG_DOT);
|
||||
lv_obj_align(menu_label[i], LV_ALIGN_LEFT_MID, 10, 0);
|
||||
menu_value[i] = label(menu_row[i], &lv_font_montserrat_14, 0xc4ccd8);
|
||||
lv_obj_align(menu_value[i], LV_ALIGN_RIGHT_MID, -10, 0);
|
||||
menu_label[i] = label(menu_row[i], th->row_font, th->text);
|
||||
lv_obj_set_width(menu_label[i], 200);
|
||||
lv_label_set_long_mode(menu_label[i], LV_LABEL_LONG_CLIP);
|
||||
lv_obj_align(menu_label[i], LV_ALIGN_LEFT_MID, 4, 0);
|
||||
menu_value[i] = label(menu_row[i], th->row_font, th->text);
|
||||
lv_obj_align(menu_value[i], LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
}
|
||||
menu_hint = label(scr_menu, th->hint_font, th->dim);
|
||||
lv_obj_set_pos(menu_hint, 6, 154);
|
||||
cursor_timer = lv_timer_create(cursor_blink_cb, 500, NULL);
|
||||
ui_set_theme(0);
|
||||
|
||||
lv_screen_load(scr_status);
|
||||
lvgl_port_unlock();
|
||||
@@ -140,7 +183,9 @@ void ui_init(void)
|
||||
void ui_status(const char *title, const char *msg)
|
||||
{
|
||||
lvgl_port_lock(0);
|
||||
lv_label_set_text(lbl_title, title ? title : "");
|
||||
char buf[200];
|
||||
snprintf(buf, sizeof buf, "> %s", title ? title : "");
|
||||
lv_label_set_text(lbl_title, buf);
|
||||
lv_label_set_text(lbl_msg, msg ? msg : "");
|
||||
if (lv_screen_active() != scr_status) lv_screen_load(scr_status);
|
||||
lvgl_port_unlock();
|
||||
@@ -237,20 +282,28 @@ void ui_set_marker(const char *text)
|
||||
void ui_menu_show(const char *title, const ui_menu_row_t *rows, int count, int top, int selected, bool editing)
|
||||
{
|
||||
lvgl_port_lock(0);
|
||||
lv_label_set_text(menu_title, title);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof buf, "T-EMBED://%s", title);
|
||||
for (char *p = buf; *p; p++) *p = (*p >= 'a' && *p <= 'z') ? *p - 32 : *p;
|
||||
lv_label_set_text(menu_title, buf);
|
||||
lv_obj_update_layout(menu_title);
|
||||
lv_obj_set_pos(menu_cursor, 6 + lv_obj_get_width(menu_title), 2);
|
||||
for (int i = 0; i < UI_MENU_ROWS; i++) {
|
||||
int idx = top + i;
|
||||
bool present = idx < count;
|
||||
lv_obj_set_hidden(menu_row[i], !present);
|
||||
if (!present) continue;
|
||||
bool sel = idx == selected;
|
||||
lv_obj_set_style_bg_color(menu_row[i], lv_color_hex(sel ? 0x2f81f7 : 0x000000), 0);
|
||||
lv_label_set_text(menu_label[i], rows[idx].label);
|
||||
lv_obj_set_style_text_color(menu_label[i], lv_color_hex(rows[idx].dim ? 0x6c7684 : 0xffffff), 0);
|
||||
uint32_t fg = sel ? th->sel_text : rows[idx].dim ? th->dim : th->text;
|
||||
lv_obj_set_style_bg_color(menu_row[i], lv_color_hex(sel ? th->sel_bg : th->bg), 0);
|
||||
snprintf(menu_line[i], sizeof menu_line[i], "%02d%c%s", idx + 1, sel ? '>' : ' ', rows[idx].label);
|
||||
lv_label_set_text(menu_label[i], menu_line[i]);
|
||||
lv_obj_set_style_text_color(menu_label[i], lv_color_hex(fg), 0);
|
||||
lv_label_set_text(menu_value[i], rows[idx].value ? rows[idx].value : "");
|
||||
lv_obj_set_style_text_color(menu_value[i],
|
||||
lv_color_hex(sel && editing ? 0xffd040 : sel ? 0xffffff : 0xc4ccd8), 0);
|
||||
lv_obj_set_style_text_color(menu_value[i], lv_color_hex(sel && editing ? th->edit : fg), 0);
|
||||
}
|
||||
snprintf(buf, sizeof buf, "[%02d/%02d] turn:move click:%s hold:back", selected + 1, count, editing ? "set" : "ok");
|
||||
lv_label_set_text(menu_hint, buf);
|
||||
if (lv_screen_active() != scr_menu) {
|
||||
prev_screen = lv_screen_active();
|
||||
lv_screen_load(scr_menu);
|
||||
@@ -272,3 +325,37 @@ void ui_set_battery(const char *text)
|
||||
else { lv_label_set_text(lbl_batt, text); lv_obj_set_hidden(lbl_batt, false); }
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
|
||||
int ui_theme_count(void) { return sizeof themes / sizeof themes[0]; }
|
||||
const char *ui_theme_name(int idx) { return themes[idx < 0 || idx >= ui_theme_count() ? 0 : idx].name; }
|
||||
|
||||
static void apply(lv_obj_t *l, const lv_font_t *f, uint32_t rgb)
|
||||
{
|
||||
lv_obj_set_style_text_font(l, f, 0);
|
||||
lv_obj_set_style_text_color(l, lv_color_hex(rgb), 0);
|
||||
}
|
||||
|
||||
void ui_set_theme(int idx)
|
||||
{
|
||||
if (idx < 0 || idx >= ui_theme_count()) idx = 0;
|
||||
lvgl_port_lock(0);
|
||||
th = &themes[idx];
|
||||
lv_obj_set_style_bg_color(scr_status, lv_color_hex(th->bg), 0);
|
||||
lv_obj_set_style_bg_color(scr_menu, lv_color_hex(th->bg), 0);
|
||||
apply(status_hdr, th->hint_font, th->dim);
|
||||
apply(lbl_title, th->title_font, th->text);
|
||||
apply(lbl_msg, th->row_font, th->text);
|
||||
apply(status_cursor, th->row_font, th->text);
|
||||
apply(menu_title, th->title_font, th->text);
|
||||
apply(menu_cursor, th->title_font, th->text);
|
||||
lv_obj_set_style_bg_color(menu_rule, lv_color_hex(th->dim), 0);
|
||||
for (int i = 0; i < UI_MENU_ROWS; i++) {
|
||||
lv_obj_set_style_text_font(menu_label[i], th->row_font, 0);
|
||||
lv_obj_set_style_text_font(menu_value[i], th->row_font, 0);
|
||||
}
|
||||
apply(menu_hint, th->hint_font, th->dim);
|
||||
apply(toast, th->row_font, th->text);
|
||||
lv_obj_set_style_bg_color(toast, lv_color_hex(th->bg), 0);
|
||||
lv_obj_set_style_border_color(toast, lv_color_hex(th->text), 0);
|
||||
lvgl_port_unlock();
|
||||
}
|
||||
@@ -5,6 +5,11 @@
|
||||
// All calls take the LVGL lock themselves; safe from any task.
|
||||
void ui_init(void);
|
||||
|
||||
// Colour/font themes for the system pages (menu, status, toast).
|
||||
int ui_theme_count(void);
|
||||
const char *ui_theme_name(int idx);
|
||||
void ui_set_theme(int idx);
|
||||
|
||||
// Full-screen text page (boot, Wi-Fi setup, fatal errors).
|
||||
void ui_status(const char *title, const char *msg);
|
||||
|
||||
|
||||
@@ -48,3 +48,7 @@ CONFIG_LV_USE_BAR=y
|
||||
CONFIG_LV_USE_LABEL=y
|
||||
CONFIG_LV_USE_PERF_MONITOR=n
|
||||
|
||||
|
||||
# terminal-style UI fonts
|
||||
CONFIG_LV_FONT_UNSCII_8=y
|
||||
CONFIG_LV_FONT_UNSCII_16=y
|
||||
Reference in new issue
Block a user