diff --git a/README.md b/README.md index 8154dff..245b792 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/main/main.c b/main/main.c index ccf52f1..0a3e136 100644 --- a/main/main.c +++ b/main/main.c @@ -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(); } diff --git a/main/menu.c b/main/menu.c index ffd3da8..9a51ad8 100644 --- a/main/menu.c +++ b/main/menu.c @@ -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(); diff --git a/main/settings.c b/main/settings.c index 764656f..3d6268b 100644 --- a/main/settings.c +++ b/main/settings.c @@ -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 +#include #include #include @@ -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); diff --git a/main/settings.h b/main/settings.h index 22dd862..1ff9541 100644 --- a/main/settings.h +++ b/main/settings.h @@ -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 diff --git a/main/slideshow.c b/main/slideshow.c index 895b123..d7bd8b8 100644 --- a/main/slideshow.c +++ b/main/slideshow.c @@ -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]; diff --git a/main/ui.c b/main/ui.c index 96f77c0..1ce82cc 100644 --- a/main/ui.c +++ b/main/ui.c @@ -4,10 +4,41 @@ #include "lvgl.h" #include -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(); +} diff --git a/main/ui.h b/main/ui.h index 20da042..56d91a5 100644 --- a/main/ui.h +++ b/main/ui.h @@ -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); diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 063c237..df31f71 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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