mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
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:
@@ -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,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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 = <
|
||||||
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 = <
|
||||||
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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user