From ece849db3b87927572db578858ce3f897024bb95 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:35:26 +0200 Subject: [PATCH] refactor(ui-lvgl): review -- shared button bar, confirms, notes, local time - Dead code gone: anim::fadeHide, radioctl::presetCount, the L2 board's setSdPower / setGrovePower / expanderOK (awRead, gnssReset now private). - buttonBar() / barButton() replace 7 copies of the popup button row. - confirmBody(): one red-button confirm for reboot / power off, reset counters, admin commands, preset and scope delete. - tapConfirmed(): the 11 "tap again" buttons share one mechanism, 3 s everywhere, the label restored when left alone. - noteLabel() for ~30 wrapped notes. - localTm() in NodePrefs.h and one MONTHS table: lvgl, MsgExpand, the L1 clock and lock screen. - The sim keeps UI settings in an in-memory nvs::, so one set of load / save helpers serves both. - Asleep, the loop wakes every 50 ms (the button / touch poll), not 20. Co-Authored-By: Claude Opus 5.5 --- docs/development/plan-next.md | 10 + examples/companion_radio/MsgExpand.h | 9 +- examples/companion_radio/NodePrefs.h | 8 + .../companion_radio/ui-core/RadioControl.h | 6 - .../companion_radio/ui-lvgl/AdminScreen.h | 27 +-- examples/companion_radio/ui-lvgl/Anim.h | 11 -- .../companion_radio/ui-lvgl/ChannelScreen.h | 40 +--- .../companion_radio/ui-lvgl/CompassScreen.h | 4 +- .../ui-lvgl/ConversationScreen.h | 47 ++--- .../companion_radio/ui-lvgl/DeviceScreen.h | 23 +-- examples/companion_radio/ui-lvgl/DiagScreen.h | 20 +- examples/companion_radio/ui-lvgl/GpsScreen.h | 4 +- examples/companion_radio/ui-lvgl/LvglPort.h | 143 +++++++------- examples/companion_radio/ui-lvgl/MapAreas.h | 26 +-- examples/companion_radio/ui-lvgl/MapRegions.h | 36 +--- examples/companion_radio/ui-lvgl/MapScreen.h | 21 +-- examples/companion_radio/ui-lvgl/NavMap.h | 111 +++-------- examples/companion_radio/ui-lvgl/OtaScreen.h | 4 +- .../companion_radio/ui-lvgl/RadioExtras.h | 45 ++--- .../companion_radio/ui-lvgl/RepeaterScreen.h | 5 +- .../companion_radio/ui-lvgl/StorageScreen.h | 52 +---- examples/companion_radio/ui-lvgl/UITask.cpp | 177 ++++++++++-------- examples/companion_radio/ui-lvgl/UITask.h | 4 - examples/companion_radio/ui-new/UITask.cpp | 14 +- variants/wio-tracker-l2/WioTrackerL2Board.h | 24 +-- 25 files changed, 308 insertions(+), 563 deletions(-) diff --git a/docs/development/plan-next.md b/docs/development/plan-next.md index e760b11b..012d3d7f 100644 --- a/docs/development/plan-next.md +++ b/docs/development/plan-next.md @@ -76,6 +76,16 @@ for a second radio). They are L2-specific and block the loop for ~90 s. places to speed things up or save RAM, flash and battery. First a list of findings to accept, then fixes in batches, each checked in the sim and on the device. + Done 2026-09-28 (-265 lines): dead code (anim::fadeHide, + presetCount, the L2 board's setSdPower / setGrovePower / expanderOK); + one buttonBar() / barButton() for 7 copies of the button row; + confirmBody() for 5 red-button popups; tapConfirmed() for 11 + "tap again" buttons, all 3 s and the label restored; noteLabel() for + ~30 wrapped notes; localTm() in NodePrefs.h and one MONTHS table + (lvgl, MsgExpand, the L1 clock); the sim keeps UI settings in an + in-memory nvs::, one set of load / save helpers; asleep, the loop + wakes every 50 ms, not 20. Left: map tables to PSRAM only if the + internal heap gets tight; emoji 1.25 MB of flash (fits). ## G. Core parity with L1 SOLO diff --git a/examples/companion_radio/MsgExpand.h b/examples/companion_radio/MsgExpand.h index f647ed85..f3dd7792 100644 --- a/examples/companion_radio/MsgExpand.h +++ b/examples/companion_radio/MsgExpand.h @@ -4,6 +4,7 @@ #include #include #include +#include "NodePrefs.h" // localTm() #include // Expands placeholders in tmpl into out (out_len bytes). @@ -85,12 +86,10 @@ inline void expandMsg(const char* tmpl, char* out, int out_len, else strcpy(lb, "no GPS"); APPEND(lb, strlen(lb)); p += 5; } else if (strncmp(p, "{time}", 6) == 0) { - if (utc_ts > 1000000000UL) { - uint32_t local_ts = utc_ts + (int32_t)tz_hours * 3600; - time_t t = (time_t)local_ts; - struct tm* ti = gmtime(&t); + struct tm ti; + if (localTm(utc_ts, tz_hours, ti)) { char tb[8]; - snprintf(tb, sizeof(tb), "%02d:%02d", ti->tm_hour, ti->tm_min); + snprintf(tb, sizeof(tb), "%02d:%02d", ti.tm_hour, ti.tm_min); APPEND(tb, strlen(tb)); } p += 6; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 7f029b4a..1d2eb7fa 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include // Firmware's own boot-default radio params — used both as the companion's // initial freq/sf/bw/cr (MyMesh.cpp) and, here, as the seed for a never- @@ -724,6 +725,13 @@ static inline bool localHour(uint32_t utc, int8_t tz_hours, int& h) { h = (int)(((int64_t)utc + (int64_t)tz_hours * 3600) / 3600 % 24); return true; } +// A UTC time as the local calendar date and time; false while the clock isn't set. +static inline bool localTm(uint32_t utc, int8_t tz_hours, struct tm& out) { + if (utc < 1000000000UL) return false; + time_t t = (time_t)((int64_t)utc + (int64_t)tz_hours * 3600); + out = *gmtime(&t); + return true; +} // Settings > Sound > Quiet hours in force at `utc` (never with the clock unset). static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) { int h; diff --git a/examples/companion_radio/ui-core/RadioControl.h b/examples/companion_radio/ui-core/RadioControl.h index 4a406e24..3c206fcc 100644 --- a/examples/companion_radio/ui-core/RadioControl.h +++ b/examples/companion_radio/ui-core/RadioControl.h @@ -27,12 +27,6 @@ static void applyApc() { the_mesh.applyApc(); } // (re)initialise Adaptive Pow // ── Presets ───────────────────────────────────────────────────────────────── // One list: built-ins 0..RADIO_PRESET_COUNT-1, then the non-empty user slots. -static int presetCount(const NodePrefs* p) { - int n = RADIO_PRESET_COUNT; - for (int i = 0; p && i < NodePrefs::USER_RADIO_PRESET_MAX; i++) if (p->user_radio_presets[i].name[0]) n++; - return n; -} - // Preset `idx` of the list; false past the end. static bool presetAt(const NodePrefs* p, int idx, const char*& name, float& freq, float& bw, uint8_t& sf, uint8_t& cr) { if (idx < 0) return false; diff --git a/examples/companion_radio/ui-lvgl/AdminScreen.h b/examples/companion_radio/ui-lvgl/AdminScreen.h index bd0ef886..945e51ad 100644 --- a/examples/companion_radio/ui-lvgl/AdminScreen.h +++ b/examples/companion_radio/ui-lvgl/AdminScreen.h @@ -96,10 +96,8 @@ void UITask::buildAdminPick() { } } if (!rows) { - lv_obj_t* t = label(body, "No repeaters or room servers yet. They show up here once their advert is heard.", - THEME_FONT_BODY, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); + noteLabel(body, "No repeaters or room servers yet. They show up here once their advert is heard.", + THEME_FONT_BODY, theme::TEXT_MUTED); } } @@ -219,18 +217,7 @@ void UITask::adminRow(int row) { const char* prompt = admin::confirmPrompt(f); if (prompt) { // takes the node out of service for a while: ask first s_confirm = &f; - lv_obj_t* panel = navPopupPanel(prompt, false); - lv_obj_t* t = label(panel, "It's offline until it comes back.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); - lv_obj_t* b = lv_button_create(panel); - lv_obj_set_size(b, LV_PCT(100), 40); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0); - lv_obj_add_event_cb(b, onAdminValue, LV_EVENT_CLICKED, (void*)(uintptr_t)V_CONFIRM); - lv_obj_center(label(b, f.label, THEME_FONT_BODY, theme::TEXT)); + confirmBody(navPopupPanel(prompt, false), "It's offline until it comes back.", f.label, onAdminValue, V_CONFIRM); return; } S.run(f); @@ -337,9 +324,7 @@ void UITask::adminValuePopup() { lv_obj_add_event_cb(s_choice, onAdminChoice, LV_EVENT_VALUE_CHANGED, NULL); } if (f->isRadio()) { - lv_obj_t* note = label(panel, "A mismatch cuts the node off.", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP); - lv_obj_set_width(note, LV_PCT(100)); + noteLabel(panel, "A mismatch cuts the node off."); } lv_obj_t* r = adminRowBox(panel); adminPopupButton(r, LV_SYMBOL_OK " Save", V_SAVE, true); @@ -395,9 +380,7 @@ void UITask::adminReplyPopup(const char* text) { lv_obj_set_flex_grow(box, 1); lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_ACTIVE); } - lv_obj_t* l = label(box, text[0] ? text : "(empty)", THEME_FONT_BODY, theme::TEXT); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + noteLabel(box, text[0] ? text : "(empty)", THEME_FONT_BODY, theme::TEXT); lv_obj_t* r = adminRowBox(panel); adminPopupButton(r, "OK", adminview::V_CLOSE, false); } diff --git a/examples/companion_radio/ui-lvgl/Anim.h b/examples/companion_radio/ui-lvgl/Anim.h index 069c1ee8..1b3ca1ae 100644 --- a/examples/companion_radio/ui-lvgl/Anim.h +++ b/examples/companion_radio/ui-lvgl/Anim.h @@ -75,15 +75,4 @@ namespace anim { lv_obj_set_style_translate_x(o, 0, 0); run(o, setOpa, LV_OPA_TRANSP, LV_OPA_COVER, ms); } - - static void hideDone(lv_anim_t* a) { - lv_obj_t* o = (lv_obj_t*)a->var; - lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN); - lv_obj_set_style_opa(o, LV_OPA_COVER, 0); - } - // Fade out, then hide (the object stays for reuse). - static void fadeHide(lv_obj_t* o) { - lv_anim_delete(o, setOpa); - run(o, setOpa, lv_obj_get_style_opa(o, LV_PART_MAIN), LV_OPA_TRANSP, OUT_MS, hideDone); - } } diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index 167b8339..76d7a190 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -17,7 +17,6 @@ enum : uint8_t { C_NOTIF, C_SCOPE, C_MELODY }; static int s_idx = -1; // channel the popup / form is about (-1: adding) static uint8_t s_type = T_HASHTAG; static bool s_hex = false; -static uint32_t s_del_armed_ms = 0; static lv_obj_t* s_del_lbl = nullptr; static lv_obj_t* s_fav_btn = nullptr; // Add / Edit form @@ -69,7 +68,6 @@ void UITask::channelMenu(int idx) { ChannelDetails ch; if (!chanctl::exists(idx) || !the_mesh.getChannel(idx, ch)) return; s_idx = idx; - s_del_armed_ms = 0; s_del_lbl = s_fav_btn = nullptr; char title[40]; snprintf(title, sizeof(title), "%s", ch.name); // as named: "#" marks a hashtag channel @@ -89,12 +87,7 @@ void UITask::channelMenu(int idx) { (void*)(uintptr_t)C_SCOPE); } - lv_obj_t* acts = lv_obj_create(panel); - styleSurface(acts, theme::BG); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(acts, theme::GAP, 0); + lv_obj_t* acts = buttonBar(panel); bool unread = _core->history.chUnread(idx) > 0; struct { const char* text; uint8_t act; } btns[] = { { UI_SYMBOL_STAR " Fav", A_FAV }, @@ -105,21 +98,10 @@ void UITask::channelMenu(int idx) { }; for (auto& b : btns) { if (b.act == A_READ && !unread) continue; - lv_obj_t* bt = lv_button_create(acts); - lv_obj_set_height(bt, 40); - lv_obj_set_flex_grow(bt, 1); - lv_obj_set_style_pad_hor(bt, 4, 0); - lv_obj_set_style_radius(bt, theme::RADIUS, 0); - lv_obj_set_style_shadow_width(bt, 0, 0); - lv_obj_set_style_bg_color(bt, lv_color_hex(theme::SURFACE), 0); - lv_obj_add_event_cb(bt, onChanAction, LV_EVENT_CLICKED, (void*)(uintptr_t)b.act); - lv_obj_t* l = label(bt, b.text, THEME_FONT_SMALL, theme::TEXT); - lv_obj_center(l); - if (b.act == A_DELETE) s_del_lbl = l; + lv_obj_t* bt = barButton(acts, b.text, onChanAction, b.act, b.act == A_FAV && chanctl::favourite(_prefs, idx)); + if (b.act == A_DELETE) s_del_lbl = lv_obj_get_child(bt, 0); if (b.act == A_FAV) s_fav_btn = bt; } - if (s_fav_btn && chanctl::favourite(_prefs, idx)) - lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(theme::ACCENT_DIM), 0); } void UITask::channelSet(uint8_t which, int v) { @@ -140,7 +122,7 @@ void UITask::channelAction(uint8_t act) { bool on = !chanctl::favourite(_prefs, idx); chanctl::setFavourite(_prefs, idx, on); prefsSave(); - if (s_fav_btn) lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0); + if (s_fav_btn) barButtonOn(s_fav_btn, on); showToast(on ? "Added to favourites" : "Removed from favourites", 1200); if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star break; @@ -158,11 +140,7 @@ void UITask::channelAction(uint8_t act) { pinPopup(true, (uint8_t)idx, nullptr); // DeviceScreen.h break; case A_DELETE: - if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // second tap within 3 s confirms - s_del_armed_ms = millis() | 1; - if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?"); - break; - } + if (!tapConfirmed(s_del_lbl, "Delete?")) break; chanctl::remove(idx); navClosePopup(); showToast("Channel deleted"); @@ -201,9 +179,7 @@ void UITask::buildChannelEdit() { lv_obj_add_event_cb(s_types, onChanType, LV_EVENT_VALUE_CHANGED, NULL); } - s_hint = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP); - lv_obj_set_width(s_hint, LV_PCT(100)); + s_hint = noteLabel(body, ""); s_name = chanField(body, "Name"); lv_textarea_set_max_length(s_name, sizeof(((ChannelDetails*)0)->name) - 1); s_secret = chanField(body, "Passphrase"); @@ -223,9 +199,7 @@ void UITask::buildChannelEdit() { lv_obj_add_event_cb(save, onChanSave, LV_EVENT_CLICKED, NULL); lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT)); stylePrimary(save); - s_status = label(body, "", THEME_FONT_SMALL, theme::FAIL); - lv_label_set_long_mode(s_status, LV_LABEL_LONG_WRAP); - lv_obj_set_width(s_status, LV_PCT(100)); + s_status = noteLabel(body, "", THEME_FONT_SMALL, theme::FAIL); if (edit) { ChannelDetails ch; diff --git a/examples/companion_radio/ui-lvgl/CompassScreen.h b/examples/companion_radio/ui-lvgl/CompassScreen.h index efbfcbb0..e88c1e3a 100644 --- a/examples/companion_radio/ui-lvgl/CompassScreen.h +++ b/examples/companion_radio/ui-lvgl/CompassScreen.h @@ -105,9 +105,7 @@ void UITask::buildCompass() { s_deg = label(num, "", THEME_FONT_CLOCK, theme::TEXT); s_degsign = label(num, "", THEME_FONT_LARGE, theme::TEXT); s_card = label(col, "", THEME_FONT_LARGE, theme::ACCENT); - s_hint = label(col, "", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP); - lv_obj_set_width(s_hint, LV_PCT(100)); + s_hint = noteLabel(col, ""); lv_obj_set_style_text_align(s_hint, LV_TEXT_ALIGN_CENTER, 0); refreshCompass(); } diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index 8f1aad9c..2ff0d460 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -25,28 +25,6 @@ static bool s_login_wait = false; // open the room once this login answers static lv_obj_t* s_fav_btn = nullptr; static int s_msg = -1; // s_msg_meta index the message popup is about -static lv_obj_t* actionRow(lv_obj_t* panel) { - lv_obj_t* acts = lv_obj_create(panel); - styleSurface(acts, theme::BG); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(acts, theme::GAP, 0); - return acts; -} - -static lv_obj_t* actionButton(lv_obj_t* acts, const char* text, lv_event_cb_t cb, uint8_t act, bool on) { - lv_obj_t* bt = lv_button_create(acts); - lv_obj_set_height(bt, 40); - lv_obj_set_flex_grow(bt, 1); - lv_obj_set_style_pad_hor(bt, 4, 0); - lv_obj_set_style_radius(bt, theme::RADIUS, 0); - lv_obj_set_style_shadow_width(bt, 0, 0); - lv_obj_set_style_bg_color(bt, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0); - lv_obj_add_event_cb(bt, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)act); - lv_obj_center(label(bt, text, THEME_FONT_SMALL, theme::TEXT)); - return bt; -} } // namespace convview @@ -172,13 +150,13 @@ void UITask::conversationMenu(const uint8_t* pub_key) { lv_obj_set_width(st, LV_PCT(100)); } - lv_obj_t* acts = actionRow(panel); - s_fav_btn = actionButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV, contactctl::favourite(ci)); - if (!room && _core->dmUnread(ci.id.pub_key) > 0) actionButton(acts, LV_SYMBOL_OK " Read", onConvAction, A_READ, false); - actionButton(acts, UI_SYMBOL_PIN, onConvAction, A_PIN, favslots::findContact(_prefs, ci.id.pub_key) >= 0); + lv_obj_t* acts = buttonBar(panel); + s_fav_btn = barButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV, contactctl::favourite(ci)); + if (!room && _core->dmUnread(ci.id.pub_key) > 0) barButton(acts, LV_SYMBOL_OK " Read", onConvAction, A_READ, false); + barButton(acts, UI_SYMBOL_PIN, onConvAction, A_PIN, favslots::findContact(_prefs, ci.id.pub_key) >= 0); if (room) { - actionButton(acts, LV_SYMBOL_EDIT " Login", onConvAction, A_LOGIN, false); - if (_core->rooms.isLoggedIn(ci.id.pub_key)) actionButton(acts, LV_SYMBOL_CLOSE " Logout", onConvAction, A_LOGOUT, false); + barButton(acts, LV_SYMBOL_EDIT " Login", onConvAction, A_LOGIN, false); + if (_core->rooms.isLoggedIn(ci.id.pub_key)) barButton(acts, LV_SYMBOL_CLOSE " Logout", onConvAction, A_LOGOUT, false); } } @@ -196,7 +174,7 @@ void UITask::conversationAction(uint8_t act) { case A_FAV: { bool on = !contactctl::favourite(ci); if (!contactctl::setFavourite(ci.id.pub_key, on)) break; - if (s_fav_btn) lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0); + if (s_fav_btn) barButtonOn(s_fav_btn, on); showToast(on ? "Added to favourites" : "Removed from favourites", 1200); if (_screen == SCR_CHATS) { buildChats(); conversationMenu(s_key); } // star / filter break; @@ -314,9 +292,8 @@ void UITask::messageMenu(int idx) { char when[64], age[16]; uint32_t now = rtc_clock.getCurrentTime(); geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now); - if (ts > 1000000000UL) { - time_t t = (time_t)((int64_t)ts + (int64_t)(_prefs ? _prefs->tz_offset_hours : 0) * 3600); - struct tm ti = *gmtime(&t); + struct tm ti; + if (localTime(_prefs, ti, ts)) { char clk[12]; fmtClock(clk, sizeof(clk), ti, _prefs, true); if (clockBehind(now, ts)) { // the clock isn't set yet: the date instead of an age @@ -372,9 +349,9 @@ void UITask::messageMenu(int idx) { bool can_reply = !m.own && m.from[0] && _compose_ta; if (!can_reply && m.loc < 0) return; - lv_obj_t* acts = actionRow(panel); - if (can_reply) actionButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false); - if (m.loc >= 0) actionButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false); + lv_obj_t* acts = buttonBar(panel); + if (can_reply) barButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false); + if (m.loc >= 0) barButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false); } void UITask::messageAction(uint8_t act) { diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index 1bde4561..3cd412d6 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -147,19 +147,10 @@ void UITask::nodeNameDone(bool ok) { } void UITask::powerPopup(bool restart) { - lv_obj_t* panel = navPopupPanel(restart ? "Reboot?" : "Power off?", false); - lv_obj_t* t = label(panel, restart ? "Restarts the device. Messages and settings are kept." - : "Turns the device off. Messages and settings are kept; no messages arrive while it is off.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); - lv_obj_t* b = lv_button_create(panel); - lv_obj_set_size(b, LV_PCT(100), 40); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0); - lv_obj_add_event_cb(b, onPowerGo, LV_EVENT_CLICKED, (void*)(uintptr_t)(restart ? 1 : 0)); - lv_obj_center(label(b, restart ? LV_SYMBOL_REFRESH " Reboot" : LV_SYMBOL_POWER " Power off", THEME_FONT_BODY, theme::TEXT)); + confirmBody(navPopupPanel(restart ? "Reboot?" : "Power off?", false), + restart ? "Restarts the device. Messages and settings are kept." + : "Turns the device off. Messages and settings are kept; no messages arrive while it is off.", + restart ? LV_SYMBOL_REFRESH " Reboot" : LV_SYMBOL_POWER " Power off", onPowerGo, restart ? 1 : 0); } // ── Lock screen ─────────────────────────────────────────────────────────────── @@ -503,9 +494,9 @@ void UITask::favHold(int slot) { if (!contactctl::favName(_prefs, slot, name, sizeof(name))) { favPickPopup(slot); return; } devview::s_fav_slot = slot; lv_obj_t* panel = navPopupPanel(name, false); - lv_obj_t* acts = convview::actionRow(panel); - convview::actionButton(acts, LV_SYMBOL_EDIT " Change", onFavAction, devview::F_CHANGE, false); - convview::actionButton(acts, LV_SYMBOL_TRASH " Remove", onFavAction, devview::F_REMOVE, false); + lv_obj_t* acts = buttonBar(panel); + barButton(acts, LV_SYMBOL_EDIT " Change", onFavAction, devview::F_CHANGE, false); + barButton(acts, LV_SYMBOL_TRASH " Remove", onFavAction, devview::F_REMOVE, false); } void UITask::favAction(uint8_t act) { diff --git a/examples/companion_radio/ui-lvgl/DiagScreen.h b/examples/companion_radio/ui-lvgl/DiagScreen.h index 8bd25794..d9b8c728 100644 --- a/examples/companion_radio/ui-lvgl/DiagScreen.h +++ b/examples/companion_radio/ui-lvgl/DiagScreen.h @@ -284,10 +284,8 @@ void UITask::buildDiag() { s_noise_status = nullptr; if (s_tab == TAB_NOISE) { - lv_obj_t* t = label(s_list, "What the radio hears with nothing on air, on the mesh frequency and 1.1 MHz " - "either side. Takes about 12 seconds.", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); + noteLabel(s_list, "What the radio hears with nothing on air, on the mesh frequency and 1.1 MHz " + "either side. Takes about 12 seconds."); lv_obj_t* b = lv_button_create(s_list); lv_obj_set_size(b, LV_PCT(100), 40); lv_obj_set_style_shadow_width(b, 0, 0); @@ -363,18 +361,8 @@ void UITask::refreshDiag() { } void UITask::diagResetPopup() { - lv_obj_t* panel = navPopupPanel("Reset counters?", false); - lv_obj_t* t = label(panel, "Zeroes packet and error counts.", THEME_FONT_SMALL, - theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); - lv_obj_t* b = lv_button_create(panel); - lv_obj_set_size(b, LV_PCT(100), 40); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0); - lv_obj_add_event_cb(b, onDiagResetGo, LV_EVENT_CLICKED, NULL); - lv_obj_center(label(b, LV_SYMBOL_REFRESH " Reset", THEME_FONT_BODY, theme::TEXT)); + confirmBody(navPopupPanel("Reset counters?", false), "Zeroes packet and error counts.", LV_SYMBOL_REFRESH " Reset", + onDiagResetGo, 0); } void UITask::diagReset() { diff --git a/examples/companion_radio/ui-lvgl/GpsScreen.h b/examples/companion_radio/ui-lvgl/GpsScreen.h index 06a0d4ff..2f829076 100644 --- a/examples/companion_radio/ui-lvgl/GpsScreen.h +++ b/examples/companion_radio/ui-lvgl/GpsScreen.h @@ -245,9 +245,7 @@ void UITask::buildGps() { lv_obj_set_style_pad_row(col, 2, 0); lv_obj_set_style_pad_top(col, 4, 0); s_status = label(col, "", THEME_FONT_LARGE, theme::TEXT); - s_used = label(col, "", THEME_FONT_BODY, theme::TEXT); - lv_label_set_long_mode(s_used, LV_LABEL_LONG_WRAP); - lv_obj_set_width(s_used, LV_PCT(100)); + s_used = noteLabel(col, "", THEME_FONT_BODY, theme::TEXT); lv_obj_set_style_pad_bottom(s_used, 4, 0); lv_obj_t* facts = infoCard(col); // no card of its own: rows on the page lv_obj_set_style_bg_opa(facts, LV_OPA_TRANSP, 0); diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index dd6c9522..d688269d 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -387,47 +387,6 @@ static void wifiStoreWrite() { } } } -// Settings > WiFi's switch: off keeps the radio off for everything (scan, map -// download). Kept with the credentials. -static int8_t s_wifi_allowed = -1; // read once (the status bar asks every second) -static bool wifiAllowed() { - if (s_wifi_allowed < 0) s_wifi_allowed = nvs::getBool("mc_wifi", "on", true); - return s_wifi_allowed; -} -static void setWifiAllowed(bool on) { s_wifi_allowed = on; nvs::putBool("mc_wifi", "on", on); } -// Map tools > Live tiles: missing tiles fetched over WiFi while the map is open. -static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); } -static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); } -// Map tools > Hiking trails: the Waymarked Trails overlay over the map. -static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); } -static void setTrailsOn(bool on) { nvs::putBool("mc_ui", "trails", on); } -// Map tools > Vector map (test). -static bool vectorOn() { return nvs::getBool("mc_ui", "vector", false); } -static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", on); } - -// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none. -static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); } -static void savePin(const char* pin) { nvs::putStr("mc_lock", "pin", pin); } - -// Accent colour (Settings > Display & power): an index into theme::ACCENTS. -static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); } -static void saveAccent(int idx) { nvs::putU8("mc_ui", "accent", idx); } -// Settings > Storage > Kept per conversation (an index into histstore::KEEP). -static int loadHistKeep() { return nvs::getI8("mc_ui", "hkeep", -1); } -static void saveHistKeep(int idx) { nvs::putI8("mc_ui", "hkeep", idx); } -// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default. -static int loadLiveCap() { return nvs::getI8("mc_ui", "ltcap", -1); } -static void saveLiveCap(int idx) { nvs::putI8("mc_ui", "ltcap", idx); } -// Settings > Display & power > Tap to wake: a touch turns the dark screen on -// (off: only the top button does). -static bool loadTapWake() { return nvs::getBool("mc_ui", "tapwake", true); } -static void saveTapWake(bool on) { nvs::putBool("mc_ui", "tapwake", on); } -// Messages: which sections are folded (bit per section). -static int loadChatFold() { return nvs::getU8("mc_ui", "chfold", 0); } -static void saveChatFold(int bits) { nvs::putU8("mc_ui", "chfold", bits); } -// Home's apps: their letters in the user's order, a hidden one in lower case. -static void loadHomeApps(char* out, size_t n) { nvs::getStr("mc_ui", "apps", out, n); } -static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); } // Filesystem size and space in use (Settings > Storage), plus the card's own // size -- a card whose FAT partition is small (e.g. written by a Raspberry Pi // imager) shows both. The first free-space count on a big card takes a moment. @@ -697,6 +656,32 @@ static const char* fetchError() { return s_ferr; } // Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to // a , the host page feeds the mouse in as touch. +// The device's NVS (Preferences) as a table for the session: the UI settings +// below read and write it by the same names. +namespace nvs { +struct Kv { char ns[10]; char key[10]; char val[40]; }; +static Kv s_kv[24]; +static int s_kv_n = 0; +static Kv* find(const char* ns, const char* key, bool add) { + for (int i = 0; i < s_kv_n; i++) if (!strcmp(s_kv[i].ns, ns) && !strcmp(s_kv[i].key, key)) return &s_kv[i]; + if (!add || s_kv_n >= (int)(sizeof(s_kv) / sizeof(s_kv[0]))) return nullptr; + Kv* k = &s_kv[s_kv_n++]; + snprintf(k->ns, sizeof(k->ns), "%s", ns); + snprintf(k->key, sizeof(k->key), "%s", key); + return k; +} +static int getInt(const char* ns, const char* key, int def) { Kv* k = find(ns, key, false); return k ? atoi(k->val) : def; } +static void putInt(const char* ns, const char* key, int v) { if (Kv* k = find(ns, key, true)) snprintf(k->val, sizeof(k->val), "%d", v); } +static bool getBool(const char* ns, const char* key, bool def) { return getInt(ns, key, def) != 0; } +static void putBool(const char* ns, const char* key, bool v) { putInt(ns, key, v); } +static int getI8(const char* ns, const char* key, int def) { return getInt(ns, key, def); } +static void putI8(const char* ns, const char* key, int v) { putInt(ns, key, v); } +static int getU8(const char* ns, const char* key, int def) { return getInt(ns, key, def); } +static void putU8(const char* ns, const char* key, int v) { putInt(ns, key, v); } +static void getStr(const char* ns, const char* key, char* out, size_t n) { Kv* k = find(ns, key, false); snprintf(out, n, "%s", k ? k->val : ""); } +static void putStr(const char* ns, const char* key, const char* v) { if (Kv* k = find(ns, key, true)) snprintf(k->val, sizeof(k->val), "%s", v); } +} // namespace nvs + static bool s_swallow = false; static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { @@ -763,27 +748,6 @@ static void restart() {} // the page's Reset does that // The browser is always online; saved WiFi networks only live for the session. static void wifiStoreLoad() { s_net_count = 0; } static void wifiStoreWrite() {} -static char s_pin[9] = ""; // the screen PIN, for the session -static void loadPin(char* out, size_t n) { snprintf(out, n, "%s", s_pin); } -static void savePin(const char* pin) { snprintf(s_pin, sizeof(s_pin), "%s", pin); } -static int s_accent = 0; -static int loadAccent() { return s_accent; } -static void saveAccent(int idx) { s_accent = idx; } -static int s_hist_keep = -1; -static int loadHistKeep() { return s_hist_keep; } -static void saveHistKeep(int idx) { s_hist_keep = idx; } -static int s_live_cap = -1; -static int loadLiveCap() { return s_live_cap; } -static void saveLiveCap(int idx) { s_live_cap = idx; } -static bool s_tap_wake = true; -static bool loadTapWake() { return s_tap_wake; } -static void saveTapWake(bool on) { s_tap_wake = on; } -static int s_chat_fold = 0; -static int loadChatFold() { return s_chat_fold; } -static void saveChatFold(int bits) { s_chat_fold = bits; } -static char s_home_apps[24] = ""; -static void loadHomeApps(char* out, size_t n) { snprintf(out, n, "%s", s_home_apps); } -static void saveHomeApps(const char* v) { snprintf(s_home_apps, sizeof(s_home_apps), "%s", v); } // The browser has no card: a nominal 32 GB, used = what the files add up to // (the storage screen counts them anyway; it passes that in). static bool sdInfo(uint64_t& total, uint64_t& used, uint64_t& card) { total = card = 32ULL << 30; used = 0; return true; } @@ -791,18 +755,6 @@ static const char* const FLASH_ROOT = "/sim_data"; static const char* resetReason() { return "Power on"; } static bool crashSummary(char*, size_t) { return false; } static bool flashInfo(uint64_t& total, uint64_t& used) { total = 1536ULL << 10; used = 0; return true; } -static bool s_wifi_on = true; -static bool wifiAllowed() { return s_wifi_on; } -static void setWifiAllowed(bool on) { s_wifi_on = on; } -static bool s_live_tiles = true; -static bool liveTiles() { return s_live_tiles; } -static void setLiveTiles(bool on) { s_live_tiles = on; } -static bool s_trails = false; -static bool trailsOn() { return s_trails; } -static void setTrailsOn(bool on) { s_trails = on; } -static bool s_vector = false; -static bool vectorOn() { return s_vector; } -static void setVectorOn(bool on) { s_vector = on; } static bool s_net_on = false; static void netBegin(const char*, const char*) { s_net_on = true; } static void netJoin(const char*, const char*) { s_net_on = true; } @@ -876,4 +828,47 @@ static const char* fetchError() { return s_fcode > 0 && s_fcode != 200 ? "HTTP e #error "ui-lvgl: no LVGL port for this board (see LvglPort.h)" #endif +// ── UI settings kept outside NodePrefs (nvs:: above: NVS, or the session) ── +// Settings > WiFi's switch: off keeps the radio off for everything (scan, map +// download). Kept with the credentials. +static int8_t s_wifi_allowed = -1; // read once (the status bar asks every second) +static bool wifiAllowed() { + if (s_wifi_allowed < 0) s_wifi_allowed = nvs::getBool("mc_wifi", "on", true); + return s_wifi_allowed; +} +static void setWifiAllowed(bool on) { s_wifi_allowed = on; nvs::putBool("mc_wifi", "on", on); } +// Map tools > Live tiles: missing tiles fetched over WiFi while the map is open. +static bool liveTiles() { return nvs::getBool("mc_wifi", "live", true); } +static void setLiveTiles(bool on) { nvs::putBool("mc_wifi", "live", on); } +// Map tools > Hiking trails: the Waymarked Trails overlay over the map. +static bool trailsOn() { return nvs::getBool("mc_ui", "trails", false); } +static void setTrailsOn(bool on) { nvs::putBool("mc_ui", "trails", on); } +// Map tools > Vector map (test). +static bool vectorOn() { return nvs::getBool("mc_ui", "vector", false); } +static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", on); } + +// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none. +static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); } +static void savePin(const char* pin) { nvs::putStr("mc_lock", "pin", pin); } + +// Accent colour (Settings > Display & power): an index into theme::ACCENTS. +static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); } +static void saveAccent(int idx) { nvs::putU8("mc_ui", "accent", idx); } +// Settings > Storage > Kept per conversation (an index into histstore::KEEP). +static int loadHistKeep() { return nvs::getI8("mc_ui", "hkeep", -1); } +static void saveHistKeep(int idx) { nvs::putI8("mc_ui", "hkeep", idx); } +// Settings > Storage > Live map tiles: index into mapview::LIVE_CAP_MB, -1 = default. +static int loadLiveCap() { return nvs::getI8("mc_ui", "ltcap", -1); } +static void saveLiveCap(int idx) { nvs::putI8("mc_ui", "ltcap", idx); } +// Settings > Display & power > Tap to wake: a touch turns the dark screen on +// (off: only the top button does). +static bool loadTapWake() { return nvs::getBool("mc_ui", "tapwake", true); } +static void saveTapWake(bool on) { nvs::putBool("mc_ui", "tapwake", on); } +// Messages: which sections are folded (bit per section). +static int loadChatFold() { return nvs::getU8("mc_ui", "chfold", 0); } +static void saveChatFold(int bits) { nvs::putU8("mc_ui", "chfold", bits); } +// Home's apps: their letters in the user's order, a hidden one in lower case. +static void loadHomeApps(char* out, size_t n) { nvs::getStr("mc_ui", "apps", out, n); } +static void saveHomeApps(const char* v) { nvs::putStr("mc_ui", "apps", v); } + } // namespace lvport diff --git a/examples/companion_radio/ui-lvgl/MapAreas.h b/examples/companion_radio/ui-lvgl/MapAreas.h index 14a19600..0a598c57 100644 --- a/examples/companion_radio/ui-lvgl/MapAreas.h +++ b/examples/companion_radio/ui-lvgl/MapAreas.h @@ -34,7 +34,6 @@ static int s_prev_z; static bool s_prev_follow; static int s_idx = -1; // area in the open popup static lv_obj_t* s_del_lbl = nullptr; -static uint32_t s_del_armed_ms = 0; static const int BAR_H = 84; static const int HANDLE = 26; @@ -69,11 +68,9 @@ static void boxWorld(const mapview::TileArea& b, double& x0, double& y0, double& } static void fmtDate(char* out, size_t n, uint32_t t, const NodePrefs* p) { - if (t < 1000000000UL) { out[0] = '\0'; return; } - time_t tt = (time_t)((int64_t)t + (int64_t)(p ? p->tz_offset_hours : 0) * 3600); - struct tm tm = *gmtime(&tt); - static const char* const MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - snprintf(out, n, "%d %s %d", tm.tm_mday, MON[tm.tm_mon], tm.tm_year + 1900); + struct tm tm; + if (!localTime(p, tm, t)) { out[0] = '\0'; return; } + snprintf(out, n, "%d %s %d", tm.tm_mday, MONTHS[tm.tm_mon], tm.tm_year + 1900); } static void onHandle(lv_event_t* e) { @@ -387,9 +384,7 @@ void UITask::mapAreasPopup() { listRow(g, m.name, sub, onArea, (void*)(intptr_t)i); } } else { - lv_obj_t* l = label(list, "No areas yet. Maps downloaded before this list are still on the card.", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + noteLabel(list, "No areas yet. Maps downloaded before this list are still on the card."); } lv_obj_t* add = lv_button_create(list); lv_obj_set_size(add, LV_PCT(100), 38); @@ -412,16 +407,13 @@ void UITask::mapAreaPopup(int idx) { snprintf(info, sizeof(info), "Zoom %d-%d, %lu tiles%s%s%s%s", m.box.zmin, m.box.zmax, (unsigned long)n, (m.flags & mapview::AreaStore::F_TRAILS) ? ", trails" : "", date[0] ? " - " : "", date, (m.flags & mapview::AreaStore::F_COMPLETE) ? "" : "\nUnfinished - Fill gaps completes it"); - lv_obj_t* l = label(panel, info, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); - lv_obj_t* r = toolRow(panel); + noteLabel(panel, info); + lv_obj_t* r = buttonBar(panel); actButton(r, LV_SYMBOL_EDIT, "Rename", A_RENAME); actButton(r, LV_SYMBOL_DOWNLOAD, "Fill", A_FILL); actButton(r, LV_SYMBOL_REFRESH, "Refresh", A_REFRESH); actButton(r, (m.flags & mapview::AreaStore::F_TRAILS) ? LV_SYMBOL_MINUS : LV_SYMBOL_PLUS, "Trails", A_TRAILS); s_del_lbl = actButton(r, LV_SYMBOL_TRASH, "Delete", A_DELETE); - s_del_armed_ms = 0; // Framed in the map above the sheet: the view is kept to go back to. lv_obj_update_layout(panel); @@ -492,11 +484,7 @@ void UITask::mapAreaAction(uint8_t act) { showToast("Fetching its trails"); return; case A_DELETE: - if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms - s_del_armed_ms = millis() | 1; - if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?"); - return; - } + if (!tapConfirmed(s_del_lbl, "Delete?")) return; if (mapview::s_areas.deleting()) { showToast("Still deleting - try again shortly"); return; } if (mapview::s_dl.active() && mapview::s_areas.find(mapview::s_dl.area()) == s_idx) mapview::s_dl.cancel(); if (s_show == s_idx) s_show = -1; diff --git a/examples/companion_radio/ui-lvgl/MapRegions.h b/examples/companion_radio/ui-lvgl/MapRegions.h index 04100007..99c1d5eb 100644 --- a/examples/companion_radio/ui-lvgl/MapRegions.h +++ b/examples/companion_radio/ui-lvgl/MapRegions.h @@ -8,7 +8,6 @@ namespace regions { static int s_sel = -1; // the pack a popup is about -static uint32_t s_del_armed_ms = 0; static lv_obj_t* s_del_lbl = nullptr; enum : uint8_t { A_SHOW, A_DELETE }; @@ -17,19 +16,8 @@ static void onDeleted(lv_event_t* e) { if (lv_event_get_target(e) == s_del_lbl) static void onPack(lv_event_t* e) { s_ui->mapRegionPopup((int)(intptr_t)lv_event_get_user_data(e)); } static void onAct(lv_event_t* e) { s_ui->mapRegionAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } -static lv_obj_t* actButton(lv_obj_t* row, const char* text, uint8_t act, bool accent) { - lv_obj_t* b = lv_button_create(row); - lv_obj_set_height(b, 36); - lv_obj_set_flex_grow(b, 1); - lv_obj_set_style_pad_hor(b, 4, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_add_event_cb(b, onAct, LV_EVENT_CLICKED, (void*)(uintptr_t)act); - lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT); - lv_obj_center(l); - return l; +static lv_obj_t* actButton(lv_obj_t* bar, const char* text, uint8_t act, bool accent) { + return lv_obj_get_child(barButton(bar, text, onAct, act, accent), 0); } static void fmtMB(char* out, size_t n, uint32_t bytes) { @@ -55,11 +43,8 @@ void UITask::mapRegionsPopup() { listRow(g, p.name[0] ? p.name : p.file, sub, onPack, (void*)(intptr_t)i); } } - lv_obj_t* l = label(list, np ? "Packs in /vmap on the SD card, made with tools/maps/osm_vector.py --pack." - : "No regions yet. Make a pack with tools/maps/osm_vector.py --pack and copy it to /vmap on the SD card.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + noteLabel(list, np ? "Packs in /vmap on the SD card, made with tools/maps/osm_vector.py --pack." + : "No regions yet. Make a pack with tools/maps/osm_vector.py --pack and copy it to /vmap on the SD card."); } void UITask::mapRegionPopup(int idx) { @@ -71,14 +56,11 @@ void UITask::mapRegionPopup(int idx) { char info[128], mb[16]; fmtMB(mb, sizeof(mb), p.size); snprintf(info, sizeof(info), "%s, zoom %d-18%s\n%s", mb, p.zmin, (p.flags & mapview::VectorPacks::F_CONTOURS) ? ", contours" : "", p.file); - lv_obj_t* l = label(panel, info, THEME_FONT_SMALL, theme::TEXT); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); - lv_obj_t* r = toolRow(panel); + noteLabel(panel, info, THEME_FONT_SMALL, theme::TEXT); + lv_obj_t* r = buttonBar(panel); actButton(r, UI_SYMBOL_MAP " Show", A_SHOW, true); s_del_lbl = actButton(r, LV_SYMBOL_TRASH " Delete", A_DELETE, false); lv_obj_add_event_cb(s_del_lbl, onDeleted, LV_EVENT_DELETE, NULL); - s_del_armed_ms = 0; } void UITask::mapRegionAction(uint8_t act) { @@ -100,11 +82,7 @@ void UITask::mapRegionAction(uint8_t act) { return; } case A_DELETE: { - if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms - s_del_armed_ms = millis() | 1; - if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?"); - return; - } + if (!tapConfirmed(s_del_lbl, "Delete?")) return; char path[80]; snprintf(path, sizeof(path), "%s/%s", mapview::VECTOR_ROOT, p.file); mapview::s_vpacks.closeOpen(); diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 29ec5fe8..2035d0c6 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -744,9 +744,7 @@ void UITask::mapDownloadPopup() { headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL); lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); - _dl_info = label(panel, "", THEME_FONT_SMALL, theme::TEXT); - lv_label_set_long_mode(_dl_info, LV_LABEL_LONG_WRAP); - lv_obj_set_width(_dl_info, LV_PCT(100)); + _dl_info = noteLabel(panel, "", THEME_FONT_SMALL, theme::TEXT); lv_obj_set_style_text_line_space(_dl_info, 2, 0); _dl_bar = lv_bar_create(panel); lv_obj_set_size(_dl_bar, LV_PCT(100), 6); @@ -758,21 +756,8 @@ void UITask::mapDownloadPopup() { lv_obj_set_width(_dl_err, LV_PCT(100)); lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN); - lv_obj_t* acts = lv_obj_create(panel); - styleSurface(acts, theme::BG); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(acts, theme::GAP, 0); - lv_obj_t* go = lv_button_create(acts); - lv_obj_set_height(go, 38); - lv_obj_set_flex_grow(go, 1); - lv_obj_set_style_shadow_width(go, 0, 0); - lv_obj_set_style_radius(go, theme::RADIUS, 0); - lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0); - lv_obj_add_event_cb(go, onDlStart, LV_EVENT_CLICKED, NULL); - _dl_start_lbl = label(go, "", THEME_FONT_SMALL, theme::TEXT); - lv_obj_center(_dl_start_lbl); + lv_obj_t* go = barButton(buttonBar(panel), "", onDlStart, 0, true); + _dl_start_lbl = lv_obj_get_child(go, 0); stylePrimary(go); refreshDownloadPopup(); diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index ce3b48bb..211aab2e 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -496,7 +496,6 @@ void UITask::navClosePopup() { if (_nav_overlay) lv_obj_delete_async(_nav_overlay); // may be closing from its own button _nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = nullptr; _nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr; - _nav_del_armed_ms = 0; } static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right) { @@ -543,10 +542,7 @@ void UITask::navTargetsPopup() { if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, 52); } if (wp.count() == 0) { - lv_obj_t* l = label(list, "None yet. Hold the map to drop one, or tap the pin to mark where you are.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + noteLabel(list, "None yet. Hold the map to drop one, or tap the pin to mark where you are."); } TrailStore& ts = _core->trail.store(); if (!ts.empty()) { @@ -598,12 +594,7 @@ void UITask::navWaypointMenu(int idx) { lv_obj_t* panel = navPopupPanel(w.label[0] ? w.label : "(unnamed)", false); placeCard(panel, w.lat_1e6, w.lon_1e6); - lv_obj_t* acts = lv_obj_create(panel); - styleSurface(acts, theme::BG); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(acts, theme::GAP, 0); + lv_obj_t* acts = buttonBar(panel); struct { const char* text; uint8_t act; bool accent; } btns[] = { { UI_SYMBOL_COMPASS " Go", navmap::WP_NAV, true }, { LV_SYMBOL_EDIT " Name", navmap::WP_RENAME, false }, @@ -611,17 +602,8 @@ void UITask::navWaypointMenu(int idx) { { LV_SYMBOL_TRASH, navmap::WP_DELETE, false }, }; for (auto& b : btns) { - lv_obj_t* bt = lv_button_create(acts); - lv_obj_set_height(bt, 40); - lv_obj_set_flex_grow(bt, 1); - lv_obj_set_style_pad_hor(bt, 4, 0); - lv_obj_set_style_radius(bt, theme::RADIUS, 0); - lv_obj_set_style_shadow_width(bt, 0, 0); - lv_obj_set_style_bg_color(bt, lv_color_hex(b.accent ? theme::ACCENT_DIM : theme::SURFACE), 0); - lv_obj_add_event_cb(bt, onNavWpAction, LV_EVENT_CLICKED, (void*)(uintptr_t)b.act); - lv_obj_t* l = label(bt, b.text, THEME_FONT_SMALL, theme::TEXT); - lv_obj_center(l); - if (b.act == navmap::WP_DELETE) _nav_del_lbl = l; + lv_obj_t* bt = barButton(acts, b.text, onNavWpAction, b.act, b.accent); + if (b.act == navmap::WP_DELETE) _nav_del_lbl = lv_obj_get_child(bt, 0); } } @@ -643,11 +625,7 @@ void UITask::navWaypointAction(uint8_t act) { break; } case navmap::WP_DELETE: - if (!_nav_del_armed_ms || millis() - _nav_del_armed_ms > 3000) { // second tap within 3 s confirms - _nav_del_armed_ms = millis() | 1; - if (_nav_del_lbl) lv_label_set_text(_nav_del_lbl, "Delete?"); - break; - } + if (!tapConfirmed(_nav_del_lbl, "Delete?")) break; _core->waypoints.remove(i); navClosePopup(); showToast("Waypoint deleted"); @@ -805,15 +783,13 @@ static const int ST_MAX = 40; static char s_st_names[ST_MAX][28]; // newest first static int s_st_n = 0; static int s_st_sel = -2; // the one the popup is about; -1 = the internal slot -static uint32_t s_st_del_armed_ms = 0; static lv_obj_t* s_st_del_lbl = nullptr; // "trail-20260926-1405.trl" -> "26 Sep 2026 14:05" (else the name). static void trailTitle(const char* name, char* out, size_t n) { - static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; int y, mo, d, h, mi; if (sscanf(name, "trail-%4d%2d%2d-%2d%2d", &y, &mo, &d, &h, &mi) == 5 && mo >= 1 && mo <= 12) - snprintf(out, n, "%d %s %d %02d:%02d", d, MON[mo - 1], y, h, mi); + snprintf(out, n, "%d %s %d %02d:%02d", d, MONTHS[mo - 1], y, h, mi); else snprintf(out, n, "%s", name); } @@ -871,29 +847,9 @@ static void onNavShareTarget(lv_event_t* e) { s_ui->navSetShareTarget(choiceSelected((lv_obj_t*)lv_event_get_target(e))); } -static lv_obj_t* toolRow(lv_obj_t* parent) { - lv_obj_t* r = lv_obj_create(parent); - styleSurface(r, theme::BG); - lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(r, theme::GAP, 0); - return r; -} - -static lv_obj_t* toolButton(lv_obj_t* row, const char* text, uint8_t act, bool accent) { - lv_obj_t* b = lv_button_create(row); - lv_obj_set_height(b, 36); - lv_obj_set_flex_grow(b, 1); - lv_obj_set_style_pad_hor(b, 4, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_add_event_cb(b, onNavTool, LV_EVENT_CLICKED, (void*)(uintptr_t)act); - lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT); - lv_obj_center(l); - return l; +// A map tool's button (UITask.cpp's barButton); returns its label. +static lv_obj_t* toolButton(lv_obj_t* bar, const char* text, uint8_t act, bool accent) { + return lv_obj_get_child(barButton(bar, text, onNavTool, act, accent), 0); } void UITask::navToolsPopup() { @@ -902,11 +858,11 @@ void UITask::navToolsPopup() { sectionTitle(list, "TRAIL"); _nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); - lv_obj_t* r = toolRow(list); + lv_obj_t* r = buttonBar(list); _nav_trail_btn = toolButton(r, "", navmap::TL_TRAIL_TOGGLE, true); toolButton(r, LV_SYMBOL_SAVE " Save", navmap::TL_TRAIL_SAVE, false); toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_TRAIL_LOAD, false); - r = toolRow(list); + r = buttonBar(list); _nav_tb_btn = toolButton(r, "", navmap::TL_TRACKBACK, false); toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_TRAIL_GPX, false); _nav_reset_lbl = toolButton(r, LV_SYMBOL_TRASH " Reset", navmap::TL_TRAIL_RESET, false); @@ -914,9 +870,7 @@ void UITask::navToolsPopup() { (void*)(uintptr_t)navmap::TL_OPT_TRAIL); sectionTitle(list, "LIVE SHARE"); - _nav_share_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); - lv_label_set_long_mode(_nav_share_lbl, LV_LABEL_LONG_WRAP); - lv_obj_set_width(_nav_share_lbl, LV_PCT(100)); + _nav_share_lbl = noteLabel(list, "", THEME_FONT_SMALL, theme::TEXT); // Target: channels (as named -- "#" marks a hashtag channel), then // favourite contacts (starred, so the two stay apart). char opts[MAX_GROUP_CHANNELS * 24 + 16 * 40]; @@ -943,7 +897,7 @@ void UITask::navToolsPopup() { favs++; } choiceRow(group(list, nullptr), "Send to", nullptr, o ? opts : "(no channels)", sel, onNavShareTarget); - r = toolRow(list); + r = buttonBar(list); _nav_share_btn = toolButton(r, "", navmap::TL_SHARE_TOGGLE, true); toolButton(r, LV_SYMBOL_UPLOAD " Send once", navmap::TL_SHARE_ONCE, false); @@ -978,7 +932,6 @@ void UITask::navToolsPopup() { snprintf(vr, sizeof(vr), nv ? "%d on the card" : "None - packs go in /vmap", nv); listRow(g, "Vector regions", vr, onNavTool, (void*)(uintptr_t)navmap::TL_REGIONS); - _nav_reset_armed_ms = 0; refreshNavTools(); } @@ -995,10 +948,6 @@ void UITask::refreshNavTools() { else lv_label_set_text_fmt(_nav_trail_lbl, "%s - %s, %s, %d points", state, dist, dur, ts.count()); lv_label_set_text(_nav_trail_btn, ts.isActive() ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Record"); lv_label_set_text(_nav_tb_btn, navmap::s_tb.active() ? LV_SYMBOL_STOP " Stop back" : LV_SYMBOL_LOOP " Track back"); - if (_nav_reset_armed_ms && millis() - _nav_reset_armed_ms > 3000) { - _nav_reset_armed_ms = 0; - lv_label_set_text(_nav_reset_lbl, LV_SYMBOL_TRASH " Reset"); - } if (_prefs->loc_share_enabled) { char left[12]; @@ -1024,11 +973,10 @@ void UITask::navSetShareTarget(int sel) { static void trailFilePath(char* path, size_t n, const char* ext, const NodePrefs* p) { char stem[40]; uint32_t now = rtc_clock.getCurrentTime(); - if (now > 1000000000UL) { - time_t t = (time_t)((int64_t)now + (int64_t)(p ? p->tz_offset_hours : 0) * 3600); - struct tm* ti = gmtime(&t); - snprintf(stem, sizeof(stem), "%s/trail-%04d%02d%02d-%02d%02d", navmap::TRAILS_DIR, ti->tm_year + 1900, - ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min); + struct tm ti; + if (localTime(p, ti, now)) { + snprintf(stem, sizeof(stem), "%s/trail-%04d%02d%02d-%02d%02d", navmap::TRAILS_DIR, ti.tm_year + 1900, + ti.tm_mon + 1, ti.tm_mday, ti.tm_hour, ti.tm_min); } else { snprintf(stem, sizeof(stem), "%s/trail-%lu", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000)); } @@ -1090,7 +1038,6 @@ void UITask::savedTrailsPopup() { void UITask::savedTrailPopup(int idx) { if (idx >= navmap::s_st_n) return; navmap::s_st_sel = idx; - navmap::s_st_del_armed_ms = 0; char title[32]; if (idx < 0) snprintf(title, sizeof(title), "Saved on the device"); else navmap::trailTitle(navmap::s_st_names[idx], title, sizeof(title)); @@ -1113,7 +1060,7 @@ void UITask::savedTrailPopup(int idx) { label(panel, info, THEME_FONT_BODY, theme::TEXT); if (!_core->trail.store().empty()) label(panel, "Loading replaces the trail on the map.", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_obj_t* r = toolRow(panel); + lv_obj_t* r = buttonBar(panel); lv_obj_t* lb = toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_ST_LOAD, true); stylePrimary(lv_obj_get_parent(lb)); if (idx >= 0) toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_ST_GPX, false); @@ -1126,11 +1073,7 @@ void UITask::savedTrailAction(uint8_t act) { char path[64] = ""; if (idx >= 0) snprintf(path, sizeof(path), "%s/%s", navmap::TRAILS_DIR, navmap::s_st_names[idx]); if (act == navmap::TL_ST_DELETE) { - if (!navmap::s_st_del_armed_ms || millis() - navmap::s_st_del_armed_ms > 3000) { // second tap confirms - navmap::s_st_del_armed_ms = millis() | 1; - if (navmap::s_st_del_lbl) lv_label_set_text(navmap::s_st_del_lbl, "Delete?"); - return; - } + if (!tapConfirmed(navmap::s_st_del_lbl, "Delete?")) return; bool ok = idx >= 0 ? remove(path) == 0 : (the_mesh.getDataStore() && the_mesh.getDataStore()->removeFile(TrailEngine::TRAIL_FILE)); showToast(ok ? "Trail deleted" : "Delete failed"); @@ -1209,13 +1152,8 @@ void UITask::navToolAction(uint8_t act) { savedTrailAction(act); return; case navmap::TL_TRAIL_RESET: - if (!_nav_reset_armed_ms || millis() - _nav_reset_armed_ms > 3000) { // second tap within 3 s confirms - _nav_reset_armed_ms = millis() | 1; - lv_label_set_text(_nav_reset_lbl, "Reset?"); - return; - } + if (!tapConfirmed(_nav_reset_lbl, "Reset?")) return; tr.reset(); - _nav_reset_armed_ms = 0; showToast("Trail cleared"); rebuildMapMarkers(); layoutMap(); @@ -1312,7 +1250,7 @@ void UITask::navWaypointsPopup() { char title[32]; snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY); lv_obj_t* panel = navPopupPanel(title, true); - lv_obj_t* r = toolRow(panel); + lv_obj_t* r = buttonBar(panel); toolButton(r, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true); toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false); @@ -1332,10 +1270,7 @@ void UITask::navWaypointsPopup() { } } if (wp.count() == 0) { - lv_obj_t* l = label(list, "None yet. Add one here, type its coordinates, or hold the map on a spot.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + noteLabel(list, "None yet. Add one here, type its coordinates, or hold the map on a spot."); } } @@ -1398,7 +1333,7 @@ void UITask::navSpotPopup(int32_t lat, int32_t lon) { _nav_spot_lon = lon; lv_obj_t* panel = navPopupPanel("This spot", false); placeCard(panel, lat, lon); - lv_obj_t* r = toolRow(panel); + lv_obj_t* r = buttonBar(panel); toolButton(r, UI_SYMBOL_FLAG " Add waypoint", navmap::TL_SPOT_ADD, true); toolButton(r, UI_SYMBOL_COMPASS " Go here", navmap::TL_SPOT_GO, false); } diff --git a/examples/companion_radio/ui-lvgl/OtaScreen.h b/examples/companion_radio/ui-lvgl/OtaScreen.h index 3b65f754..fd1f6d43 100644 --- a/examples/companion_radio/ui-lvgl/OtaScreen.h +++ b/examples/companion_radio/ui-lvgl/OtaScreen.h @@ -186,9 +186,7 @@ void UITask::buildOta() { lv_obj_t* card = infoCard(body); infoRow(card, "Installed", FIRMWARE_VERSION); s_ota_latest = infoRow(card, "Latest release", ""); - _ota_status = label(body, "", THEME_FONT_BODY, theme::TEXT); - lv_label_set_long_mode(_ota_status, LV_LABEL_LONG_WRAP); - lv_obj_set_width(_ota_status, LV_PCT(100)); + _ota_status = noteLabel(body, "", THEME_FONT_BODY, theme::TEXT); _ota_bar = lv_bar_create(body); lv_obj_set_size(_ota_bar, LV_PCT(100), 10); lv_bar_set_range(_ota_bar, 0, 1000); diff --git a/examples/companion_radio/ui-lvgl/RadioExtras.h b/examples/companion_radio/ui-lvgl/RadioExtras.h index c6590454..e889d652 100644 --- a/examples/companion_radio/ui-lvgl/RadioExtras.h +++ b/examples/companion_radio/ui-lvgl/RadioExtras.h @@ -22,24 +22,6 @@ static uint8_t s_naming = N_PRESET; static int s_slot = -1; // user preset slot the popup is about static int s_scope = -1; // scope list index the popup is about -static lv_obj_t* redButton(lv_obj_t* panel, const char* text, lv_event_cb_t cb, uint8_t act) { - lv_obj_t* b = lv_button_create(panel); - lv_obj_set_size(b, LV_PCT(100), 40); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0); - lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)act); - lv_obj_center(label(b, text, THEME_FONT_BODY, theme::TEXT)); - return b; -} - -static lv_obj_t* note(lv_obj_t* parent, const char* text) { - lv_obj_t* t = label(parent, text, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); - return t; -} - } // namespace radiox static void onPresetRow(lv_event_t* e) { s_ui->presetMenu((int)(uintptr_t)lv_event_get_user_data(e)); } @@ -86,9 +68,9 @@ void UITask::presetMenu(int slot) { char info[64]; snprintf(info, sizeof(info), "%.3f MHz, SF%u, BW %g kHz, CR 4/%u", u.freq, (unsigned)u.sf, (double)u.bw, (unsigned)u.cr); label(panel, info, THEME_FONT_BODY, theme::TEXT); - lv_obj_t* acts = convview::actionRow(panel); - convview::actionButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true); - convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false); + lv_obj_t* acts = buttonBar(panel); + barButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true); + barButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false); } void UITask::presetAction(uint8_t act) { @@ -108,9 +90,8 @@ void UITask::presetAction(uint8_t act) { case P_DELETE: { char t[40]; snprintf(t, sizeof(t), "Delete %s?", u.name); - lv_obj_t* panel = navPopupPanel(t, false); - note(panel, "Removes the saved preset. The radio settings stay as they are."); - redButton(panel, LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO); + confirmBody(navPopupPanel(t, false), "Removes the saved preset. The radio settings stay as they are.", + LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO); break; } case P_DELETE_GO: @@ -199,13 +180,13 @@ void UITask::scopeMenu(int idx) { radiox::s_scope = idx; bool def = idx == sl.default_idx; lv_obj_t* panel = navPopupPanel(idx == 0 ? "* (no scope)" : sl.name((uint8_t)idx), false); - if (def) radiox::note(panel, "This is the default: direct messages and relaying use it."); + if (def) noteLabel(panel, "This is the default: direct messages and relaying use it."); if (idx == 0 && def) return; // nothing else to do with "*" - lv_obj_t* acts = convview::actionRow(panel); - if (!def) convview::actionButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true); + lv_obj_t* acts = buttonBar(panel); + if (!def) barButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true); if (idx > 0) { - convview::actionButton(acts, LV_SYMBOL_EDIT " Rename", onScopeAction, radiox::S_RENAME, false); - convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onScopeAction, radiox::S_DELETE, false); + barButton(acts, LV_SYMBOL_EDIT " Rename", onScopeAction, radiox::S_RENAME, false); + barButton(acts, LV_SYMBOL_TRASH " Delete", onScopeAction, radiox::S_DELETE, false); } } @@ -227,9 +208,9 @@ void UITask::scopeAction(uint8_t act) { if (s_scope == 0) break; char t[40]; snprintf(t, sizeof(t), "Delete %s?", sl.name((uint8_t)s_scope)); - lv_obj_t* panel = navPopupPanel(t, false); - note(panel, "Channels using it go back to no scope. If it is the default, * becomes the default."); - redButton(panel, LV_SYMBOL_TRASH " Delete", onScopeAction, S_DELETE_GO); + confirmBody(navPopupPanel(t, false), + "Channels using it go back to no scope. If it is the default, * becomes the default.", + LV_SYMBOL_TRASH " Delete", onScopeAction, S_DELETE_GO); break; } case S_DELETE_GO: diff --git a/examples/companion_radio/ui-lvgl/RepeaterScreen.h b/examples/companion_radio/ui-lvgl/RepeaterScreen.h index 35fbf298..9179ecdc 100644 --- a/examples/companion_radio/ui-lvgl/RepeaterScreen.h +++ b/examples/companion_radio/ui-lvgl/RepeaterScreen.h @@ -133,10 +133,7 @@ void UITask::repeaterScopesPopup() { if (sl.count == 0) { showToast("Set up scopes in Settings > Radio first"); return; } lv_obj_t* panel = navPopupPanel("Extra scopes", sl.count > 2); if (sl.count > 2) lv_obj_add_flag(panel, LV_OBJ_FLAG_SCROLLABLE); // up to 8 rows - lv_obj_t* t = label(panel, "Relayed besides the default scope.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); + noteLabel(panel, "Relayed besides the default scope."); for (uint8_t i = 0; i < sl.count; i++) { lv_obj_t* sw = switchRow(panel, sl.name((uint8_t)(i + 1)), (i + 1) == sl.default_idx ? "Default - always relayed" : nullptr, nullptr); diff --git a/examples/companion_radio/ui-lvgl/StorageScreen.h b/examples/companion_radio/ui-lvgl/StorageScreen.h index 2fd2017c..f3993a98 100644 --- a/examples/companion_radio/ui-lvgl/StorageScreen.h +++ b/examples/companion_radio/ui-lvgl/StorageScreen.h @@ -37,14 +37,11 @@ static lv_obj_t* s_sd_lbl = nullptr; static lv_obj_t* s_cat_val[C_COUNT]; static lv_obj_t* s_status = nullptr; static lv_obj_t* s_clear_lbl = nullptr; -static uint32_t s_clear_armed_ms = 0; static lv_obj_t* s_live_lbl = nullptr; // "Delete live map tiles" row label static lv_obj_t* s_live_info = nullptr; // count / size from the index -static uint32_t s_live_armed_ms = 0; static uint32_t s_shown_ms = 0; static lv_obj_t* s_fmt_overlay = nullptr; // Format card: the popup static lv_obj_t* s_fmt_go_lbl = nullptr; // its Format button's label -static bool s_fmt_armed = false; // Format tapped once static void fmtBytes(char* out, size_t n, uint64_t b) { if (b < 1024) snprintf(out, n, "%u B", (unsigned)b); @@ -264,9 +261,7 @@ void UITask::buildStorage() { lv_obj_t* body = newScreen("Storage", true); s_sd_bar = s_sd_lbl = s_status = s_clear_lbl = s_live_lbl = s_live_info = nullptr; s_fmt_overlay = s_fmt_go_lbl = nullptr; // went with the old screen - s_fmt_armed = false; memset(s_cat_val, 0, sizeof(s_cat_val)); - s_clear_armed_ms = 0; lv_obj_t* g = group(body, "SD CARD"); if (!lvport::mountStorage()) { @@ -310,7 +305,6 @@ void UITask::buildStorage() { choiceRow(g, "Keep up to", "Past it, the oldest go first", mapview::LIVE_CAP_OPTS, cap_idx, onLiveCap); s_live_lbl = actionRow(g, LV_SYMBOL_TRASH " Delete live map tiles", onLiveClear, NULL, theme::FAIL); groupNote(body, "Downloaded areas stay."); - s_live_armed_ms = 0; s_shown_ms = 0; } @@ -326,9 +320,7 @@ void UITask::pollStorage() { fmtBytes(t, sizeof(t), s_sd_total); snprintf(line, sizeof(line), "The card is %s but its FAT partition only %s. " "Format it as one FAT32 partition on a computer to use all of it.", c, t); - lv_obj_t* n = label(lv_obj_get_parent(s_sd_bar), line, THEME_FONT_SMALL, theme::ACCENT); - lv_label_set_long_mode(n, LV_LABEL_LONG_WRAP); - lv_obj_set_width(n, LV_PCT(100)); + noteLabel(lv_obj_get_parent(s_sd_bar), line, THEME_FONT_SMALL, theme::ACCENT); } } if (s_walking) stepWalk(12); @@ -347,14 +339,6 @@ void UITask::pollStorage() { else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b); lv_label_set_text(s_live_info, t); } - if (s_live_armed_ms && millis() - s_live_armed_ms > 4000) { - s_live_armed_ms = 0; - if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles"); - } - if (s_clear_armed_ms && millis() - s_clear_armed_ms > 4000) { // the confirm tap timed out - s_clear_armed_ms = 0; - if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history"); - } } void UITask::storageKeep(int idx) { @@ -367,16 +351,10 @@ void UITask::storageKeep(int idx) { void UITask::storageClearHistory() { using namespace storeview; - if (!s_clear_armed_ms) { // first tap: ask - s_clear_armed_ms = millis(); - if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Tap again to delete"); - return; - } - s_clear_armed_ms = 0; + if (!tapConfirmed(s_clear_lbl, LV_SYMBOL_TRASH " Tap again to delete")) return; s_archive.clearAll(); _core->history.clearAll(); _core->markAllRead(); - if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history"); showToast("Message history deleted"); startWalk(); } @@ -389,14 +367,8 @@ void UITask::storageLiveCap(int idx) { void UITask::storageClearLive() { using namespace storeview; - if (!s_live_armed_ms) { // first tap: ask - s_live_armed_ms = millis(); - if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete"); - return; - } - s_live_armed_ms = 0; + if (!tapConfirmed(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete")) return; mapview::s_live_cache.clearAll(); // a few files per loop (mapDownloadTick) - if (s_live_lbl) lv_label_set_text(s_live_lbl, LV_SYMBOL_TRASH " Delete live map tiles"); showToast("Deleting live map tiles"); } @@ -419,15 +391,11 @@ static lv_obj_t* fmtButton(lv_obj_t* row, const char* text, uint32_t bg, bool go void UITask::storageFormatAsk() { using namespace storeview; if (s_fmt_overlay) return; - s_fmt_armed = false; lv_obj_t* panel = popupOpen(screen(), POP_FIT, s_fmt_overlay); lv_obj_set_style_pad_row(panel, 8, 0); label(panel, "Format the SD card?", THEME_FONT_TITLE, theme::TEXT); - lv_obj_t* t = label(panel, "Everything on it is erased: map areas, live map tiles, message history, " - "trails and screenshots. The device restarts afterwards.", - THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); + noteLabel(panel, "Everything on it is erased: map areas, live map tiles, message history, " + "trails and screenshots. The device restarts afterwards."); lv_obj_t* row = flexBox(panel, LV_FLEX_FLOW_ROW); lv_obj_set_width(row, LV_PCT(100)); lv_obj_set_style_pad_column(row, theme::GAP, 0); @@ -442,11 +410,7 @@ void UITask::storageFormatTap(bool go) { s_fmt_overlay = s_fmt_go_lbl = nullptr; return; } - if (!s_fmt_armed) { // the second confirmation - s_fmt_armed = true; - if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Tap again"); - return; - } + if (!tapConfirmed(s_fmt_go_lbl, "Tap again")) return; // the second confirmation if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Formatting..."); lv_refr_now(NULL); // shown before the loop blocks stopWalk(); @@ -485,9 +449,7 @@ static void usbPanel(const char* title, const char* text, bool buttons) { lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); label(row, LV_SYMBOL_USB, THEME_FONT_TITLE, theme::ACCENT); label(row, title, THEME_FONT_TITLE, theme::TEXT); - lv_obj_t* t = label(panel, text, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); + noteLabel(panel, text); if (!buttons) return; lv_obj_t* br = flexBox(panel, LV_FLEX_FLOW_ROW); lv_obj_set_width(br, LV_PCT(100)); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index de0dedbc..5c34eb4f 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -132,6 +132,14 @@ static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font lv_obj_set_style_text_color(l, lv_color_hex(color), 0); return l; } +// A sentence across the parent's width, wrapping: a hint, a note, a status. +static lv_obj_t* noteLabel(lv_obj_t* parent, const char* text, const lv_font_t* font = THEME_FONT_SMALL, + uint32_t color = theme::TEXT_MUTED) { + lv_obj_t* l = label(parent, text, font, color); + lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); + lv_obj_set_width(l, LV_PCT(100)); + return l; +} // A list filling the rest of a flex column, scrolling on its own (the // scrollbar only while it moves). @@ -277,9 +285,7 @@ static lv_obj_t* infoNote(lv_obj_t* card, const char* key, const char* text) { lv_obj_set_flex_flow(r, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(r, 2, 0); label(r, key, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_obj_t* t = label(r, text, THEME_FONT_SMALL, theme::TEXT); - lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP); - lv_obj_set_width(t, LV_PCT(100)); + lv_obj_t* t = noteLabel(r, text, THEME_FONT_SMALL, theme::TEXT); return t; } @@ -322,9 +328,7 @@ static lv_obj_t* group(lv_obj_t* parent, const char* title) { } static lv_obj_t* groupNote(lv_obj_t* parent, const char* text) { - lv_obj_t* l = label(parent, text, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + lv_obj_t* l = noteLabel(parent, text); lv_obj_set_style_pad_hor(l, theme::PAD, 0); return l; } @@ -357,13 +361,9 @@ static lv_obj_t* groupText(lv_obj_t* row, const char* text, const char* hint, ui // after its flex layout, which then sits everything at the top. lv_obj_set_style_min_height(t, theme::ROW_H - 12, 0); lv_obj_set_flex_align(t, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); - lv_obj_t* l = label(t, text, THEME_FONT_BODY, col); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + lv_obj_t* l = noteLabel(t, text, THEME_FONT_BODY, col); if (hint) { - lv_obj_t* h = label(t, hint, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP); - lv_obj_set_width(h, LV_PCT(100)); + noteLabel(t, hint); } lv_obj_set_user_data(row, l); // rowTitle() return l; @@ -656,6 +656,77 @@ static void stylePrimary(lv_obj_t* b) { lv_obj_set_style_text_color(lv_obj_get_child(b, i), lv_color_hex(theme::BG), 0); } +// A row of equal buttons at the foot of a popup or screen: buttonBar(), then +// barButton() for each -- `accent` for the main action or one that's on +// (barButtonOn() flips it later). Returns the button; its label is child 0. +static lv_obj_t* buttonBar(lv_obj_t* parent) { + lv_obj_t* bar = lv_obj_create(parent); + styleSurface(bar, theme::BG); + lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(bar, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(bar, theme::GAP, 0); + return bar; +} +static void barButtonOn(lv_obj_t* b, bool on) { + lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0); +} +static lv_obj_t* barButton(lv_obj_t* bar, const char* text, lv_event_cb_t cb, uintptr_t user, bool accent = false) { + lv_obj_t* b = lv_button_create(bar); + lv_obj_set_height(b, 40); + lv_obj_set_flex_grow(b, 1); + lv_obj_set_style_pad_hor(b, 4, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); + lv_obj_set_style_shadow_width(b, 0, 0); + barButtonOn(b, accent); + lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); + lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)user); + lv_obj_center(label(b, text, THEME_FONT_SMALL, theme::TEXT)); + return b; +} + +// Asking before something that can't be undone: under navPopupPanel("...?"), +// what happens in a sentence and the red button that does it. +static lv_obj_t* confirmBody(lv_obj_t* panel, const char* text, const char* button, lv_event_cb_t cb, uintptr_t user) { + noteLabel(panel, text); + lv_obj_t* b = lv_button_create(panel); + lv_obj_set_size(b, LV_PCT(100), 40); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(theme::FAIL), 0); + lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)user); + lv_obj_center(label(b, button, THEME_FONT_BODY, theme::TEXT)); + return b; +} + +// A button that asks by itself before it destroys: the first tap puts `ask` +// on its label, a second tap within 3 s is the yes. Left alone, the label +// comes back and the next tap asks again. One button asks at a time. +static lv_obj_t* s_ask_lbl = nullptr; +static char s_ask_text[64]; +static lv_timer_t* s_ask_timer = nullptr; +static void onAskDeleted(lv_event_t* e); +static void askReset() { + if (s_ask_timer) { lv_timer_delete(s_ask_timer); s_ask_timer = nullptr; } + if (!s_ask_lbl) return; + lv_obj_remove_event_cb(s_ask_lbl, onAskDeleted); + lv_label_set_text(s_ask_lbl, s_ask_text); + s_ask_lbl = nullptr; +} +static void onAskDeleted(lv_event_t* e) { (void)e; s_ask_lbl = nullptr; askReset(); } +static bool tapConfirmed(lv_obj_t* lbl, const char* ask) { + bool yes = lbl && lbl == s_ask_lbl; + askReset(); + if (yes || !lbl) return yes; + s_ask_lbl = lbl; + snprintf(s_ask_text, sizeof(s_ask_text), "%s", lv_label_get_text(lbl)); + lv_label_set_text(lbl, ask); + lv_obj_add_event_cb(lbl, onAskDeleted, LV_EVENT_DELETE, NULL); + s_ask_timer = lv_timer_create([](lv_timer_t*) { s_ask_timer = nullptr; askReset(); }, 3000, NULL); + lv_timer_set_repeat_count(s_ask_timer, 1); + return false; +} + // "@[nick]" -> "@nick" in place, for one-line text (previews, quotes) where // the bubble's highlight doesn't reach. static void plainMentions(char* t) { @@ -698,14 +769,12 @@ static void tileBadge(lv_obj_t* tile, int n) { lv_obj_align(lv_obj_get_child(tile, -1), LV_ALIGN_TOP_RIGHT, -2, 2); } -// Local time (NodePrefs::tz_offset_hours); false before the clock is set. -static bool localTime(const NodePrefs* p, struct tm& out) { - uint32_t now = rtc_clock.getCurrentTime(); - if (now <= 1000000000UL) return false; - time_t t = (time_t)((int64_t)now + (int64_t)(p ? p->tz_offset_hours : 0) * 3600); - out = *gmtime(&t); - return true; +// Local time (NodePrefs::tz_offset_hours), now or at `utc`; false before the clock is set. +static bool localTime(const NodePrefs* p, struct tm& out, uint32_t utc = rtc_clock.getCurrentTime()) { + return localTm(utc, p ? p->tz_offset_hours : 0, out); } +static const char* const MONTHS[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; // "14:05", or "2:05" (+ " PM" with suffix) with Settings > 12-hour clock; // with `seconds`, ":09" follows unless Settings > Clock seconds is off. static void fmtClock(char* b, size_t n, const struct tm& ti, const NodePrefs* p, bool suffix, bool seconds = false) { @@ -721,9 +790,7 @@ static void fmtClock(char* b, size_t n, const struct tm& ti, const NodePrefs* p, // "Thu 25 Sep 2026". static void fmtDate(char* b, size_t n, const struct tm& ti) { static const char* DOW[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; - static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - snprintf(b, n, "%s %d %s %d", DOW[ti.tm_wday], ti.tm_mday, MON[ti.tm_mon], ti.tm_year + 1900); + snprintf(b, n, "%s %d %s %d", DOW[ti.tm_wday], ti.tm_mday, MONTHS[ti.tm_mon], ti.tm_year + 1900); } // Settings changed here reach flash once their screen is left (as on the L1: @@ -777,13 +844,11 @@ static const NodePrefs* s_prefs = nullptr; // for the free helpers below; set static bool clockBehind(uint32_t now, uint32_t ts) { return ts > 1000000000UL && ts > now + 120; } static void fmtMsgAge(char* b, size_t n, uint32_t now, uint32_t ts, const NodePrefs* p) { if (!clockBehind(now, ts)) { geo::fmtAgeShort(b, (int)n, now, ts ? ts : now); return; } - static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - time_t t = (time_t)((int64_t)ts + (int64_t)(p ? p->tz_offset_hours : 0) * 3600); - struct tm ti = *gmtime(&t); + struct tm ti; + localTime(p, ti, ts); // set: clockBehind() says ts is char clk[12]; fmtClock(clk, sizeof(clk), ti, p, true); - snprintf(b, n, "%d %s %s", ti.tm_mday, MON[ti.tm_mon], clk); + snprintf(b, n, "%d %s %s", ti.tm_mday, MONTHS[ti.tm_mon], clk); } static void contactName(const uint8_t* prefix, char* out, size_t n) { @@ -1056,7 +1121,7 @@ uint32_t UITask::idleMillis(uint32_t lv_next) { if (_buzzer.isPlaying()) return 1; #endif if (isClientConnected()) return 2; - if (_asleep) return 20; // the buttons and tap to wake are polled every 50 ms + if (_asleep) return 50; // as often as the buttons and tap to wake are polled return lv_next < 10 ? lv_next : 10; // LVGL's next timer: a refresh, an animation, input } @@ -2204,11 +2269,9 @@ void UITask::refreshScanPopup() { lv_obj_align(label(row, right, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, -theme::PAD, 0); } if (n == 0) { - lv_obj_t* l = label(_scan_list, _scanning ? "Repeaters and rooms in range will answer." - : "Nobody answered. Try again later or move.", - THEME_FONT_BODY, theme::TEXT_MUTED); - lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); - lv_obj_set_width(l, LV_PCT(100)); + lv_obj_t* l = noteLabel(_scan_list, _scanning ? "Repeaters and rooms in range will answer." + : "Nobody answered. Try again later or move.", + THEME_FONT_BODY, theme::TEXT_MUTED); lv_obj_set_style_pad_top(l, 8, 0); } } @@ -2220,7 +2283,6 @@ void UITask::openScanNode(int row) { _node_from_map = false; _scanning = false; _pinging = false; - _delete_armed_ms = 0; _screen = SCR_NODE; buildNode(); } @@ -2292,26 +2354,10 @@ void UITask::openNode(int row) { _node_from_scan = false; _node_from_map = false; _pinging = false; - _delete_armed_ms = 0; _screen = SCR_NODE; buildNode(); } -static lv_obj_t* actionButton(lv_obj_t* parent, const char* text, uint8_t action, bool accent) { - lv_obj_t* b = lv_button_create(parent); - lv_obj_set_height(b, 40); - lv_obj_set_flex_grow(b, 1); - lv_obj_set_style_pad_hor(b, 4, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0); - lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_add_event_cb(b, onNodeAction, LV_EVENT_CLICKED, (void*)(uintptr_t)action); - lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT); - lv_obj_center(l); - return l; -} - void UITask::buildNode() { const NearbyModel::Entry& e = s_node; lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true); @@ -2327,12 +2373,7 @@ void UITask::buildNode() { s_nd_id = infoRow(_node_info, "ID", ""); _node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT); - lv_obj_t* acts = lv_obj_create(body); - styleSurface(acts, theme::BG); - lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(acts, theme::GAP, 0); + lv_obj_t* acts = buttonBar(body); bool contact = e.contact_idx >= 0; bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM); bool pos = e.lat_e6 != 0 || e.lon_e6 != 0; @@ -2350,8 +2391,8 @@ void UITask::buildNode() { for (int i = 0; i < n; i++) { // five or more: icons only, so every button fits one row char t[24]; snprintf(t, sizeof(t), "%s%s", list[i].icon, n >= 5 ? "" : list[i].text); - lv_obj_t* l = actionButton(acts, t, list[i].action, list[i].accent); - if (list[i].action == NODE_DELETE) _node_delete_lbl = l; + lv_obj_t* b = barButton(acts, t, onNodeAction, list[i].action, list[i].accent); + if (list[i].action == NODE_DELETE) _node_delete_lbl = lv_obj_get_child(b, 0); } refreshNode(); @@ -2420,10 +2461,6 @@ void UITask::refreshNode() { _pinging = false; } } - if (_node_delete_lbl && _delete_armed_ms && millis() - _delete_armed_ms > 3000) { - _delete_armed_ms = 0; - lv_label_set_text(_node_delete_lbl, LV_SYMBOL_TRASH); - } } void UITask::nodeAction(uint8_t action) { @@ -2488,10 +2525,7 @@ void UITask::nodeAction(uint8_t action) { pinPopup(false, 0, e.pub_key); break; case NODE_DELETE: - if (!_delete_armed_ms) { // destructive: second tap within 3 s confirms - _delete_armed_ms = millis(); - if (_delete_armed_ms == 0) _delete_armed_ms = 1; - lv_label_set_text(_node_delete_lbl, LV_SYMBOL_TRASH "?"); // fits an icon-only button + if (!tapConfirmed(_node_delete_lbl, LV_SYMBOL_TRASH "?")) { // "?" fits an icon-only button showToast("Tap again to delete the contact", 2500); break; } @@ -3121,17 +3155,13 @@ void UITask::pruneContacts() { showToast(_prefs && _prefs->contact_expiry_idx == 0 ? "Contact expiry is off" : "No inactive contacts"); return; } - if (!_prune_armed_ms || millis() - _prune_armed_ms > 3000) { - _prune_armed_ms = millis() | 1; - if (_prune_lbl) lv_label_set_text_fmt(_prune_lbl, LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s"); - return; - } - _prune_armed_ms = 0; + char ask[40]; + snprintf(ask, sizeof(ask), LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s"); + if (!tapConfirmed(_prune_lbl, ask)) return; int removed = the_mesh.pruneStaleContacts(); char t[40]; snprintf(t, sizeof(t), "Removed %d contact%s", removed, removed == 1 ? "" : "s"); showToast(t); - if (_prune_lbl) lv_label_set_text(_prune_lbl, LV_SYMBOL_TRASH " Remove inactive contacts now"); } void UITask::applyDisplayPrefs() { @@ -3171,7 +3201,6 @@ void UITask::buildSchemaSettings() { : s_nav_section == settings::SEC_LIVE_SHARE ? "Share options" : "Alert options"; lv_obj_t* body = newScreen(title, true); _prune_lbl = nullptr; - _prune_armed_ms = 0; switch (_settings_page) { case PG_KEYBOARD: buildKeyboardPage(body); return; case PG_ABOUT: buildAboutPage(body); return; diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index be4accfa..826f76ff 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -455,7 +455,6 @@ private: lv_obj_t* _ota_btn_lbl = nullptr; uint32_t _map_left_ms = 0; // when the map was left, for dropping live tiles' WiFi bool _admin_from_pick = false; // Admin opened from its Home tile (back returns there) - uint32_t _prune_armed_ms = 0; uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown uint32_t _next_batt_ms = 0; lv_obj_t* _prune_lbl = nullptr; @@ -508,7 +507,6 @@ private: uint32_t _next_nearby_ms = 0; bool _pinging = false; uint32_t _ping_started_ms = 0; - uint32_t _delete_armed_ms = 0; lv_obj_t* _nearby_list = nullptr; lv_obj_t* _nearby_status = nullptr; lv_obj_t* _nearby_sort_lbl = nullptr; @@ -553,7 +551,6 @@ private: lv_obj_t* _nav_ta = nullptr; lv_obj_t* _nav_kb = nullptr; lv_obj_t* _nav_del_lbl = nullptr; - uint32_t _nav_del_armed_ms = 0; int _nav_wp = -1; // waypoint the menu / rename is about lv_obj_t* _nav_rec = nullptr; // "REC 1.2 km LIVE 58m" pill lv_obj_t* _nav_avg_pill = nullptr; // GPS averaging progress (tap: cancel) @@ -563,7 +560,6 @@ private: lv_obj_t* _nav_share_lbl = nullptr; lv_obj_t* _nav_share_btn = nullptr; lv_obj_t* _nav_tb_btn = nullptr; - uint32_t _nav_reset_armed_ms = 0; int32_t _nav_spot_lat = 0, _nav_spot_lon = 0; // the spot a long-press picked char _share_text[96] = ""; // waiting for a conversation to be picked (shareToMessage) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6fbbd416..1b7613d2 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -807,10 +807,9 @@ public: display.drawTextCentered(display.width() / 2, mid_y + step, "Enable GPS or"); display.drawTextCentered(display.width() / 2, mid_y + step * 2, "connect app"); } else { - int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; - unix_ts += (int32_t)tz * 3600; - time_t t = (time_t)unix_ts; - struct tm* ti = gmtime(&t); + struct tm lt; + localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, lt); + struct tm* ti = < char buf[24]; display.setColor(DisplayDriver::LIGHT); @@ -954,10 +953,9 @@ public: if (unix_ts < 1000000000UL) { display.drawTextCentered(display.width() / 2, display.height() / 2 - step, "No time sync"); } else { - int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; - unix_ts += (int32_t)tz * 3600; - time_t t = (time_t)unix_ts; - struct tm* ti = gmtime(&t); + struct tm lt; + localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, lt); + struct tm* ti = < char buf[12]; bool h12 = _node_prefs && _node_prefs->clock_12h; int date_y = drawClockTime(display, 0, ti, h12, /*show_sec*/false); diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.h b/variants/wio-tracker-l2/WioTrackerL2Board.h index 21b745bb..f2eeb16f 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.h +++ b/variants/wio-tracker-l2/WioTrackerL2Board.h @@ -68,21 +68,9 @@ public: expWritePin(EXP_PIN_GNSS_EN, LOW); } } - // microSD rail (unmount first) - void setSdPower(bool on) { expWritePin(EXP_PIN_TF_EN, on); } - // Grove expansion port rail (nothing on-board depends on it) - void setGrovePower(bool on) { expWritePin(EXP_PIN_GROVE_EN, on); } bool gnssPowered() const { return (out_shadow[EXP_PIN_GNSS_EN >> 3] >> (EXP_PIN_GNSS_EN & 7)) & 1; } - // hardware reset pulse to the L76K GNSS (active HIGH on this board); the - // module keeps almanac/ephemeris across it, so this is a warm restart - void gnssReset() { - expWritePin(EXP_PIN_GNSS_RST, HIGH); - delay(10); - expWritePin(EXP_PIN_GNSS_RST, LOW); - } - // WAKE button on expander P00: pressed = level differs from boot baseline bool readWakeButton(); @@ -93,9 +81,6 @@ public: // VBUS presence via the AW35615 USB-C controller (I2C 0x22) bool isExternalPowered() override; - int awRead(uint8_t reg); // AW35615 register, -1 on error - - bool expanderOK() const { return expander_ok; } private: uint8_t out_shadow[2] = { 0xFF, 0xFF }; // TCA9535 output regs default high @@ -107,6 +92,15 @@ private: uint32_t batt_read_ms = 0; // when it was taken (reads closer together reuse it) int expReadInputs(); // 16-bit input register pair, -1 on error + int awRead(uint8_t reg); // AW35615 register, -1 on error + + // hardware reset pulse to the L76K GNSS (active HIGH on this board); the + // module keeps almanac/ephemeris across it, so this is a warm restart + void gnssReset() { + expWritePin(EXP_PIN_GNSS_RST, HIGH); + delay(10); + expWritePin(EXP_PIN_GNSS_RST, LOW); + } bool expWriteReg(uint8_t reg, uint8_t val); void expSetOutput(uint8_t pin, bool initial_level);