fix(companion): Live Share UX + map labels

- Live Share tool: add scrollbar, drop non-clickable section headers and
  the one-shot "Share now" (keep Auto-share toggle; manual share stays on
  the Map). Ellipsize the "To" target so long names don't wrap.
- Move "Track loc" (receive) from Settings into Live Share.
- Map: 2-char contact labels (distinguishable); drop "press" hint from the
  Home Map page status line so it fits on OLED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-22 10:23:22 +02:00
co-authored by Claude Opus 4.8
parent d78e74d4cd
commit 1d2f2beaba
3 changed files with 31 additions and 71 deletions
@@ -1,11 +1,15 @@
#pragma once #pragma once
// Live location sharing config tool. Tools Live Share. // Live location sharing config tool. Tools Live Share.
// Periodically broadcasts a [LOC] message to a chosen channel/contact while // One place for both directions of position sharing:
// moving. Independent of (and able to run alongside) the 0-hop Auto-Advert. // Track loc — receive [LOC] shares from others (LiveTrackStore).
// Auto share — periodically broadcast my own [LOC] to a channel/contact
// while moving (movement-gated engine in UITask).
// Independent of (and able to run alongside) the 0-hop Auto-Advert. The
// one-shot "Share my pos" lives on the Map screen.
// Included by UITask.cpp after AutoAdvertScreen.h. // Included by UITask.cpp after AutoAdvertScreen.h.
#include "../GeoUtils.h" // LOCATION_MSG_TAG
#include "../NodePrefs.h" #include "../NodePrefs.h"
#include "icons.h" // scrollIndicatorReserve / drawScrollIndicator
class LiveShareScreen : public UIScreen { class LiveShareScreen : public UIScreen {
UITask* _task; UITask* _task;
@@ -14,43 +18,27 @@ class LiveShareScreen : public UIScreen {
int _sel = 0; int _sel = 0;
int _scroll = 0; int _scroll = 0;
// Row model — headers are non-selectable section separators. enum Kind : uint8_t { K_TRACK, K_AUTO, K_TARGET, K_MOVE, K_GAP, K_HB };
enum Kind : uint8_t { K_HEADER, K_TRACK, K_AUTO, K_TARGET, K_MOVE, K_GAP, K_HB, K_SHARE_NOW };
struct Row { Kind kind; const char* label; }; struct Row { Kind kind; const char* label; };
static const int ROW_COUNT = 12; static const int ROW_COUNT = 6;
static Row rows(int i) { static Row rows(int i) {
static const Row R[ROW_COUNT] = { static const Row R[ROW_COUNT] = {
{ K_HEADER, "Receive" }, { K_TRACK, "Track loc" },
{ K_TRACK, "Track loc" }, { K_AUTO, "Auto share" },
{ K_HEADER, "Send" }, { K_TARGET, "To" },
{ K_AUTO, "Auto share" }, { K_MOVE, "Move" },
{ K_HEADER, "Target" }, { K_GAP, "Min gap" },
{ K_TARGET, "To" }, { K_HB, "Heartbeat" },
{ K_HEADER, "Triggers" },
{ K_MOVE, "Move" },
{ K_GAP, "Min gap" },
{ K_HB, "Heartbeat" },
{ K_HEADER, "" },
{ K_SHARE_NOW, "Share now" },
}; };
return R[i]; return R[i];
} }
static bool selectable(Kind k) { return k != K_HEADER; }
public: public:
LiveShareScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {} LiveShareScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
void enter() { void enter() { _dirty = false; _sel = 0; _scroll = 0; }
_dirty = false;
if (!selectable(rows(_sel).kind)) _sel = firstSelectable();
}
int firstSelectable() const { // Resolve the configured target's display name (channel name or contact name).
for (int i = 0; i < ROW_COUNT; i++) if (selectable(rows(i).kind)) return i;
return 0;
}
// ── target helpers ─────────────────────────────────────────────────────────
void currentTargetName(char* buf, int n) { void currentTargetName(char* buf, int n) {
if (!_prefs) { snprintf(buf, n, "none"); return; } if (!_prefs) { snprintf(buf, n, "none"); return; }
if (_prefs->loc_share_target_type == 0) { if (_prefs->loc_share_target_type == 0) {
@@ -66,7 +54,6 @@ public:
} }
} }
// ── value label for a row ──────────────────────────────────────────────────
void valueLabel(Kind k, char* buf, int n) { void valueLabel(Kind k, char* buf, int n) {
switch (k) { switch (k) {
case K_TRACK: case K_TRACK:
@@ -104,57 +91,41 @@ public:
const int y0 = display.listStart(); const int y0 = display.listStart();
const int step = display.lineStep(); const int step = display.lineStep();
const int valx = display.width() / 2 + 6;
// Scroll window: keep the selected row visible.
int vis = (display.height() - y0) / step; int vis = (display.height() - y0) / step;
if (vis < 1) vis = 1; if (vis < 1) vis = 1;
if (_sel < _scroll) _scroll = _sel; if (_sel < _scroll) _scroll = _sel;
if (_sel >= _scroll + vis) _scroll = _sel - vis + 1; if (_sel >= _scroll + vis) _scroll = _sel - vis + 1;
if (_scroll < 0) _scroll = 0; if (_scroll < 0) _scroll = 0;
if (_scroll > ROW_COUNT - vis) _scroll = (ROW_COUNT > vis) ? ROW_COUNT - vis : 0;
const int reserve = scrollIndicatorReserve(display, ROW_COUNT, vis);
const int valx = display.width() / 2 + 6;
for (int i = _scroll; i < ROW_COUNT && i < _scroll + vis; i++) { for (int i = _scroll; i < ROW_COUNT && i < _scroll + vis; i++) {
int y = y0 + (i - _scroll) * step; int y = y0 + (i - _scroll) * step;
Row r = rows(i); Row r = rows(i);
if (r.kind == K_HEADER) {
if (r.label[0]) {
display.setColor(DisplayDriver::LIGHT);
display.setCursor(2, y);
display.print(r.label);
}
continue;
}
bool sel = (i == _sel); bool sel = (i == _sel);
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel); display.drawSelectionRow(0, y - 1, display.width() - reserve, step - 1, sel);
display.setCursor(4, y); display.setCursor(4, y);
display.print(r.label); display.print(r.label);
char val[24]; char val[24];
valueLabel(r.kind, val, sizeof(val)); valueLabel(r.kind, val, sizeof(val));
if (val[0]) { if (val[0]) display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val);
display.setCursor(valx, y);
display.print(val);
}
} }
drawScrollIndicator(display, y0, vis * step, ROW_COUNT, vis, _scroll);
return 500; return 500;
} }
void moveSel(int dir) { void moveSel(int dir) { _sel = (_sel + dir + ROW_COUNT) % ROW_COUNT; }
int i = _sel;
for (int step = 0; step < ROW_COUNT; step++) {
i = (i + dir + ROW_COUNT) % ROW_COUNT;
if (selectable(rows(i).kind)) { _sel = i; return; }
}
}
// Cycle/act on the selected row. `enter` distinguishes ENTER (which opens the // Cycle/act on the selected row. `enter` opens the target picker (vs LEFT/RIGHT
// target picker / fires Share now) from LEFT/RIGHT (value cycling only). // value cycling).
void activate(int dir, bool enter) { void activate(int dir, bool enter) {
if (!_prefs) return; if (!_prefs) return;
Kind k = rows(_sel).kind; switch (rows(_sel).kind) {
switch (k) {
case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break; case K_TRACK: _prefs->track_shared_loc ^= 1; _dirty = true; break;
case K_AUTO: _prefs->loc_share_enabled ^= 1; _dirty = true; break; case K_AUTO: _prefs->loc_share_enabled ^= 1; _dirty = true; break;
case K_TARGET: case K_TARGET:
if (enter) { _task->pickLocShareTarget(); return; } // full chooser if (enter) { _task->pickLocShareTarget(); return; } // full chooser
cycleTarget(dir); _dirty = true; // L/R quick cycle cycleTarget(dir); _dirty = true; // L/R quick cycle
@@ -168,8 +139,6 @@ public:
case K_HB: case K_HB:
_prefs->loc_share_heartbeat_idx = (uint8_t)((_prefs->loc_share_heartbeat_idx + (dir >= 0 ? 1 : NodePrefs::LOC_SHARE_HEARTBEAT_COUNT - 1)) % NodePrefs::LOC_SHARE_HEARTBEAT_COUNT); _prefs->loc_share_heartbeat_idx = (uint8_t)((_prefs->loc_share_heartbeat_idx + (dir >= 0 ? 1 : NodePrefs::LOC_SHARE_HEARTBEAT_COUNT - 1)) % NodePrefs::LOC_SHARE_HEARTBEAT_COUNT);
_dirty = true; break; _dirty = true; break;
case K_SHARE_NOW: shareNow(); break;
default: break;
} }
} }
@@ -202,14 +171,6 @@ public:
else memcpy(_prefs->loc_share_dm_prefix, list[nx].prefix, NodePrefs::FAVOURITE_PREFIX_LEN); else memcpy(_prefs->loc_share_dm_prefix, list[nx].prefix, NodePrefs::FAVOURITE_PREFIX_LEN);
} }
void shareNow() {
int32_t lat, lon;
if (!_task->currentLocation(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; }
char text[40];
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
_task->shareToMessage(text);
}
bool handleInput(char c) override { bool handleInput(char c) override {
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
if (_dirty) { the_mesh.savePrefs(); _dirty = false; } if (_dirty) { the_mesh.savePrefs(); _dirty = false; }
@@ -584,7 +584,7 @@ private:
if (cx < area_x) cx = area_x; else if (cx > wp_x_max) cx = wp_x_max; if (cx < area_x) cx = area_x; else if (cx > wp_x_max) cx = wp_x_max;
if (cy < area_y) cy = area_y; else if (cy > wp_y_max) cy = wp_y_max; if (cy < area_y) cy = area_y; else if (cy > wp_y_max) cy = wp_y_max;
drawContactMarker(display, cx, cy); drawContactMarker(display, cx, cy);
char s2[2] = { s.name[0], 0 }; char s2[3] = { s.name[0], s.name[0] ? s.name[1] : (char)0, 0 };
drawWaypointLabel(display, s2, cx, cy, area_x, area_y, area_w, area_h); drawWaypointLabel(display, s2, cx, cy, area_x, area_y, area_w, area_h);
} }
} }
+1 -2
View File
@@ -963,8 +963,7 @@ public:
int32_t la, lo; int32_t la, lo;
bool fix = _task->currentLocation(la, lo); bool fix = _task->currentLocation(la, lo);
int trk = _task->liveTrack().active(rtc_clock.getCurrentTime()); int trk = _task->liveTrack().active(rtc_clock.getCurrentTime());
snprintf(info, sizeof(info), "Fix:%s Track:%d %s", snprintf(info, sizeof(info), "Fix:%s Track:%d", fix ? "y" : "n", trk);
fix ? "y" : "n", trk, PRESS_LABEL);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
if (!drew) if (!drew)
display.drawTextCentered(display.width() / 2, content_y + area_h / 2, "No GPS / no trail"); display.drawTextCentered(display.width() / 2, content_y + area_h / 2, "No GPS / no trail");