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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-28 22:35:26 +02:00
co-authored by Claude Opus 5.5
parent 1def29335c
commit ece849db3b
25 changed files with 308 additions and 563 deletions
+10
View File
@@ -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. places to speed things up or save RAM, flash and battery.
First a list of findings to accept, then fixes in batches, each checked First a list of findings to accept, then fixes in batches, each checked
in the sim and on the device. 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 ## G. Core parity with L1 SOLO
+4 -5
View File
@@ -4,6 +4,7 @@
#include <ctime> #include <ctime>
#include <cstdint> #include <cstdint>
#include <helpers/SensorManager.h> #include <helpers/SensorManager.h>
#include "NodePrefs.h" // localTm()
#include <helpers/sensors/LPPDataHelpers.h> #include <helpers/sensors/LPPDataHelpers.h>
// Expands placeholders in tmpl into out (out_len bytes). // 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"); else strcpy(lb, "no GPS");
APPEND(lb, strlen(lb)); p += 5; APPEND(lb, strlen(lb)); p += 5;
} else if (strncmp(p, "{time}", 6) == 0) { } else if (strncmp(p, "{time}", 6) == 0) {
if (utc_ts > 1000000000UL) { struct tm ti;
uint32_t local_ts = utc_ts + (int32_t)tz_hours * 3600; if (localTm(utc_ts, tz_hours, ti)) {
time_t t = (time_t)local_ts;
struct tm* ti = gmtime(&t);
char tb[8]; 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)); APPEND(tb, strlen(tb));
} }
p += 6; p += 6;
+8
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <stdio.h> #include <stdio.h>
#include <time.h>
// Firmware's own boot-default radio params — used both as the companion's // 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- // 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); h = (int)(((int64_t)utc + (int64_t)tz_hours * 3600) / 3600 % 24);
return true; 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). // Settings > Sound > Quiet hours in force at `utc` (never with the clock unset).
static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) { static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) {
int h; int h;
@@ -27,12 +27,6 @@ static void applyApc() { the_mesh.applyApc(); } // (re)initialise Adaptive Pow
// ── Presets ───────────────────────────────────────────────────────────────── // ── Presets ─────────────────────────────────────────────────────────────────
// One list: built-ins 0..RADIO_PRESET_COUNT-1, then the non-empty user slots. // 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. // 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) { 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; if (idx < 0) return false;
+5 -22
View File
@@ -96,10 +96,8 @@ void UITask::buildAdminPick() {
} }
} }
if (!rows) { if (!rows) {
lv_obj_t* t = label(body, "No repeaters or room servers yet. They show up here once their advert is heard.", noteLabel(body, "No repeaters or room servers yet. They show up here once their advert is heard.",
THEME_FONT_BODY, theme::TEXT_MUTED); THEME_FONT_BODY, theme::TEXT_MUTED);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
} }
} }
@@ -219,18 +217,7 @@ void UITask::adminRow(int row) {
const char* prompt = admin::confirmPrompt(f); const char* prompt = admin::confirmPrompt(f);
if (prompt) { // takes the node out of service for a while: ask first if (prompt) { // takes the node out of service for a while: ask first
s_confirm = &f; s_confirm = &f;
lv_obj_t* panel = navPopupPanel(prompt, false); confirmBody(navPopupPanel(prompt, false), "It's offline until it comes back.", f.label, onAdminValue, V_CONFIRM);
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));
return; return;
} }
S.run(f); S.run(f);
@@ -337,9 +324,7 @@ void UITask::adminValuePopup() {
lv_obj_add_event_cb(s_choice, onAdminChoice, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(s_choice, onAdminChoice, LV_EVENT_VALUE_CHANGED, NULL);
} }
if (f->isRadio()) { if (f->isRadio()) {
lv_obj_t* note = label(panel, "A mismatch cuts the node off.", THEME_FONT_SMALL, theme::TEXT_MUTED); noteLabel(panel, "A mismatch cuts the node off.");
lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP);
lv_obj_set_width(note, LV_PCT(100));
} }
lv_obj_t* r = adminRowBox(panel); lv_obj_t* r = adminRowBox(panel);
adminPopupButton(r, LV_SYMBOL_OK " Save", V_SAVE, true); 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_flex_grow(box, 1);
lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_ACTIVE); 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); noteLabel(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));
lv_obj_t* r = adminRowBox(panel); lv_obj_t* r = adminRowBox(panel);
adminPopupButton(r, "OK", adminview::V_CLOSE, false); adminPopupButton(r, "OK", adminview::V_CLOSE, false);
} }
-11
View File
@@ -75,15 +75,4 @@ namespace anim {
lv_obj_set_style_translate_x(o, 0, 0); lv_obj_set_style_translate_x(o, 0, 0);
run(o, setOpa, LV_OPA_TRANSP, LV_OPA_COVER, ms); 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);
}
} }
@@ -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 int s_idx = -1; // channel the popup / form is about (-1: adding)
static uint8_t s_type = T_HASHTAG; static uint8_t s_type = T_HASHTAG;
static bool s_hex = false; 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_del_lbl = nullptr;
static lv_obj_t* s_fav_btn = nullptr; static lv_obj_t* s_fav_btn = nullptr;
// Add / Edit form // Add / Edit form
@@ -69,7 +68,6 @@ void UITask::channelMenu(int idx) {
ChannelDetails ch; ChannelDetails ch;
if (!chanctl::exists(idx) || !the_mesh.getChannel(idx, ch)) return; if (!chanctl::exists(idx) || !the_mesh.getChannel(idx, ch)) return;
s_idx = idx; s_idx = idx;
s_del_armed_ms = 0;
s_del_lbl = s_fav_btn = nullptr; s_del_lbl = s_fav_btn = nullptr;
char title[40]; char title[40];
snprintf(title, sizeof(title), "%s", ch.name); // as named: "#" marks a hashtag channel 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); (void*)(uintptr_t)C_SCOPE);
} }
lv_obj_t* acts = lv_obj_create(panel); lv_obj_t* acts = buttonBar(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);
bool unread = _core->history.chUnread(idx) > 0; bool unread = _core->history.chUnread(idx) > 0;
struct { const char* text; uint8_t act; } btns[] = { struct { const char* text; uint8_t act; } btns[] = {
{ UI_SYMBOL_STAR " Fav", A_FAV }, { UI_SYMBOL_STAR " Fav", A_FAV },
@@ -105,21 +98,10 @@ void UITask::channelMenu(int idx) {
}; };
for (auto& b : btns) { for (auto& b : btns) {
if (b.act == A_READ && !unread) continue; if (b.act == A_READ && !unread) continue;
lv_obj_t* bt = lv_button_create(acts); lv_obj_t* bt = barButton(acts, b.text, onChanAction, b.act, b.act == A_FAV && chanctl::favourite(_prefs, idx));
lv_obj_set_height(bt, 40); if (b.act == A_DELETE) s_del_lbl = lv_obj_get_child(bt, 0);
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;
if (b.act == A_FAV) s_fav_btn = bt; 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) { void UITask::channelSet(uint8_t which, int v) {
@@ -140,7 +122,7 @@ void UITask::channelAction(uint8_t act) {
bool on = !chanctl::favourite(_prefs, idx); bool on = !chanctl::favourite(_prefs, idx);
chanctl::setFavourite(_prefs, idx, on); chanctl::setFavourite(_prefs, idx, on);
prefsSave(); 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); showToast(on ? "Added to favourites" : "Removed from favourites", 1200);
if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star
break; break;
@@ -158,11 +140,7 @@ void UITask::channelAction(uint8_t act) {
pinPopup(true, (uint8_t)idx, nullptr); // DeviceScreen.h pinPopup(true, (uint8_t)idx, nullptr); // DeviceScreen.h
break; break;
case A_DELETE: case A_DELETE:
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // second tap within 3 s confirms if (!tapConfirmed(s_del_lbl, "Delete?")) break;
s_del_armed_ms = millis() | 1;
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
break;
}
chanctl::remove(idx); chanctl::remove(idx);
navClosePopup(); navClosePopup();
showToast("Channel deleted"); showToast("Channel deleted");
@@ -201,9 +179,7 @@ void UITask::buildChannelEdit() {
lv_obj_add_event_cb(s_types, onChanType, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(s_types, onChanType, LV_EVENT_VALUE_CHANGED, NULL);
} }
s_hint = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED); s_hint = noteLabel(body, "");
lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s_hint, LV_PCT(100));
s_name = chanField(body, "Name"); s_name = chanField(body, "Name");
lv_textarea_set_max_length(s_name, sizeof(((ChannelDetails*)0)->name) - 1); lv_textarea_set_max_length(s_name, sizeof(((ChannelDetails*)0)->name) - 1);
s_secret = chanField(body, "Passphrase"); 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_add_event_cb(save, onChanSave, LV_EVENT_CLICKED, NULL);
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT)); lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
stylePrimary(save); stylePrimary(save);
s_status = label(body, "", THEME_FONT_SMALL, theme::FAIL); s_status = noteLabel(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));
if (edit) { if (edit) {
ChannelDetails ch; ChannelDetails ch;
@@ -105,9 +105,7 @@ void UITask::buildCompass() {
s_deg = label(num, "", THEME_FONT_CLOCK, theme::TEXT); s_deg = label(num, "", THEME_FONT_CLOCK, theme::TEXT);
s_degsign = label(num, "", THEME_FONT_LARGE, theme::TEXT); s_degsign = label(num, "", THEME_FONT_LARGE, theme::TEXT);
s_card = label(col, "", THEME_FONT_LARGE, theme::ACCENT); s_card = label(col, "", THEME_FONT_LARGE, theme::ACCENT);
s_hint = label(col, "", THEME_FONT_SMALL, theme::TEXT_MUTED); s_hint = noteLabel(col, "");
lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(s_hint, LV_PCT(100));
lv_obj_set_style_text_align(s_hint, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_align(s_hint, LV_TEXT_ALIGN_CENTER, 0);
refreshCompass(); refreshCompass();
} }
@@ -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 lv_obj_t* s_fav_btn = nullptr;
static int s_msg = -1; // s_msg_meta index the message popup is about 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 } // namespace convview
@@ -172,13 +150,13 @@ void UITask::conversationMenu(const uint8_t* pub_key) {
lv_obj_set_width(st, LV_PCT(100)); lv_obj_set_width(st, LV_PCT(100));
} }
lv_obj_t* acts = actionRow(panel); lv_obj_t* acts = buttonBar(panel);
s_fav_btn = actionButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV, contactctl::favourite(ci)); s_fav_btn = barButton(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); if (!room && _core->dmUnread(ci.id.pub_key) > 0) barButton(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); barButton(acts, UI_SYMBOL_PIN, onConvAction, A_PIN, favslots::findContact(_prefs, ci.id.pub_key) >= 0);
if (room) { if (room) {
actionButton(acts, LV_SYMBOL_EDIT " Login", onConvAction, A_LOGIN, false); barButton(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); 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: { case A_FAV: {
bool on = !contactctl::favourite(ci); bool on = !contactctl::favourite(ci);
if (!contactctl::setFavourite(ci.id.pub_key, on)) break; 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); showToast(on ? "Added to favourites" : "Removed from favourites", 1200);
if (_screen == SCR_CHATS) { buildChats(); conversationMenu(s_key); } // star / filter if (_screen == SCR_CHATS) { buildChats(); conversationMenu(s_key); } // star / filter
break; break;
@@ -314,9 +292,8 @@ void UITask::messageMenu(int idx) {
char when[64], age[16]; char when[64], age[16];
uint32_t now = rtc_clock.getCurrentTime(); uint32_t now = rtc_clock.getCurrentTime();
geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now); geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now);
if (ts > 1000000000UL) { struct tm ti;
time_t t = (time_t)((int64_t)ts + (int64_t)(_prefs ? _prefs->tz_offset_hours : 0) * 3600); if (localTime(_prefs, ti, ts)) {
struct tm ti = *gmtime(&t);
char clk[12]; char clk[12];
fmtClock(clk, sizeof(clk), ti, _prefs, true); fmtClock(clk, sizeof(clk), ti, _prefs, true);
if (clockBehind(now, ts)) { // the clock isn't set yet: the date instead of an age 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; bool can_reply = !m.own && m.from[0] && _compose_ta;
if (!can_reply && m.loc < 0) return; if (!can_reply && m.loc < 0) return;
lv_obj_t* acts = actionRow(panel); lv_obj_t* acts = buttonBar(panel);
if (can_reply) actionButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false); if (can_reply) barButton(acts, LV_SYMBOL_EDIT " Reply", onMsgAction, M_REPLY, false);
if (m.loc >= 0) actionButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false); if (m.loc >= 0) barButton(acts, UI_SYMBOL_COMPASS " Set target", onMsgAction, M_TARGET, false);
} }
void UITask::messageAction(uint8_t act) { void UITask::messageAction(uint8_t act) {
@@ -147,19 +147,10 @@ void UITask::nodeNameDone(bool ok) {
} }
void UITask::powerPopup(bool restart) { void UITask::powerPopup(bool restart) {
lv_obj_t* panel = navPopupPanel(restart ? "Reboot?" : "Power off?", false); confirmBody(navPopupPanel(restart ? "Reboot?" : "Power off?", false),
lv_obj_t* t = label(panel, restart ? "Restarts the device. Messages and settings are kept." 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.", : "Turns the device off. Messages and settings are kept; no messages arrive while it is off.",
THEME_FONT_SMALL, theme::TEXT_MUTED); restart ? LV_SYMBOL_REFRESH " Reboot" : LV_SYMBOL_POWER " Power off", onPowerGo, restart ? 1 : 0);
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));
} }
// ── Lock screen ─────────────────────────────────────────────────────────────── // ── Lock screen ───────────────────────────────────────────────────────────────
@@ -503,9 +494,9 @@ void UITask::favHold(int slot) {
if (!contactctl::favName(_prefs, slot, name, sizeof(name))) { favPickPopup(slot); return; } if (!contactctl::favName(_prefs, slot, name, sizeof(name))) { favPickPopup(slot); return; }
devview::s_fav_slot = slot; devview::s_fav_slot = slot;
lv_obj_t* panel = navPopupPanel(name, false); lv_obj_t* panel = navPopupPanel(name, false);
lv_obj_t* acts = convview::actionRow(panel); lv_obj_t* acts = buttonBar(panel);
convview::actionButton(acts, LV_SYMBOL_EDIT " Change", onFavAction, devview::F_CHANGE, false); barButton(acts, LV_SYMBOL_EDIT " Change", onFavAction, devview::F_CHANGE, false);
convview::actionButton(acts, LV_SYMBOL_TRASH " Remove", onFavAction, devview::F_REMOVE, false); barButton(acts, LV_SYMBOL_TRASH " Remove", onFavAction, devview::F_REMOVE, false);
} }
void UITask::favAction(uint8_t act) { void UITask::favAction(uint8_t act) {
+4 -16
View File
@@ -284,10 +284,8 @@ void UITask::buildDiag() {
s_noise_status = nullptr; s_noise_status = nullptr;
if (s_tab == TAB_NOISE) { 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 " 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.", THEME_FONT_SMALL, theme::TEXT_MUTED); "either side. Takes about 12 seconds.");
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(s_list); lv_obj_t* b = lv_button_create(s_list);
lv_obj_set_size(b, LV_PCT(100), 40); lv_obj_set_size(b, LV_PCT(100), 40);
lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_shadow_width(b, 0, 0);
@@ -363,18 +361,8 @@ void UITask::refreshDiag() {
} }
void UITask::diagResetPopup() { void UITask::diagResetPopup() {
lv_obj_t* panel = navPopupPanel("Reset counters?", false); confirmBody(navPopupPanel("Reset counters?", false), "Zeroes packet and error counts.", LV_SYMBOL_REFRESH " Reset",
lv_obj_t* t = label(panel, "Zeroes packet and error counts.", THEME_FONT_SMALL, onDiagResetGo, 0);
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));
} }
void UITask::diagReset() { void UITask::diagReset() {
+1 -3
View File
@@ -245,9 +245,7 @@ void UITask::buildGps() {
lv_obj_set_style_pad_row(col, 2, 0); lv_obj_set_style_pad_row(col, 2, 0);
lv_obj_set_style_pad_top(col, 4, 0); lv_obj_set_style_pad_top(col, 4, 0);
s_status = label(col, "", THEME_FONT_LARGE, theme::TEXT); s_status = label(col, "", THEME_FONT_LARGE, theme::TEXT);
s_used = label(col, "", THEME_FONT_BODY, theme::TEXT); s_used = noteLabel(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));
lv_obj_set_style_pad_bottom(s_used, 4, 0); 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_t* facts = infoCard(col); // no card of its own: rows on the page
lv_obj_set_style_bg_opa(facts, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(facts, LV_OPA_TRANSP, 0);
+69 -74
View File
@@ -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 // 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 // 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. // 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 // Browser simulator (variants/sim/build_wasm_lvgl.sh): SimLcdDisplay blits to
// a <canvas>, the host page feeds the mouse in as touch. // a <canvas>, 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 bool s_swallow = false;
static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { 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. // The browser is always online; saved WiFi networks only live for the session.
static void wifiStoreLoad() { s_net_count = 0; } static void wifiStoreLoad() { s_net_count = 0; }
static void wifiStoreWrite() {} 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 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). // (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; } 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 const char* resetReason() { return "Power on"; }
static bool crashSummary(char*, size_t) { return false; } 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 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 bool s_net_on = false;
static void netBegin(const char*, const char*) { s_net_on = true; } static void netBegin(const char*, const char*) { s_net_on = true; }
static void netJoin(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)" #error "ui-lvgl: no LVGL port for this board (see LvglPort.h)"
#endif #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 } // namespace lvport
+7 -19
View File
@@ -34,7 +34,6 @@ static int s_prev_z;
static bool s_prev_follow; static bool s_prev_follow;
static int s_idx = -1; // area in the open popup static int s_idx = -1; // area in the open popup
static lv_obj_t* s_del_lbl = nullptr; 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 BAR_H = 84;
static const int HANDLE = 26; 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) { static void fmtDate(char* out, size_t n, uint32_t t, const NodePrefs* p) {
if (t < 1000000000UL) { out[0] = '\0'; return; } struct tm tm;
time_t tt = (time_t)((int64_t)t + (int64_t)(p ? p->tz_offset_hours : 0) * 3600); if (!localTime(p, tm, t)) { out[0] = '\0'; return; }
struct tm tm = *gmtime(&tt); snprintf(out, n, "%d %s %d", tm.tm_mday, MONTHS[tm.tm_mon], tm.tm_year + 1900);
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);
} }
static void onHandle(lv_event_t* e) { static void onHandle(lv_event_t* e) {
@@ -387,9 +384,7 @@ void UITask::mapAreasPopup() {
listRow(g, m.name, sub, onArea, (void*)(intptr_t)i); listRow(g, m.name, sub, onArea, (void*)(intptr_t)i);
} }
} else { } 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); noteLabel(list, "No areas yet. Maps downloaded before this list are still on the card.");
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
} }
lv_obj_t* add = lv_button_create(list); lv_obj_t* add = lv_button_create(list);
lv_obj_set_size(add, LV_PCT(100), 38); 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, 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_TRAILS) ? ", trails" : "", date[0] ? " - " : "", date,
(m.flags & mapview::AreaStore::F_COMPLETE) ? "" : "\nUnfinished - Fill gaps completes it"); (m.flags & mapview::AreaStore::F_COMPLETE) ? "" : "\nUnfinished - Fill gaps completes it");
lv_obj_t* l = label(panel, info, THEME_FONT_SMALL, theme::TEXT_MUTED); noteLabel(panel, info);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); lv_obj_t* r = buttonBar(panel);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* r = toolRow(panel);
actButton(r, LV_SYMBOL_EDIT, "Rename", A_RENAME); actButton(r, LV_SYMBOL_EDIT, "Rename", A_RENAME);
actButton(r, LV_SYMBOL_DOWNLOAD, "Fill", A_FILL); actButton(r, LV_SYMBOL_DOWNLOAD, "Fill", A_FILL);
actButton(r, LV_SYMBOL_REFRESH, "Refresh", A_REFRESH); 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); 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_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. // Framed in the map above the sheet: the view is kept to go back to.
lv_obj_update_layout(panel); lv_obj_update_layout(panel);
@@ -492,11 +484,7 @@ void UITask::mapAreaAction(uint8_t act) {
showToast("Fetching its trails"); showToast("Fetching its trails");
return; return;
case A_DELETE: case A_DELETE:
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms if (!tapConfirmed(s_del_lbl, "Delete?")) return;
s_del_armed_ms = millis() | 1;
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
return;
}
if (mapview::s_areas.deleting()) { showToast("Still deleting - try again shortly"); 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 (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; if (s_show == s_idx) s_show = -1;
+7 -29
View File
@@ -8,7 +8,6 @@
namespace regions { namespace regions {
static int s_sel = -1; // the pack a popup is about 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; static lv_obj_t* s_del_lbl = nullptr;
enum : uint8_t { A_SHOW, A_DELETE }; 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 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 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) { static lv_obj_t* actButton(lv_obj_t* bar, const char* text, uint8_t act, bool accent) {
lv_obj_t* b = lv_button_create(row); return lv_obj_get_child(barButton(bar, text, onAct, act, accent), 0);
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 void fmtMB(char* out, size_t n, uint32_t bytes) { 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); 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." 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.", : "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));
} }
void UITask::mapRegionPopup(int idx) { void UITask::mapRegionPopup(int idx) {
@@ -71,14 +56,11 @@ void UITask::mapRegionPopup(int idx) {
char info[128], mb[16]; char info[128], mb[16];
fmtMB(mb, sizeof(mb), p.size); 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); 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); noteLabel(panel, info, THEME_FONT_SMALL, theme::TEXT);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); lv_obj_t* r = buttonBar(panel);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_t* r = toolRow(panel);
actButton(r, UI_SYMBOL_MAP " Show", A_SHOW, true); actButton(r, UI_SYMBOL_MAP " Show", A_SHOW, true);
s_del_lbl = actButton(r, LV_SYMBOL_TRASH " Delete", A_DELETE, false); 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); lv_obj_add_event_cb(s_del_lbl, onDeleted, LV_EVENT_DELETE, NULL);
s_del_armed_ms = 0;
} }
void UITask::mapRegionAction(uint8_t act) { void UITask::mapRegionAction(uint8_t act) {
@@ -100,11 +82,7 @@ void UITask::mapRegionAction(uint8_t act) {
return; return;
} }
case A_DELETE: { case A_DELETE: {
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // a second tap within 3 s confirms if (!tapConfirmed(s_del_lbl, "Delete?")) return;
s_del_armed_ms = millis() | 1;
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
return;
}
char path[80]; char path[80];
snprintf(path, sizeof(path), "%s/%s", mapview::VECTOR_ROOT, p.file); snprintf(path, sizeof(path), "%s/%s", mapview::VECTOR_ROOT, p.file);
mapview::s_vpacks.closeOpen(); mapview::s_vpacks.closeOpen();
+3 -18
View File
@@ -744,9 +744,7 @@ void UITask::mapDownloadPopup() {
headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL); headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL);
lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
_dl_info = label(panel, "", THEME_FONT_SMALL, theme::TEXT); _dl_info = noteLabel(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));
lv_obj_set_style_text_line_space(_dl_info, 2, 0); lv_obj_set_style_text_line_space(_dl_info, 2, 0);
_dl_bar = lv_bar_create(panel); _dl_bar = lv_bar_create(panel);
lv_obj_set_size(_dl_bar, LV_PCT(100), 6); 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_set_width(_dl_err, LV_PCT(100));
lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(_dl_err, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* acts = lv_obj_create(panel); lv_obj_t* go = barButton(buttonBar(panel), "", onDlStart, 0, true);
styleSurface(acts, theme::BG); _dl_start_lbl = lv_obj_get_child(go, 0);
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);
stylePrimary(go); stylePrimary(go);
refreshDownloadPopup(); refreshDownloadPopup();
+23 -88
View File
@@ -496,7 +496,6 @@ void UITask::navClosePopup() {
if (_nav_overlay) lv_obj_delete_async(_nav_overlay); // may be closing from its own button 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_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_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) { 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 (d[0]) navRowRight(row, d, theme::TEXT_MUTED, 52);
} }
if (wp.count() == 0) { 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.", noteLabel(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));
} }
TrailStore& ts = _core->trail.store(); TrailStore& ts = _core->trail.store();
if (!ts.empty()) { if (!ts.empty()) {
@@ -598,12 +594,7 @@ void UITask::navWaypointMenu(int idx) {
lv_obj_t* panel = navPopupPanel(w.label[0] ? w.label : "(unnamed)", false); lv_obj_t* panel = navPopupPanel(w.label[0] ? w.label : "(unnamed)", false);
placeCard(panel, w.lat_1e6, w.lon_1e6); placeCard(panel, w.lat_1e6, w.lon_1e6);
lv_obj_t* acts = lv_obj_create(panel); lv_obj_t* acts = buttonBar(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);
struct { const char* text; uint8_t act; bool accent; } btns[] = { struct { const char* text; uint8_t act; bool accent; } btns[] = {
{ UI_SYMBOL_COMPASS " Go", navmap::WP_NAV, true }, { UI_SYMBOL_COMPASS " Go", navmap::WP_NAV, true },
{ LV_SYMBOL_EDIT " Name", navmap::WP_RENAME, false }, { LV_SYMBOL_EDIT " Name", navmap::WP_RENAME, false },
@@ -611,17 +602,8 @@ void UITask::navWaypointMenu(int idx) {
{ LV_SYMBOL_TRASH, navmap::WP_DELETE, false }, { LV_SYMBOL_TRASH, navmap::WP_DELETE, false },
}; };
for (auto& b : btns) { for (auto& b : btns) {
lv_obj_t* bt = lv_button_create(acts); lv_obj_t* bt = barButton(acts, b.text, onNavWpAction, b.act, b.accent);
lv_obj_set_height(bt, 40); if (b.act == navmap::WP_DELETE) _nav_del_lbl = lv_obj_get_child(bt, 0);
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;
} }
} }
@@ -643,11 +625,7 @@ void UITask::navWaypointAction(uint8_t act) {
break; break;
} }
case navmap::WP_DELETE: case navmap::WP_DELETE:
if (!_nav_del_armed_ms || millis() - _nav_del_armed_ms > 3000) { // second tap within 3 s confirms if (!tapConfirmed(_nav_del_lbl, "Delete?")) break;
_nav_del_armed_ms = millis() | 1;
if (_nav_del_lbl) lv_label_set_text(_nav_del_lbl, "Delete?");
break;
}
_core->waypoints.remove(i); _core->waypoints.remove(i);
navClosePopup(); navClosePopup();
showToast("Waypoint deleted"); showToast("Waypoint deleted");
@@ -805,15 +783,13 @@ static const int ST_MAX = 40;
static char s_st_names[ST_MAX][28]; // newest first static char s_st_names[ST_MAX][28]; // newest first
static int s_st_n = 0; static int s_st_n = 0;
static int s_st_sel = -2; // the one the popup is about; -1 = the internal slot 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; static lv_obj_t* s_st_del_lbl = nullptr;
// "trail-20260926-1405.trl" -> "26 Sep 2026 14:05" (else the name). // "trail-20260926-1405.trl" -> "26 Sep 2026 14:05" (else the name).
static void trailTitle(const char* name, char* out, size_t n) { 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; 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) 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); 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))); s_ui->navSetShareTarget(choiceSelected((lv_obj_t*)lv_event_get_target(e)));
} }
static lv_obj_t* toolRow(lv_obj_t* parent) { // A map tool's button (UITask.cpp's barButton); returns its label.
lv_obj_t* r = lv_obj_create(parent); static lv_obj_t* toolButton(lv_obj_t* bar, const char* text, uint8_t act, bool accent) {
styleSurface(r, theme::BG); return lv_obj_get_child(barButton(bar, text, onNavTool, act, accent), 0);
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;
} }
void UITask::navToolsPopup() { void UITask::navToolsPopup() {
@@ -902,11 +858,11 @@ void UITask::navToolsPopup() {
sectionTitle(list, "TRAIL"); sectionTitle(list, "TRAIL");
_nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); _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); _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_SAVE " Save", navmap::TL_TRAIL_SAVE, false);
toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_TRAIL_LOAD, 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); _nav_tb_btn = toolButton(r, "", navmap::TL_TRACKBACK, false);
toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_TRAIL_GPX, 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); _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); (void*)(uintptr_t)navmap::TL_OPT_TRAIL);
sectionTitle(list, "LIVE SHARE"); sectionTitle(list, "LIVE SHARE");
_nav_share_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); _nav_share_lbl = noteLabel(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));
// Target: channels (as named -- "#" marks a hashtag channel), then // Target: channels (as named -- "#" marks a hashtag channel), then
// favourite contacts (starred, so the two stay apart). // favourite contacts (starred, so the two stay apart).
char opts[MAX_GROUP_CHANNELS * 24 + 16 * 40]; char opts[MAX_GROUP_CHANNELS * 24 + 16 * 40];
@@ -943,7 +897,7 @@ void UITask::navToolsPopup() {
favs++; favs++;
} }
choiceRow(group(list, nullptr), "Send to", nullptr, o ? opts : "(no channels)", sel, onNavShareTarget); 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); _nav_share_btn = toolButton(r, "", navmap::TL_SHARE_TOGGLE, true);
toolButton(r, LV_SYMBOL_UPLOAD " Send once", navmap::TL_SHARE_ONCE, false); 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); 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); listRow(g, "Vector regions", vr, onNavTool, (void*)(uintptr_t)navmap::TL_REGIONS);
_nav_reset_armed_ms = 0;
refreshNavTools(); 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()); 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_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"); 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) { if (_prefs->loc_share_enabled) {
char left[12]; 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) { static void trailFilePath(char* path, size_t n, const char* ext, const NodePrefs* p) {
char stem[40]; char stem[40];
uint32_t now = rtc_clock.getCurrentTime(); uint32_t now = rtc_clock.getCurrentTime();
if (now > 1000000000UL) { struct tm ti;
time_t t = (time_t)((int64_t)now + (int64_t)(p ? p->tz_offset_hours : 0) * 3600); if (localTime(p, ti, now)) {
struct tm* ti = gmtime(&t); snprintf(stem, sizeof(stem), "%s/trail-%04d%02d%02d-%02d%02d", navmap::TRAILS_DIR, ti.tm_year + 1900,
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);
ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min);
} else { } else {
snprintf(stem, sizeof(stem), "%s/trail-%lu", navmap::TRAILS_DIR, (unsigned long)(millis() / 1000)); 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) { void UITask::savedTrailPopup(int idx) {
if (idx >= navmap::s_st_n) return; if (idx >= navmap::s_st_n) return;
navmap::s_st_sel = idx; navmap::s_st_sel = idx;
navmap::s_st_del_armed_ms = 0;
char title[32]; char title[32];
if (idx < 0) snprintf(title, sizeof(title), "Saved on the device"); if (idx < 0) snprintf(title, sizeof(title), "Saved on the device");
else navmap::trailTitle(navmap::s_st_names[idx], title, sizeof(title)); 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); label(panel, info, THEME_FONT_BODY, theme::TEXT);
if (!_core->trail.store().empty()) if (!_core->trail.store().empty())
label(panel, "Loading replaces the trail on the map.", THEME_FONT_SMALL, theme::TEXT_MUTED); 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); lv_obj_t* lb = toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_ST_LOAD, true);
stylePrimary(lv_obj_get_parent(lb)); stylePrimary(lv_obj_get_parent(lb));
if (idx >= 0) toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_ST_GPX, false); 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] = ""; char path[64] = "";
if (idx >= 0) snprintf(path, sizeof(path), "%s/%s", navmap::TRAILS_DIR, navmap::s_st_names[idx]); if (idx >= 0) snprintf(path, sizeof(path), "%s/%s", navmap::TRAILS_DIR, navmap::s_st_names[idx]);
if (act == navmap::TL_ST_DELETE) { if (act == navmap::TL_ST_DELETE) {
if (!navmap::s_st_del_armed_ms || millis() - navmap::s_st_del_armed_ms > 3000) { // second tap confirms if (!tapConfirmed(navmap::s_st_del_lbl, "Delete?")) return;
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;
}
bool ok = idx >= 0 ? remove(path) == 0 bool ok = idx >= 0 ? remove(path) == 0
: (the_mesh.getDataStore() && the_mesh.getDataStore()->removeFile(TrailEngine::TRAIL_FILE)); : (the_mesh.getDataStore() && the_mesh.getDataStore()->removeFile(TrailEngine::TRAIL_FILE));
showToast(ok ? "Trail deleted" : "Delete failed"); showToast(ok ? "Trail deleted" : "Delete failed");
@@ -1209,13 +1152,8 @@ void UITask::navToolAction(uint8_t act) {
savedTrailAction(act); savedTrailAction(act);
return; return;
case navmap::TL_TRAIL_RESET: case navmap::TL_TRAIL_RESET:
if (!_nav_reset_armed_ms || millis() - _nav_reset_armed_ms > 3000) { // second tap within 3 s confirms if (!tapConfirmed(_nav_reset_lbl, "Reset?")) return;
_nav_reset_armed_ms = millis() | 1;
lv_label_set_text(_nav_reset_lbl, "Reset?");
return;
}
tr.reset(); tr.reset();
_nav_reset_armed_ms = 0;
showToast("Trail cleared"); showToast("Trail cleared");
rebuildMapMarkers(); rebuildMapMarkers();
layoutMap(); layoutMap();
@@ -1312,7 +1250,7 @@ void UITask::navWaypointsPopup() {
char title[32]; char title[32];
snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY); snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY);
lv_obj_t* panel = navPopupPanel(title, true); 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, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true);
toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false); toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false);
@@ -1332,10 +1270,7 @@ void UITask::navWaypointsPopup() {
} }
} }
if (wp.count() == 0) { 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.", noteLabel(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));
} }
} }
@@ -1398,7 +1333,7 @@ void UITask::navSpotPopup(int32_t lat, int32_t lon) {
_nav_spot_lon = lon; _nav_spot_lon = lon;
lv_obj_t* panel = navPopupPanel("This spot", false); lv_obj_t* panel = navPopupPanel("This spot", false);
placeCard(panel, lat, lon); 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_FLAG " Add waypoint", navmap::TL_SPOT_ADD, true);
toolButton(r, UI_SYMBOL_COMPASS " Go here", navmap::TL_SPOT_GO, false); toolButton(r, UI_SYMBOL_COMPASS " Go here", navmap::TL_SPOT_GO, false);
} }
+1 -3
View File
@@ -186,9 +186,7 @@ void UITask::buildOta() {
lv_obj_t* card = infoCard(body); lv_obj_t* card = infoCard(body);
infoRow(card, "Installed", FIRMWARE_VERSION); infoRow(card, "Installed", FIRMWARE_VERSION);
s_ota_latest = infoRow(card, "Latest release", ""); s_ota_latest = infoRow(card, "Latest release", "");
_ota_status = label(body, "", THEME_FONT_BODY, theme::TEXT); _ota_status = noteLabel(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_bar = lv_bar_create(body); _ota_bar = lv_bar_create(body);
lv_obj_set_size(_ota_bar, LV_PCT(100), 10); lv_obj_set_size(_ota_bar, LV_PCT(100), 10);
lv_bar_set_range(_ota_bar, 0, 1000); lv_bar_set_range(_ota_bar, 0, 1000);
+13 -32
View File
@@ -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_slot = -1; // user preset slot the popup is about
static int s_scope = -1; // scope list index 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 } // namespace radiox
static void onPresetRow(lv_event_t* e) { s_ui->presetMenu((int)(uintptr_t)lv_event_get_user_data(e)); } 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]; 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); 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); label(panel, info, THEME_FONT_BODY, theme::TEXT);
lv_obj_t* acts = convview::actionRow(panel); lv_obj_t* acts = buttonBar(panel);
convview::actionButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true); barButton(acts, LV_SYMBOL_OK " Use", onPresetAction, radiox::P_USE, true);
convview::actionButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false); barButton(acts, LV_SYMBOL_TRASH " Delete", onPresetAction, radiox::P_DELETE, false);
} }
void UITask::presetAction(uint8_t act) { void UITask::presetAction(uint8_t act) {
@@ -108,9 +90,8 @@ void UITask::presetAction(uint8_t act) {
case P_DELETE: { case P_DELETE: {
char t[40]; char t[40];
snprintf(t, sizeof(t), "Delete %s?", u.name); snprintf(t, sizeof(t), "Delete %s?", u.name);
lv_obj_t* panel = navPopupPanel(t, false); confirmBody(navPopupPanel(t, false), "Removes the saved preset. The radio settings stay as they are.",
note(panel, "Removes the saved preset. The radio settings stay as they are."); LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO);
redButton(panel, LV_SYMBOL_TRASH " Delete", onPresetAction, P_DELETE_GO);
break; break;
} }
case P_DELETE_GO: case P_DELETE_GO:
@@ -199,13 +180,13 @@ void UITask::scopeMenu(int idx) {
radiox::s_scope = idx; radiox::s_scope = idx;
bool def = idx == sl.default_idx; bool def = idx == sl.default_idx;
lv_obj_t* panel = navPopupPanel(idx == 0 ? "* (no scope)" : sl.name((uint8_t)idx), false); 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 "*" if (idx == 0 && def) return; // nothing else to do with "*"
lv_obj_t* acts = convview::actionRow(panel); lv_obj_t* acts = buttonBar(panel);
if (!def) convview::actionButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true); if (!def) barButton(acts, LV_SYMBOL_OK " Default", onScopeAction, radiox::S_DEFAULT, true);
if (idx > 0) { if (idx > 0) {
convview::actionButton(acts, LV_SYMBOL_EDIT " Rename", onScopeAction, radiox::S_RENAME, false); barButton(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_TRASH " Delete", onScopeAction, radiox::S_DELETE, false);
} }
} }
@@ -227,9 +208,9 @@ void UITask::scopeAction(uint8_t act) {
if (s_scope == 0) break; if (s_scope == 0) break;
char t[40]; char t[40];
snprintf(t, sizeof(t), "Delete %s?", sl.name((uint8_t)s_scope)); snprintf(t, sizeof(t), "Delete %s?", sl.name((uint8_t)s_scope));
lv_obj_t* panel = navPopupPanel(t, false); confirmBody(navPopupPanel(t, false),
note(panel, "Channels using it go back to no scope. If it is the default, * becomes the default."); "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); LV_SYMBOL_TRASH " Delete", onScopeAction, S_DELETE_GO);
break; break;
} }
case S_DELETE_GO: case S_DELETE_GO:
@@ -133,10 +133,7 @@ void UITask::repeaterScopesPopup() {
if (sl.count == 0) { showToast("Set up scopes in Settings > Radio first"); return; } if (sl.count == 0) { showToast("Set up scopes in Settings > Radio first"); return; }
lv_obj_t* panel = navPopupPanel("Extra scopes", sl.count > 2); 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 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.", noteLabel(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));
for (uint8_t i = 0; i < sl.count; i++) { 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, lv_obj_t* sw = switchRow(panel, sl.name((uint8_t)(i + 1)), (i + 1) == sl.default_idx ? "Default - always relayed" : nullptr,
nullptr); nullptr);
@@ -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_cat_val[C_COUNT];
static lv_obj_t* s_status = nullptr; static lv_obj_t* s_status = nullptr;
static lv_obj_t* s_clear_lbl = 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_lbl = nullptr; // "Delete live map tiles" row label
static lv_obj_t* s_live_info = nullptr; // count / size from the index 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 uint32_t s_shown_ms = 0;
static lv_obj_t* s_fmt_overlay = nullptr; // Format card: the popup 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 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) { static void fmtBytes(char* out, size_t n, uint64_t b) {
if (b < 1024) snprintf(out, n, "%u B", (unsigned)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); 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_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_overlay = s_fmt_go_lbl = nullptr; // went with the old screen
s_fmt_armed = false;
memset(s_cat_val, 0, sizeof(s_cat_val)); memset(s_cat_val, 0, sizeof(s_cat_val));
s_clear_armed_ms = 0;
lv_obj_t* g = group(body, "SD CARD"); lv_obj_t* g = group(body, "SD CARD");
if (!lvport::mountStorage()) { 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); 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); s_live_lbl = actionRow(g, LV_SYMBOL_TRASH " Delete live map tiles", onLiveClear, NULL, theme::FAIL);
groupNote(body, "Downloaded areas stay."); groupNote(body, "Downloaded areas stay.");
s_live_armed_ms = 0;
s_shown_ms = 0; s_shown_ms = 0;
} }
@@ -326,9 +320,7 @@ void UITask::pollStorage() {
fmtBytes(t, sizeof(t), s_sd_total); fmtBytes(t, sizeof(t), s_sd_total);
snprintf(line, sizeof(line), "The card is %s but its FAT partition only %s. " 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); "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); noteLabel(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));
} }
} }
if (s_walking) stepWalk(12); 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); else snprintf(t, sizeof(t), "%lu tiles, %s", (unsigned long)mapview::s_live_cache.count(), b);
lv_label_set_text(s_live_info, t); 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) { void UITask::storageKeep(int idx) {
@@ -367,16 +351,10 @@ void UITask::storageKeep(int idx) {
void UITask::storageClearHistory() { void UITask::storageClearHistory() {
using namespace storeview; using namespace storeview;
if (!s_clear_armed_ms) { // first tap: ask if (!tapConfirmed(s_clear_lbl, LV_SYMBOL_TRASH " Tap again to delete")) return;
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;
s_archive.clearAll(); s_archive.clearAll();
_core->history.clearAll(); _core->history.clearAll();
_core->markAllRead(); _core->markAllRead();
if (s_clear_lbl) lv_label_set_text(s_clear_lbl, LV_SYMBOL_TRASH " Delete message history");
showToast("Message history deleted"); showToast("Message history deleted");
startWalk(); startWalk();
} }
@@ -389,14 +367,8 @@ void UITask::storageLiveCap(int idx) {
void UITask::storageClearLive() { void UITask::storageClearLive() {
using namespace storeview; using namespace storeview;
if (!s_live_armed_ms) { // first tap: ask if (!tapConfirmed(s_live_lbl, LV_SYMBOL_TRASH " Tap again to delete")) return;
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;
mapview::s_live_cache.clearAll(); // a few files per loop (mapDownloadTick) 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"); 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() { void UITask::storageFormatAsk() {
using namespace storeview; using namespace storeview;
if (s_fmt_overlay) return; if (s_fmt_overlay) return;
s_fmt_armed = false;
lv_obj_t* panel = popupOpen(screen(), POP_FIT, s_fmt_overlay); lv_obj_t* panel = popupOpen(screen(), POP_FIT, s_fmt_overlay);
lv_obj_set_style_pad_row(panel, 8, 0); lv_obj_set_style_pad_row(panel, 8, 0);
label(panel, "Format the SD card?", THEME_FONT_TITLE, theme::TEXT); 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, " noteLabel(panel, "Everything on it is erased: map areas, live map tiles, message history, "
"trails and screenshots. The device restarts afterwards.", "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));
lv_obj_t* row = flexBox(panel, LV_FLEX_FLOW_ROW); lv_obj_t* row = flexBox(panel, LV_FLEX_FLOW_ROW);
lv_obj_set_width(row, LV_PCT(100)); lv_obj_set_width(row, LV_PCT(100));
lv_obj_set_style_pad_column(row, theme::GAP, 0); 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; s_fmt_overlay = s_fmt_go_lbl = nullptr;
return; return;
} }
if (!s_fmt_armed) { // the second confirmation if (!tapConfirmed(s_fmt_go_lbl, "Tap again")) return; // the second confirmation
s_fmt_armed = true;
if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Tap again");
return;
}
if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Formatting..."); if (s_fmt_go_lbl) lv_label_set_text(s_fmt_go_lbl, "Formatting...");
lv_refr_now(NULL); // shown before the loop blocks lv_refr_now(NULL); // shown before the loop blocks
stopWalk(); 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); 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, LV_SYMBOL_USB, THEME_FONT_TITLE, theme::ACCENT);
label(row, title, THEME_FONT_TITLE, theme::TEXT); label(row, title, THEME_FONT_TITLE, theme::TEXT);
lv_obj_t* t = label(panel, text, THEME_FONT_SMALL, theme::TEXT_MUTED); noteLabel(panel, text);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_width(t, LV_PCT(100));
if (!buttons) return; if (!buttons) return;
lv_obj_t* br = flexBox(panel, LV_FLEX_FLOW_ROW); lv_obj_t* br = flexBox(panel, LV_FLEX_FLOW_ROW);
lv_obj_set_width(br, LV_PCT(100)); lv_obj_set_width(br, LV_PCT(100));
+103 -74
View File
@@ -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); lv_obj_set_style_text_color(l, lv_color_hex(color), 0);
return l; 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 // A list filling the rest of a flex column, scrolling on its own (the
// scrollbar only while it moves). // 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_flex_flow(r, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_row(r, 2, 0); lv_obj_set_style_pad_row(r, 2, 0);
label(r, key, THEME_FONT_SMALL, theme::TEXT_MUTED); label(r, key, THEME_FONT_SMALL, theme::TEXT_MUTED);
lv_obj_t* t = label(r, text, THEME_FONT_SMALL, theme::TEXT); lv_obj_t* t = noteLabel(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));
return t; 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) { 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_obj_t* l = noteLabel(parent, text);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
lv_obj_set_style_pad_hor(l, theme::PAD, 0); lv_obj_set_style_pad_hor(l, theme::PAD, 0);
return l; 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. // 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_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_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_obj_t* l = noteLabel(t, text, THEME_FONT_BODY, col);
lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l, LV_PCT(100));
if (hint) { if (hint) {
lv_obj_t* h = label(t, hint, THEME_FONT_SMALL, theme::TEXT_MUTED); noteLabel(t, hint);
lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP);
lv_obj_set_width(h, LV_PCT(100));
} }
lv_obj_set_user_data(row, l); // rowTitle() lv_obj_set_user_data(row, l); // rowTitle()
return l; 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); 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 // "@[nick]" -> "@nick" in place, for one-line text (previews, quotes) where
// the bubble's highlight doesn't reach. // the bubble's highlight doesn't reach.
static void plainMentions(char* t) { 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); 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. // 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) { static bool localTime(const NodePrefs* p, struct tm& out, uint32_t utc = rtc_clock.getCurrentTime()) {
uint32_t now = rtc_clock.getCurrentTime(); return localTm(utc, p ? p->tz_offset_hours : 0, out);
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;
} }
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; // "14:05", or "2:05" (+ " PM" with suffix) with Settings > 12-hour clock;
// with `seconds`, ":09" follows unless Settings > Clock seconds is off. // 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) { 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". // "Thu 25 Sep 2026".
static void fmtDate(char* b, size_t n, const struct tm& ti) { 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* DOW[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", snprintf(b, n, "%s %d %s %d", DOW[ti.tm_wday], ti.tm_mday, MONTHS[ti.tm_mon], ti.tm_year + 1900);
"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);
} }
// Settings changed here reach flash once their screen is left (as on the L1: // 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 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) { 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; } if (!clockBehind(now, ts)) { geo::fmtAgeShort(b, (int)n, now, ts ? ts : now); return; }
static const char* MON[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", struct tm ti;
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; localTime(p, ti, ts); // set: clockBehind() says ts is
time_t t = (time_t)((int64_t)ts + (int64_t)(p ? p->tz_offset_hours : 0) * 3600);
struct tm ti = *gmtime(&t);
char clk[12]; char clk[12];
fmtClock(clk, sizeof(clk), ti, p, true); 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) { 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; if (_buzzer.isPlaying()) return 1;
#endif #endif
if (isClientConnected()) return 2; 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 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); lv_obj_align(label(row, right, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
} }
if (n == 0) { if (n == 0) {
lv_obj_t* l = label(_scan_list, _scanning ? "Repeaters and rooms in range will answer." lv_obj_t* l = noteLabel(_scan_list, _scanning ? "Repeaters and rooms in range will answer."
: "Nobody answered. Try again later or move.", : "Nobody answered. Try again later or move.",
THEME_FONT_BODY, theme::TEXT_MUTED); 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_set_style_pad_top(l, 8, 0); lv_obj_set_style_pad_top(l, 8, 0);
} }
} }
@@ -2220,7 +2283,6 @@ void UITask::openScanNode(int row) {
_node_from_map = false; _node_from_map = false;
_scanning = false; _scanning = false;
_pinging = false; _pinging = false;
_delete_armed_ms = 0;
_screen = SCR_NODE; _screen = SCR_NODE;
buildNode(); buildNode();
} }
@@ -2292,26 +2354,10 @@ void UITask::openNode(int row) {
_node_from_scan = false; _node_from_scan = false;
_node_from_map = false; _node_from_map = false;
_pinging = false; _pinging = false;
_delete_armed_ms = 0;
_screen = SCR_NODE; _screen = SCR_NODE;
buildNode(); 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() { void UITask::buildNode() {
const NearbyModel::Entry& e = s_node; const NearbyModel::Entry& e = s_node;
lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true); 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", ""); s_nd_id = infoRow(_node_info, "ID", "");
_node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT); _node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT);
lv_obj_t* acts = lv_obj_create(body); lv_obj_t* acts = buttonBar(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);
bool contact = e.contact_idx >= 0; bool contact = e.contact_idx >= 0;
bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM); bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM);
bool pos = e.lat_e6 != 0 || e.lon_e6 != 0; 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 for (int i = 0; i < n; i++) { // five or more: icons only, so every button fits one row
char t[24]; char t[24];
snprintf(t, sizeof(t), "%s%s", list[i].icon, n >= 5 ? "" : list[i].text); 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); lv_obj_t* b = barButton(acts, t, onNodeAction, list[i].action, list[i].accent);
if (list[i].action == NODE_DELETE) _node_delete_lbl = l; if (list[i].action == NODE_DELETE) _node_delete_lbl = lv_obj_get_child(b, 0);
} }
refreshNode(); refreshNode();
@@ -2420,10 +2461,6 @@ void UITask::refreshNode() {
_pinging = false; _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) { void UITask::nodeAction(uint8_t action) {
@@ -2488,10 +2525,7 @@ void UITask::nodeAction(uint8_t action) {
pinPopup(false, 0, e.pub_key); pinPopup(false, 0, e.pub_key);
break; break;
case NODE_DELETE: case NODE_DELETE:
if (!_delete_armed_ms) { // destructive: second tap within 3 s confirms if (!tapConfirmed(_node_delete_lbl, LV_SYMBOL_TRASH "?")) { // "?" fits an icon-only button
_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
showToast("Tap again to delete the contact", 2500); showToast("Tap again to delete the contact", 2500);
break; break;
} }
@@ -3121,17 +3155,13 @@ void UITask::pruneContacts() {
showToast(_prefs && _prefs->contact_expiry_idx == 0 ? "Contact expiry is off" : "No inactive contacts"); showToast(_prefs && _prefs->contact_expiry_idx == 0 ? "Contact expiry is off" : "No inactive contacts");
return; return;
} }
if (!_prune_armed_ms || millis() - _prune_armed_ms > 3000) { char ask[40];
_prune_armed_ms = millis() | 1; snprintf(ask, sizeof(ask), LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s");
if (_prune_lbl) lv_label_set_text_fmt(_prune_lbl, LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s"); if (!tapConfirmed(_prune_lbl, ask)) return;
return;
}
_prune_armed_ms = 0;
int removed = the_mesh.pruneStaleContacts(); int removed = the_mesh.pruneStaleContacts();
char t[40]; char t[40];
snprintf(t, sizeof(t), "Removed %d contact%s", removed, removed == 1 ? "" : "s"); snprintf(t, sizeof(t), "Removed %d contact%s", removed, removed == 1 ? "" : "s");
showToast(t); showToast(t);
if (_prune_lbl) lv_label_set_text(_prune_lbl, LV_SYMBOL_TRASH " Remove inactive contacts now");
} }
void UITask::applyDisplayPrefs() { void UITask::applyDisplayPrefs() {
@@ -3171,7 +3201,6 @@ void UITask::buildSchemaSettings() {
: s_nav_section == settings::SEC_LIVE_SHARE ? "Share options" : "Alert options"; : s_nav_section == settings::SEC_LIVE_SHARE ? "Share options" : "Alert options";
lv_obj_t* body = newScreen(title, true); lv_obj_t* body = newScreen(title, true);
_prune_lbl = nullptr; _prune_lbl = nullptr;
_prune_armed_ms = 0;
switch (_settings_page) { switch (_settings_page) {
case PG_KEYBOARD: buildKeyboardPage(body); return; case PG_KEYBOARD: buildKeyboardPage(body); return;
case PG_ABOUT: buildAboutPage(body); return; case PG_ABOUT: buildAboutPage(body); return;
@@ -455,7 +455,6 @@ private:
lv_obj_t* _ota_btn_lbl = nullptr; lv_obj_t* _ota_btn_lbl = nullptr;
uint32_t _map_left_ms = 0; // when the map was left, for dropping live tiles' WiFi 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) 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 uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown
uint32_t _next_batt_ms = 0; uint32_t _next_batt_ms = 0;
lv_obj_t* _prune_lbl = nullptr; lv_obj_t* _prune_lbl = nullptr;
@@ -508,7 +507,6 @@ private:
uint32_t _next_nearby_ms = 0; uint32_t _next_nearby_ms = 0;
bool _pinging = false; bool _pinging = false;
uint32_t _ping_started_ms = 0; uint32_t _ping_started_ms = 0;
uint32_t _delete_armed_ms = 0;
lv_obj_t* _nearby_list = nullptr; lv_obj_t* _nearby_list = nullptr;
lv_obj_t* _nearby_status = nullptr; lv_obj_t* _nearby_status = nullptr;
lv_obj_t* _nearby_sort_lbl = nullptr; lv_obj_t* _nearby_sort_lbl = nullptr;
@@ -553,7 +551,6 @@ private:
lv_obj_t* _nav_ta = nullptr; lv_obj_t* _nav_ta = nullptr;
lv_obj_t* _nav_kb = nullptr; lv_obj_t* _nav_kb = nullptr;
lv_obj_t* _nav_del_lbl = 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 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_rec = nullptr; // "REC 1.2 km LIVE 58m" pill
lv_obj_t* _nav_avg_pill = nullptr; // GPS averaging progress (tap: cancel) 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_lbl = nullptr;
lv_obj_t* _nav_share_btn = nullptr; lv_obj_t* _nav_share_btn = nullptr;
lv_obj_t* _nav_tb_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 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) char _share_text[96] = ""; // waiting for a conversation to be picked (shareToMessage)
+6 -8
View File
@@ -807,10 +807,9 @@ public:
display.drawTextCentered(display.width() / 2, mid_y + step, "Enable GPS or"); display.drawTextCentered(display.width() / 2, mid_y + step, "Enable GPS or");
display.drawTextCentered(display.width() / 2, mid_y + step * 2, "connect app"); display.drawTextCentered(display.width() / 2, mid_y + step * 2, "connect app");
} else { } else {
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; struct tm lt;
unix_ts += (int32_t)tz * 3600; localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, lt);
time_t t = (time_t)unix_ts; struct tm* ti = &lt;
struct tm* ti = gmtime(&t);
char buf[24]; char buf[24];
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -954,10 +953,9 @@ public:
if (unix_ts < 1000000000UL) { if (unix_ts < 1000000000UL) {
display.drawTextCentered(display.width() / 2, display.height() / 2 - step, "No time sync"); display.drawTextCentered(display.width() / 2, display.height() / 2 - step, "No time sync");
} else { } else {
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; struct tm lt;
unix_ts += (int32_t)tz * 3600; localTm(unix_ts, _node_prefs ? _node_prefs->tz_offset_hours : 0, lt);
time_t t = (time_t)unix_ts; struct tm* ti = &lt;
struct tm* ti = gmtime(&t);
char buf[12]; char buf[12];
bool h12 = _node_prefs && _node_prefs->clock_12h; bool h12 = _node_prefs && _node_prefs->clock_12h;
int date_y = drawClockTime(display, 0, ti, h12, /*show_sec*/false); int date_y = drawClockTime(display, 0, ti, h12, /*show_sec*/false);
+9 -15
View File
@@ -68,21 +68,9 @@ public:
expWritePin(EXP_PIN_GNSS_EN, LOW); 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; } 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 // WAKE button on expander P00: pressed = level differs from boot baseline
bool readWakeButton(); bool readWakeButton();
@@ -93,9 +81,6 @@ public:
// VBUS presence via the AW35615 USB-C controller (I2C 0x22) // VBUS presence via the AW35615 USB-C controller (I2C 0x22)
bool isExternalPowered() override; bool isExternalPowered() override;
int awRead(uint8_t reg); // AW35615 register, -1 on error
bool expanderOK() const { return expander_ok; }
private: private:
uint8_t out_shadow[2] = { 0xFF, 0xFF }; // TCA9535 output regs default high 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) 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 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); bool expWriteReg(uint8_t reg, uint8_t val);
void expSetOutput(uint8_t pin, bool initial_level); void expSetOutput(uint8_t pin, bool initial_level);