From 1d2f2beabaddc55447c89308f47579797c5eef66 Mon Sep 17 00:00:00 2001 From: MarekZegare4 Date: Mon, 22 Jun 2026 10:23:22 +0200 Subject: [PATCH] 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 --- .../companion_radio/ui-new/LiveShareScreen.h | 97 ++++++------------- examples/companion_radio/ui-new/TrailScreen.h | 2 +- examples/companion_radio/ui-new/UITask.cpp | 3 +- 3 files changed, 31 insertions(+), 71 deletions(-) diff --git a/examples/companion_radio/ui-new/LiveShareScreen.h b/examples/companion_radio/ui-new/LiveShareScreen.h index 83e97052..bcfc3548 100644 --- a/examples/companion_radio/ui-new/LiveShareScreen.h +++ b/examples/companion_radio/ui-new/LiveShareScreen.h @@ -1,11 +1,15 @@ #pragma once // Live location sharing config tool. Tools › Live Share. -// Periodically broadcasts a [LOC] message to a chosen channel/contact while -// moving. Independent of (and able to run alongside) the 0-hop Auto-Advert. +// One place for both directions of position sharing: +// 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. -#include "../GeoUtils.h" // LOCATION_MSG_TAG #include "../NodePrefs.h" +#include "icons.h" // scrollIndicatorReserve / drawScrollIndicator class LiveShareScreen : public UIScreen { UITask* _task; @@ -14,43 +18,27 @@ class LiveShareScreen : public UIScreen { int _sel = 0; int _scroll = 0; - // Row model — headers are non-selectable section separators. - enum Kind : uint8_t { K_HEADER, K_TRACK, K_AUTO, K_TARGET, K_MOVE, K_GAP, K_HB, K_SHARE_NOW }; + enum Kind : uint8_t { K_TRACK, K_AUTO, K_TARGET, K_MOVE, K_GAP, K_HB }; 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 const Row R[ROW_COUNT] = { - { K_HEADER, "Receive" }, - { K_TRACK, "Track loc" }, - { K_HEADER, "Send" }, - { K_AUTO, "Auto share" }, - { K_HEADER, "Target" }, - { K_TARGET, "To" }, - { K_HEADER, "Triggers" }, - { K_MOVE, "Move" }, - { K_GAP, "Min gap" }, - { K_HB, "Heartbeat" }, - { K_HEADER, "" }, - { K_SHARE_NOW, "Share now" }, + { K_TRACK, "Track loc" }, + { K_AUTO, "Auto share" }, + { K_TARGET, "To" }, + { K_MOVE, "Move" }, + { K_GAP, "Min gap" }, + { K_HB, "Heartbeat" }, }; return R[i]; } - static bool selectable(Kind k) { return k != K_HEADER; } public: LiveShareScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {} - void enter() { - _dirty = false; - if (!selectable(rows(_sel).kind)) _sel = firstSelectable(); - } + void enter() { _dirty = false; _sel = 0; _scroll = 0; } - int firstSelectable() const { - for (int i = 0; i < ROW_COUNT; i++) if (selectable(rows(i).kind)) return i; - return 0; - } - - // ── target helpers ───────────────────────────────────────────────────────── + // Resolve the configured target's display name (channel name or contact name). void currentTargetName(char* buf, int n) { if (!_prefs) { snprintf(buf, n, "none"); return; } 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) { switch (k) { case K_TRACK: @@ -104,57 +91,41 @@ public: const int y0 = display.listStart(); 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; if (vis < 1) vis = 1; if (_sel < _scroll) _scroll = _sel; if (_sel >= _scroll + vis) _scroll = _sel - vis + 1; 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++) { int y = y0 + (i - _scroll) * step; 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); - 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.print(r.label); char val[24]; valueLabel(r.kind, val, sizeof(val)); - if (val[0]) { - display.setCursor(valx, y); - display.print(val); - } + if (val[0]) display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val); } + + drawScrollIndicator(display, y0, vis * step, ROW_COUNT, vis, _scroll); return 500; } - void moveSel(int dir) { - 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; } - } - } + void moveSel(int dir) { _sel = (_sel + dir + ROW_COUNT) % ROW_COUNT; } - // Cycle/act on the selected row. `enter` distinguishes ENTER (which opens the - // target picker / fires Share now) from LEFT/RIGHT (value cycling only). + // Cycle/act on the selected row. `enter` opens the target picker (vs LEFT/RIGHT + // value cycling). void activate(int dir, bool enter) { if (!_prefs) return; - Kind k = rows(_sel).kind; - switch (k) { + switch (rows(_sel).kind) { 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: if (enter) { _task->pickLocShareTarget(); return; } // full chooser cycleTarget(dir); _dirty = true; // L/R quick cycle @@ -168,8 +139,6 @@ public: 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); _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); } - 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 { if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { if (_dirty) { the_mesh.savePrefs(); _dirty = false; } diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 03faa48f..1fa7255c 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -584,7 +584,7 @@ private: 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; 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); } } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index abd3b9d2..5afd5c70 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -963,8 +963,7 @@ public: int32_t la, lo; bool fix = _task->currentLocation(la, lo); int trk = _task->liveTrack().active(rtc_clock.getCurrentTime()); - snprintf(info, sizeof(info), "Fix:%s Track:%d %s", - fix ? "y" : "n", trk, PRESS_LABEL); + snprintf(info, sizeof(info), "Fix:%s Track:%d", fix ? "y" : "n", trk); display.setColor(DisplayDriver::LIGHT); if (!drew) display.drawTextCentered(display.width() / 2, content_y + area_h / 2, "No GPS / no trail");