From f502d2c6196e9d72059ce95ecadf50aa1eaccdac Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:55:36 +0200 Subject: [PATCH] refactor(ui-lvgl,ui-new): shared segmented and text field widgets, favourites through contactctl, one age formatter Co-Authored-By: Claude Opus 5.5 --- docs/development/ui-core.md | 2 +- .../companion_radio/ui-lvgl/AdminScreen.h | 44 +---------- examples/companion_radio/ui-lvgl/BotScreen.h | 30 +------- .../companion_radio/ui-lvgl/ChannelScreen.h | 15 +--- .../ui-lvgl/ConversationScreen.h | 26 +------ examples/companion_radio/ui-lvgl/NavMap.h | 22 +----- .../companion_radio/ui-lvgl/RadioScreen.h | 27 +------ examples/companion_radio/ui-lvgl/UITask.cpp | 73 ++++++++++++------- examples/companion_radio/ui-lvgl/WifiScreen.h | 12 +-- .../companion_radio/ui-new/LocatorScreen.h | 4 +- .../companion_radio/ui-new/MessagesScreen.h | 12 +-- .../companion_radio/ui-new/NearbyScreen.h | 2 +- 12 files changed, 70 insertions(+), 199 deletions(-) diff --git a/docs/development/ui-core.md b/docs/development/ui-core.md index 3220c7f9..5ada9d95 100644 --- a/docs/development/ui-core.md +++ b/docs/development/ui-core.md @@ -110,7 +110,7 @@ call the same ones, so e.g. "marking a channel read" has one implementation. ### 5. Declarative settings schema -`ui-core/SettingsSchema.h` holds four pages -- navigation (trail, live share, arrival alert, units), display & power (brightness, screen off, wake on message, lock screen, battery display, battery shutdown, GPS power saving, time zone, 12-hour clock, clock seconds) messages & contacts (DM resends, contact expiry, favourites first) and sound (volume, the sound for direct messages / channels / adverts, advert sound scope). An entry is a one-byte index by default, or a `values` table over a 1/2/4-byte field (seconds, millivolts, an hour offset, `{1, 0}` for a field stored inverted); side effects run through `UiCore` (`applyGpsInterval()`, `host()->applyDisplayPrefs()`). ui-lvgl renders each page; ui-new still uses its own screens. Radio settings are not schema rows (preset list, typed frequency, radio side effects): `ui-core/RadioControl.h` applies them (`applyParams`, `applyTxPower`, `applyApc`, presets = built-ins + user slots) for both ui-new's Settings > Radio and ui-lvgl's Radio screen. Channel management is `ui-core/ChannelControl.h` (`chanctl::`): add Public / Hashtag / Private (passphrase or 32-hex key), rename, delete, notifications / favourite / scope, and `onRemoved()` -- the cleanup of every channel-keyed pref, run by `UiCore::onChannelRemoved` whoever deleted the channel (this device or the app); contacts get the same through `UiCore::cleanupContactPrefs`. Favourites dial slots: `ui-core/Favourites.h` (`pinContact` / `pinChannel` move an entry that is already on another slot; the slot's name is `contactctl::favName`). The schema's display page also carries the lock screen (`auto_lock`) and the 12-hour clock, which ui-lvgl implements (a slide-to-unlock card after the screen turns off). Remote admin is `ui-core/AdminSession.h` (`UiCore::admin`): login with the saved password (forgotten on a wrong password or a silent timeout), CLI round trips with timeouts, the field table (`admin::`), typed values; results come out one at a time through `take()`. `UiCore` offers login answers and CLI replies to it first; only room logins it wasn't waiting for reach the frontend. ui-new's Tools > Admin and ui-lvgl's Admin screen are views over it. The bot's settings are a table too, `ui-core/BotConfig.h` (`botcfg::`: tabs, rows, the NodePrefs flag / text / hour behind each, reply placeholders); ui-new's Tools > Bot and ui-lvgl's Bot screen render it, the bot itself runs in MyMesh. Room servers: `ui-core/RoomSessions.h` (`UiCore::rooms`) keeps who is logged in this session, the one login in flight (with a timeout) and the saved passwords (saved on success, forgotten on failure); `open(contact)` says whether to go straight in, wait for a background login or ask for a password, and outcomes come out through `take()`. Per-contact helpers are `ui-core/ContactControl.h` (`contactctl::`: DM notification / melody overrides, favourite flag, the `@[name] ` reply prefix, splitting a room post into author and text, naming a message's path hops). ui-new's Messages and ui-lvgl's Chats / thread use both. Diagnostics rows are `ui-core/Diagnostics.h` (`diag::`: live counters, system card, font card, counter reset), shown by ui-new's Tools > Diagnostics and ui-lvgl's Settings > Diagnostics. Auto-advert is a schema row (`advert_auto_interval_sec`, sent by MyMesh); ui-new keeps its Tools > Auto-Advert screen over the same pref. The compass (ui-lvgl Home > Compass, ui-new Tools > Compass) reads `UiCore::course`. Saving / deleting the user's radio presets is `radioctl::saveUserPreset` / `deleteUserPreset` (ui-new's preset picker and ui-lvgl's Radio > My presets); the scope list is edited through MyMesh (`addScope` / `renameScope` / `removeScope` / `setDefaultScope`), which ui-lvgl's Radio > Scopes and ui-new's Settings > Radio > Scope call directly. The battery percentage (LiPo curve, empty at the shutdown voltage) and the display mode are `ui-core/Battery.h`, used by both status bars; the schema carries Battery display and Clock seconds. The companion's own repeater mode is `ui-core/RepeaterControl.h` (`rptctl::`: on / off with its radio, RX power-save and APC side effects, Current / Custom network with the profile seeded from the chat params, the profile's preset, the filters' ranges and labels, the extra scopes mask); ui-new's Tools > Repeater and ui-lvgl's Settings > Repeater use it, MyMesh does the relaying. Sound is `ui-core/SoundControl.h` (`soundctl::`: the On / Off / Auto mode over `buzzer_quiet` + `buzzer_auto`, the volume with its preview, the built-in sounds, the two user melodies as an editable note list) and `ui-core/SoundNotifier.h` (which sound a message / advert plays: global choice, per-contact / per-channel alert and melody overrides); the schema's Sound page holds the volume and the PLAYS FOR choices, the channel melody override is `chanctl::melody`. Both frontends drive a `genericBuzzer`: a PWM piezo on L1, the ES8311 codec on the L2 (`BUZZER_I2S`: an audio task synthesises the notes and counts samples, the board powers the amp through `buzzerAmpPower()`). Outgoing text is `ui-core/MessageText.h` (`msgtext::`: the ten quick messages, the placeholders on offer -- `{loc}`, `{time}`, `{batt}`, live sensor readings -- and expanding them at send time, keeping a reply's `@[name] ` prefix as typed); ui-new's Messages / keyboard and ui-lvgl's compose "+" use it. `UiCore::markAllRead()` clears every unread counter; `MyMesh::advertFlood()` (formerly sim-only) sends the self-advert through repeaters, `advert()` zero-hop. +`ui-core/SettingsSchema.h` holds four pages -- navigation (trail, live share, arrival alert, units), display & power (brightness, screen off, wake on message, lock screen, battery display, battery shutdown, GPS power saving, time zone, 12-hour clock, clock seconds) messages & contacts (DM resends, contact expiry, favourites first) and sound (volume, the sound for direct messages / channels / adverts, advert sound scope). An entry is a one-byte index by default, or a `values` table over a 1/2/4-byte field (seconds, millivolts, an hour offset, `{1, 0}` for a field stored inverted); side effects run through `UiCore` (`applyGpsInterval()`, `host()->applyDisplayPrefs()`). ui-lvgl renders each page; ui-new still uses its own screens. Radio settings are not schema rows (preset list, typed frequency, radio side effects): `ui-core/RadioControl.h` applies them (`applyParams`, `applyTxPower`, `applyApc`, presets = built-ins + user slots) for both ui-new's Settings > Radio and ui-lvgl's Radio screen. Channel management is `ui-core/ChannelControl.h` (`chanctl::`): add Public / Hashtag / Private (passphrase or 32-hex key), rename, delete, notifications / favourite / scope, and `onRemoved()` -- the cleanup of every channel-keyed pref, run by `UiCore::onChannelRemoved` whoever deleted the channel (this device or the app); contacts get the same through `UiCore::cleanupContactPrefs`. Favourites dial slots: `ui-core/Favourites.h` (`pinContact` / `pinChannel` move an entry that is already on another slot; the slot's name is `contactctl::favName`). The schema's display page also carries the lock screen (`auto_lock`) and the 12-hour clock, which ui-lvgl implements (a slide-to-unlock card after the screen turns off). Remote admin is `ui-core/AdminSession.h` (`UiCore::admin`): login with the saved password (forgotten on a wrong password or a silent timeout), CLI round trips with timeouts, the field table (`admin::`), typed values; results come out one at a time through `take()`. `UiCore` offers login answers and CLI replies to it first; only room logins it wasn't waiting for reach the frontend. ui-new's Tools > Admin and ui-lvgl's Admin screen are views over it. The bot's settings are a table too, `ui-core/BotConfig.h` (`botcfg::`: tabs, rows, the NodePrefs flag / text / hour behind each, reply placeholders); ui-new's Tools > Bot and ui-lvgl's Bot screen render it, the bot itself runs in MyMesh. Room servers: `ui-core/RoomSessions.h` (`UiCore::rooms`) keeps who is logged in this session, the one login in flight (with a timeout) and the saved passwords (saved on success, forgotten on failure); `open(contact)` says whether to go straight in, wait for a background login or ask for a password, and outcomes come out through `take()`. Per-contact helpers are `ui-core/ContactControl.h` (`contactctl::`: DM notification / melody overrides, favourite flag, the `@[name] ` reply prefix, splitting a room post into author and text, naming a message's path hops). ui-new's Messages and ui-lvgl's Chats / thread use both. Diagnostics rows are `ui-core/Diagnostics.h` (`diag::`: live counters, system card, font card, counter reset), shown by ui-new's Tools > Diagnostics and ui-lvgl's Settings > Diagnostics. Auto-advert is a schema row (`advert_auto_interval_sec`, sent by MyMesh); ui-new keeps its Tools > Auto-Advert screen over the same pref. The compass (ui-lvgl Home > Compass, ui-new Tools > Compass) reads `UiCore::course`. Saving / deleting the user's radio presets is `radioctl::saveUserPreset` / `deleteUserPreset` (ui-new's preset picker and ui-lvgl's Radio > My presets); the scope list is edited through MyMesh (`addScope` / `renameScope` / `removeScope` / `setDefaultScope`), which ui-lvgl's Radio > Scopes and ui-new's Settings > Radio > Scope call directly. The battery percentage (LiPo curve, empty at the shutdown voltage) and the display mode are `ui-core/Battery.h`, used by both status bars; the schema carries Battery display and Clock seconds. The companion's own repeater mode is `ui-core/RepeaterControl.h` (`rptctl::`: on / off with its radio, RX power-save and APC side effects, Current / Custom network with the profile seeded from the chat params, the profile's preset, the filters' ranges and labels, the extra scopes mask); ui-new's Tools > Repeater and ui-lvgl's Settings > Repeater use it, MyMesh does the relaying. Sound is `ui-core/SoundControl.h` (`soundctl::`: the On / Off / Auto mode over `buzzer_quiet` + `buzzer_auto`, the volume with its preview, the built-in sounds, the two user melodies as an editable note list) and `ui-core/SoundNotifier.h` (which sound a message / advert plays: global choice, per-contact / per-channel alert and melody overrides); the schema's Sound page holds the volume and the PLAYS FOR choices, the channel melody override is `chanctl::melody`. Both frontends drive a `genericBuzzer`: a PWM piezo on L1, the ES8311 codec on the L2 (`BUZZER_I2S`: an audio task synthesises the notes and counts samples, the board powers the amp through `buzzerAmpPower()`). Outgoing text is `ui-core/MessageText.h` (`msgtext::`: the ten quick messages, the placeholders on offer -- `{loc}`, `{time}`, `{batt}`, live sensor readings -- and expanding them at send time, keeping a reply's `@[name] ` prefix as typed); ui-new's Messages / keyboard and ui-lvgl's compose "+" use it. `UiCore::markAllRead()` clears every unread counter; `MyMesh::advertFlood()` (formerly sim-only) sends the self-advert through repeaters, `advert()` zero-hop. ui-lvgl's screens share their widgets from `UITask.cpp`: `segmented()` (a one-of-N button row; `radioview::rowSegmented` puts one at the right of a settings row), `textField()` (a one-line field padded to exactly one line), `listRow`, `switchRow`, `headerButton`. Replaces the per-item code in `SettingsScreen`: diff --git a/examples/companion_radio/ui-lvgl/AdminScreen.h b/examples/companion_radio/ui-lvgl/AdminScreen.h index 78ac1dfe..72b74bc3 100644 --- a/examples/companion_radio/ui-lvgl/AdminScreen.h +++ b/examples/companion_radio/ui-lvgl/AdminScreen.h @@ -110,22 +110,7 @@ void UITask::buildAdmin() { static const char* TABS[admin::TAB_COUNT + 1]; for (int i = 0; i < admin::TAB_COUNT; i++) TABS[i] = admin::TAB_LABELS[i]; TABS[admin::TAB_COUNT] = ""; - s_tabs = lv_buttonmatrix_create(body); - lv_buttonmatrix_set_map(s_tabs, TABS); - lv_buttonmatrix_set_button_ctrl_all(s_tabs, LV_BUTTONMATRIX_CTRL_CHECKABLE); - lv_buttonmatrix_set_one_checked(s_tabs, true); - lv_buttonmatrix_set_button_ctrl(s_tabs, s_tab, LV_BUTTONMATRIX_CTRL_CHECKED); - lv_obj_set_size(s_tabs, LV_PCT(100), 34); - lv_obj_set_style_pad_all(s_tabs, 0, 0); - lv_obj_set_style_pad_column(s_tabs, 4, 0); - lv_obj_set_style_bg_opa(s_tabs, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(s_tabs, 0, 0); - lv_obj_set_style_bg_color(s_tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS); - lv_obj_set_style_bg_color(s_tabs, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); - lv_obj_set_style_text_color(s_tabs, lv_color_hex(theme::TEXT), LV_PART_ITEMS); - lv_obj_set_style_text_font(s_tabs, THEME_FONT_SMALL, LV_PART_ITEMS); - lv_obj_set_style_shadow_width(s_tabs, 0, LV_PART_ITEMS); - lv_obj_set_style_radius(s_tabs, 8, LV_PART_ITEMS); + s_tabs = segmented(body, TABS, s_tab, LV_PCT(100), 34, theme::SURFACE); lv_obj_add_event_cb(s_tabs, onAdminTab, LV_EVENT_VALUE_CHANGED, NULL); s_status = label(body, "", THEME_FONT_SMALL, theme::ACCENT); @@ -307,22 +292,7 @@ void UITask::adminValuePopup() { int per_row = 1; int sel = radioChoices(*f, S, per_row); int rows = f->kind == admin::K_RADIO_BW ? 2 : 1; - s_choice = lv_buttonmatrix_create(panel); - lv_buttonmatrix_set_map(s_choice, s_map); - lv_buttonmatrix_set_button_ctrl_all(s_choice, LV_BUTTONMATRIX_CTRL_CHECKABLE); - lv_buttonmatrix_set_one_checked(s_choice, true); - lv_buttonmatrix_set_button_ctrl(s_choice, sel, LV_BUTTONMATRIX_CTRL_CHECKED); - lv_obj_set_size(s_choice, LV_PCT(100), rows * 34 + (rows - 1) * 4); - lv_obj_set_style_pad_all(s_choice, 0, 0); - lv_obj_set_style_pad_gap(s_choice, 4, 0); - lv_obj_set_style_bg_opa(s_choice, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(s_choice, 0, 0); - lv_obj_set_style_bg_color(s_choice, lv_color_hex(theme::SURFACE), LV_PART_ITEMS); - lv_obj_set_style_bg_color(s_choice, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); - lv_obj_set_style_text_color(s_choice, lv_color_hex(theme::TEXT), LV_PART_ITEMS); - lv_obj_set_style_text_font(s_choice, THEME_FONT_SMALL, LV_PART_ITEMS); - lv_obj_set_style_shadow_width(s_choice, 0, LV_PART_ITEMS); - lv_obj_set_style_radius(s_choice, 8, LV_PART_ITEMS); + s_choice = segmented(panel, s_map, sel, LV_PCT(100), rows * 34 + (rows - 1) * 4, theme::SURFACE); lv_obj_add_event_cb(s_choice, onAdminChoice, LV_EVENT_VALUE_CHANGED, NULL); } if (f->isRadio()) { @@ -340,19 +310,11 @@ void UITask::adminTextPopup(const char* text, bool digits) { const admin::Field* f = S.currentField(); lv_obj_t* panel = navPopupPanel(f ? f->label : "Command", false); lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard - _nav_ta = lv_textarea_create(panel); - lv_textarea_set_one_line(_nav_ta, true); + _nav_ta = textField(panel); lv_textarea_set_max_length(_nav_ta, 150); if (digits) lv_textarea_set_accepted_chars(_nav_ta, "0123456789."); if (f && f->isCustom()) lv_textarea_set_placeholder_text(_nav_ta, "e.g. get advert.interval"); lv_textarea_set_text(_nav_ta, text); - lv_obj_set_size(_nav_ta, LV_PCT(100), 34); - lv_obj_set_style_border_width(_nav_ta, 1, 0); - lv_obj_set_style_pad_ver(_nav_ta, 6, 0); - lv_obj_set_style_pad_hor(_nav_ta, 10, 0); - lv_obj_set_scrollbar_mode(_nav_ta, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_style_border_color(_nav_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(_nav_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); _nav_kb = kb::create(_nav_overlay, _prefs); lv_obj_set_size(_nav_kb, LV_PCT(100), 124); diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index 437f26f8..ca8eecd0 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -57,26 +57,6 @@ void UITask::showBot() { buildBot(); } -static lv_obj_t* segmented(lv_obj_t* parent, const char** map, int sel, int w, int h) { - lv_obj_t* m = lv_buttonmatrix_create(parent); - lv_buttonmatrix_set_map(m, map); - lv_buttonmatrix_set_button_ctrl_all(m, LV_BUTTONMATRIX_CTRL_CHECKABLE); - lv_buttonmatrix_set_one_checked(m, true); - lv_buttonmatrix_set_button_ctrl(m, sel, LV_BUTTONMATRIX_CTRL_CHECKED); - lv_obj_set_size(m, w, h); - lv_obj_set_style_pad_all(m, 0, 0); - lv_obj_set_style_pad_column(m, 4, 0); - lv_obj_set_style_bg_opa(m, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(m, 0, 0); - lv_obj_set_style_bg_color(m, lv_color_hex(theme::SURFACE_2), LV_PART_ITEMS); - lv_obj_set_style_bg_color(m, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); - lv_obj_set_style_text_color(m, lv_color_hex(theme::TEXT), LV_PART_ITEMS); - lv_obj_set_style_text_font(m, THEME_FONT_SMALL, LV_PART_ITEMS); - lv_obj_set_style_shadow_width(m, 0, LV_PART_ITEMS); - lv_obj_set_style_radius(m, 8, LV_PART_ITEMS); - return m; -} - void UITask::buildBot() { using namespace botview; lv_obj_t* body = newScreen("Bot", true); @@ -169,18 +149,10 @@ void UITask::botRow(int row) { bool reply = !botcfg::isTrigger(r.kind); lv_obj_t* panel = navPopupPanel(r.label, false); lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard - _nav_ta = lv_textarea_create(panel); - lv_textarea_set_one_line(_nav_ta, true); + _nav_ta = textField(panel); lv_textarea_set_max_length(_nav_ta, cap - 1); lv_textarea_set_text(_nav_ta, t); lv_textarea_set_placeholder_text(_nav_ta, botcfg::isTrigger(r.kind) ? "e.g. !hi, or * for any message" : "Reply text"); - lv_obj_set_size(_nav_ta, LV_PCT(100), 34); - lv_obj_set_style_border_width(_nav_ta, 1, 0); - lv_obj_set_style_pad_ver(_nav_ta, 6, 0); - lv_obj_set_style_pad_hor(_nav_ta, 10, 0); - lv_obj_set_scrollbar_mode(_nav_ta, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_style_border_color(_nav_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(_nav_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); if (reply) { // Placeholders the bot fills in, as buttons in place of the title (no diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index f25aa334..ded36803 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -204,21 +204,8 @@ void UITask::buildChannelEdit() { if (!edit) { static const char* TYPES[] = { "Public", "Hashtag", "Private", "" }; - s_types = lv_buttonmatrix_create(body); - lv_buttonmatrix_set_map(s_types, TYPES); - lv_buttonmatrix_set_button_ctrl_all(s_types, LV_BUTTONMATRIX_CTRL_CHECKABLE); - lv_buttonmatrix_set_one_checked(s_types, true); - lv_buttonmatrix_set_button_ctrl(s_types, s_type, LV_BUTTONMATRIX_CTRL_CHECKED); - lv_obj_set_size(s_types, LV_PCT(100), 38); - lv_obj_set_style_pad_all(s_types, 2, 0); - lv_obj_set_style_pad_column(s_types, 4, 0); - lv_obj_set_style_bg_color(s_types, lv_color_hex(theme::BG), 0); - lv_obj_set_style_border_width(s_types, 0, 0); - lv_obj_set_style_bg_color(s_types, lv_color_hex(theme::SURFACE), LV_PART_ITEMS); - lv_obj_set_style_bg_color(s_types, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); - lv_obj_set_style_text_color(s_types, lv_color_hex(theme::TEXT), LV_PART_ITEMS); + s_types = segmented(body, TYPES, s_type, LV_PCT(100), 38, theme::SURFACE); lv_obj_set_style_text_font(s_types, THEME_FONT_BODY, LV_PART_ITEMS); - lv_obj_set_style_shadow_width(s_types, 0, LV_PART_ITEMS); lv_obj_add_event_cb(s_types, onChanType, LV_EVENT_VALUE_CHANGED, NULL); } diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index 8be0c582..887c93a3 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -25,28 +25,6 @@ static bool s_login_wait = false; // open the room once this login answers static lv_obj_t* s_fav_btn = nullptr; static int s_msg = -1; // s_msg_meta index the message popup is about -static lv_obj_t* segmented(lv_obj_t* parent, const char** map, int sel, int w, lv_event_cb_t cb) { - lv_obj_t* seg = lv_buttonmatrix_create(parent); - lv_buttonmatrix_set_map(seg, map); - lv_buttonmatrix_set_button_ctrl_all(seg, LV_BUTTONMATRIX_CTRL_CHECKABLE); - lv_buttonmatrix_set_one_checked(seg, true); - lv_buttonmatrix_set_button_ctrl(seg, sel, LV_BUTTONMATRIX_CTRL_CHECKED); - lv_obj_set_size(seg, w, 36); - lv_obj_align(seg, LV_ALIGN_RIGHT_MID, -4, 0); - lv_obj_set_style_pad_all(seg, 0, 0); - lv_obj_set_style_pad_column(seg, 3, 0); - lv_obj_set_style_bg_opa(seg, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(seg, 0, 0); - lv_obj_set_style_bg_color(seg, lv_color_hex(theme::SURFACE_2), LV_PART_ITEMS); - lv_obj_set_style_bg_color(seg, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); - lv_obj_set_style_text_color(seg, lv_color_hex(theme::TEXT), LV_PART_ITEMS); - lv_obj_set_style_text_font(seg, THEME_FONT_SMALL, LV_PART_ITEMS); - lv_obj_set_style_shadow_width(seg, 0, LV_PART_ITEMS); - lv_obj_set_style_radius(seg, 8, LV_PART_ITEMS); - lv_obj_add_event_cb(seg, cb, LV_EVENT_VALUE_CHANGED, NULL); - return seg; -} - static lv_obj_t* actionRow(lv_obj_t* panel) { lv_obj_t* acts = lv_obj_create(panel); styleSurface(acts, theme::BG); @@ -186,10 +164,10 @@ void UITask::conversationMenu(const uint8_t* pub_key) { if (!room) { // alerts: three segments (a dropdown's list runs off screen this low) static const char* NOTIF[] = { "Default", "Muted", "Always", "" }; lv_obj_t* row = radioview::settingRow(panel, "Alerts", NULL); - segmented(row, NOTIF, contactctl::notif(_prefs, ci.id.pub_key), 200, onConvNotif); + radioview::rowSegmented(row, NOTIF, contactctl::notif(_prefs, ci.id.pub_key), 200, onConvNotif, 0); static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" }; row = radioview::settingRow(panel, "Sound", NULL); - segmented(row, MELODY, contactctl::melody(_prefs, ci.id.pub_key), 200, onConvMelody); + radioview::rowSegmented(row, MELODY, contactctl::melody(_prefs, ci.id.pub_key), 200, onConvMelody, 0); } else { bool in = _core->rooms.isLoggedIn(ci.id.pub_key); lv_obj_t* st = label(panel, in ? LV_SYMBOL_OK " Logged in: you can post" diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index 4726530c..f4aa282f 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -642,16 +642,8 @@ void UITask::navRenamePopup(int idx) { _nav_wp = idx; lv_obj_t* panel = navPopupPanel("Waypoint name", false); lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard - _nav_ta = lv_textarea_create(panel); - lv_textarea_set_one_line(_nav_ta, true); + _nav_ta = textField(panel); lv_textarea_set_text(_nav_ta, _core->waypoints.at(idx).label); - lv_obj_set_size(_nav_ta, LV_PCT(100), 34); - lv_obj_set_style_border_width(_nav_ta, 1, 0); - lv_obj_set_style_pad_ver(_nav_ta, 6, 0); - lv_obj_set_style_pad_hor(_nav_ta, 10, 0); - lv_obj_set_scrollbar_mode(_nav_ta, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_style_border_color(_nav_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(_nav_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor lv_obj_add_event_cb(_nav_ta, navmap::onLabelInsert, LV_EVENT_INSERT, NULL); @@ -848,7 +840,7 @@ void UITask::navToolsPopup() { for (int i = 0; i < the_mesh.getNumContacts() && favs < 16 && navmap::s_share_n < navmap::SHARE_TARGETS; i++) { ContactInfo c; if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue; - if (c.type != ADV_TYPE_CHAT || !(c.flags & 0x01)) continue; + if (c.type != ADV_TYPE_CHAT || !contactctl::favourite(c)) continue; if (_prefs->loc_share_target_type == 1 && memcmp(_prefs->loc_share_dm_prefix, c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) sel = navmap::s_share_n; navmap::s_share_kind[navmap::s_share_n] = 1; @@ -1101,16 +1093,8 @@ void UITask::navCoordsPopup() { if (_core->waypoints.full()) { showToast("Waypoints full (16)"); return; } lv_obj_t* panel = navPopupPanel("Add by coordinates", false); lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard - _nav_ta = lv_textarea_create(panel); - lv_textarea_set_one_line(_nav_ta, true); + _nav_ta = textField(panel); lv_textarea_set_placeholder_text(_nav_ta, "50.06142, 19.93721 Name"); - lv_obj_set_size(_nav_ta, LV_PCT(100), 34); - lv_obj_set_style_border_width(_nav_ta, 1, 0); - lv_obj_set_style_pad_ver(_nav_ta, 6, 0); - lv_obj_set_style_pad_hor(_nav_ta, 10, 0); - lv_obj_set_scrollbar_mode(_nav_ta, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_style_border_color(_nav_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(_nav_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor _nav_kb = kb::create(_nav_overlay, _prefs); diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h index 90374045..2a867cd7 100644 --- a/examples/companion_radio/ui-lvgl/RadioScreen.h +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -52,23 +52,8 @@ static lv_obj_t* rowDropdown(lv_obj_t* row, const char* opts, int sel, int width // A few choices side by side at the right of a row (a dropdown's list can run // off the bottom of a popup). `map` ends with "". static lv_obj_t* rowSegmented(lv_obj_t* row, const char** map, int sel, int width, lv_event_cb_t cb, uintptr_t which) { - lv_obj_t* seg = lv_buttonmatrix_create(row); - lv_buttonmatrix_set_map(seg, map); - lv_buttonmatrix_set_button_ctrl_all(seg, LV_BUTTONMATRIX_CTRL_CHECKABLE); - lv_buttonmatrix_set_one_checked(seg, true); - lv_buttonmatrix_set_button_ctrl(seg, sel, LV_BUTTONMATRIX_CTRL_CHECKED); - lv_obj_set_size(seg, width, 36); + lv_obj_t* seg = segmented(row, map, sel, width, 36); lv_obj_align(seg, LV_ALIGN_RIGHT_MID, -4, 0); - lv_obj_set_style_pad_all(seg, 0, 0); - lv_obj_set_style_pad_column(seg, 3, 0); - lv_obj_set_style_bg_opa(seg, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(seg, 0, 0); - lv_obj_set_style_bg_color(seg, lv_color_hex(theme::SURFACE_2), LV_PART_ITEMS); - lv_obj_set_style_bg_color(seg, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); - lv_obj_set_style_text_color(seg, lv_color_hex(theme::TEXT), LV_PART_ITEMS); - lv_obj_set_style_text_font(seg, THEME_FONT_SMALL, LV_PART_ITEMS); - lv_obj_set_style_shadow_width(seg, 0, LV_PART_ITEMS); - lv_obj_set_style_radius(seg, 8, LV_PART_ITEMS); lv_obj_add_event_cb(seg, cb, LV_EVENT_VALUE_CHANGED, (void*)which); return seg; } @@ -206,20 +191,12 @@ void UITask::radioFreqPopup(bool repeater) { char t[48]; snprintf(t, sizeof(t), "Frequency, MHz (%.0f - %.0f)", lo, hi); label(panel, t, THEME_FONT_BODY, theme::TEXT); - s_ta = lv_textarea_create(panel); - lv_textarea_set_one_line(s_ta, true); + s_ta = textField(panel); lv_textarea_set_accepted_chars(s_ta, "0123456789."); lv_textarea_set_max_length(s_ta, 10); char fs[16]; snprintf(fs, sizeof(fs), "%.3f", repeater ? _prefs->repeater_freq : _prefs->freq); lv_textarea_set_text(s_ta, fs); - lv_obj_set_size(s_ta, LV_PCT(100), 34); - lv_obj_set_style_border_width(s_ta, 1, 0); - lv_obj_set_style_pad_ver(s_ta, 6, 0); - lv_obj_set_style_pad_hor(s_ta, 10, 0); - lv_obj_set_scrollbar_mode(s_ta, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_style_border_color(s_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(s_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); lv_obj_add_state(s_ta, LV_STATE_FOCUSED); lv_obj_t* kbd = kb::create(s_overlay, _prefs); lv_obj_set_size(kbd, LV_PCT(100), 124); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index a239a7de..2d403997 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -61,6 +61,48 @@ static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font return l; } +// A one-of-N choice: a row (or grid, with "\n" in the map) of checkable +// buttons, `sel` checked (-1: none). `item` is the unchecked button colour — +// SURFACE on a SURFACE_2 panel, SURFACE_2 on the page. +static lv_obj_t* segmented(lv_obj_t* parent, const char** map, int sel, int w, int h, + uint32_t item = theme::SURFACE_2) { + lv_obj_t* m = lv_buttonmatrix_create(parent); + lv_buttonmatrix_set_map(m, map); + lv_buttonmatrix_set_button_ctrl_all(m, LV_BUTTONMATRIX_CTRL_CHECKABLE); + lv_buttonmatrix_set_one_checked(m, true); + if (sel >= 0) lv_buttonmatrix_set_button_ctrl(m, sel, LV_BUTTONMATRIX_CTRL_CHECKED); + lv_obj_set_size(m, w, h); + lv_obj_set_style_pad_all(m, 0, 0); + lv_obj_set_style_pad_gap(m, 4, 0); + lv_obj_set_style_bg_opa(m, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(m, 0, 0); + lv_obj_set_style_bg_color(m, lv_color_hex(item), LV_PART_ITEMS); + lv_obj_set_style_bg_color(m, lv_color_hex(theme::ACCENT_DIM), LV_PART_ITEMS | LV_STATE_CHECKED); + lv_obj_set_style_text_color(m, lv_color_hex(theme::TEXT), LV_PART_ITEMS); + lv_obj_set_style_text_font(m, THEME_FONT_SMALL, LV_PART_ITEMS); + lv_obj_set_style_shadow_width(m, 0, LV_PART_ITEMS); + lv_obj_set_style_radius(m, 8, LV_PART_ITEMS); + return m; +} + +// A one-line text field, full width. The theme's padding leaves less than one +// line inside 34 px, which makes the field scroll vertically (text jumps as +// it's typed), so it's padded to fit exactly one body line: 34 = 2*1 border + +// 2*6 pad + 20. The theme draws the cursor only while FOCUSED. +static lv_obj_t* textField(lv_obj_t* parent, const char* placeholder = NULL) { + lv_obj_t* ta = lv_textarea_create(parent); + lv_textarea_set_one_line(ta, true); + if (placeholder) lv_textarea_set_placeholder_text(ta, placeholder); + lv_obj_set_size(ta, LV_PCT(100), 34); + lv_obj_set_style_border_width(ta, 1, 0); + lv_obj_set_style_pad_ver(ta, 6, 0); + lv_obj_set_style_pad_hor(ta, 10, 0); + lv_obj_set_scrollbar_mode(ta, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_border_color(ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_set_style_border_width(ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); + return ta; +} + // Unread badge: amber pill with a count, on the right edge of `parent`. static void badge(lv_obj_t* parent, int n, bool overflow) { if (n <= 0) return; @@ -79,16 +121,6 @@ static void badge(lv_obj_t* parent, int n, bool overflow) { lv_obj_center(l); } -// "12s" / "5m" / "3h" / "2d" since a unix timestamp. -static void formatAge(char* buf, size_t n, uint32_t ts) { - uint32_t now = rtc_clock.getCurrentTime(); - uint32_t d = (ts && now > ts) ? now - ts : 0; - if (d < 60) snprintf(buf, n, "%lus", (unsigned long)d); - else if (d < 3600) snprintf(buf, n, "%lum", (unsigned long)(d / 60)); - else if (d < 86400) snprintf(buf, n, "%luh", (unsigned long)(d / 3600)); - else snprintf(buf, n, "%lud", (unsigned long)(d / 86400)); -} - // Local time (NodePrefs::tz_offset_hours); false before the clock is set. static bool localTime(const NodePrefs* p, struct tm& out) { uint32_t now = rtc_clock.getCurrentTime(); @@ -1431,7 +1463,7 @@ void UITask::nodeAction(uint8_t action) { break; } case NODE_FAV: - if (the_mesh.setContactFavourite(e.pub_key, !e.fav)) { + if (contactctl::setFavourite(e.pub_key, !e.fav)) { e.fav = !e.fav; buildNode(); } @@ -1613,22 +1645,10 @@ void UITask::buildThread() { lv_obj_set_style_bg_color(more, lv_color_hex(theme::SURFACE), 0); lv_obj_add_event_cb(more, onComposeMore, LV_EVENT_CLICKED, NULL); lv_obj_center(label(more, LV_SYMBOL_PLUS, THEME_FONT_BODY, theme::TEXT)); - _compose_ta = lv_textarea_create(bar); - lv_textarea_set_one_line(_compose_ta, true); - lv_textarea_set_placeholder_text(_compose_ta, "Message"); + _compose_ta = textField(bar, "Message"); // FOCUSED while the keyboard is up lv_obj_add_event_cb(_compose_ta, onComposeInsert, LV_EVENT_INSERT, NULL); - lv_obj_set_size(_compose_ta, lv_display_get_horizontal_resolution(NULL) - 8 - 34 - 4, 34); // beside "+" + lv_obj_set_width(_compose_ta, lv_display_get_horizontal_resolution(NULL) - 8 - 34 - 4); // beside "+" lv_obj_align(_compose_ta, LV_ALIGN_RIGHT_MID, 0, 0); - // The theme's padding leaves less than one line inside 36 px, which makes - // the field scroll vertically (text jumps as it's typed). Pad so exactly one - // body line (20 px) fits: 34 = 2*1 border + 2*6 pad + 20. - lv_obj_set_style_border_width(_compose_ta, 1, 0); - lv_obj_set_style_pad_ver(_compose_ta, 6, 0); - lv_obj_set_style_pad_hor(_compose_ta, 10, 0); - lv_obj_set_scrollbar_mode(_compose_ta, LV_SCROLLBAR_MODE_OFF); - // Cursor: the theme draws it only while FOCUSED (set while the keyboard is up). - lv_obj_set_style_border_color(_compose_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(_compose_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); // In the body's flex column below the compose bar: showing it shrinks the // message list, so the text field stays visible just above the keys. @@ -1741,7 +1761,8 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, lv_obj_set_flex_flow(b, LV_FLEX_FLOW_COLUMN); char meta[32]; - formatAge(meta, sizeof(meta), ts); + uint32_t now = rtc_clock.getCurrentTime(); + geo::fmtAgeShort(meta, sizeof(meta), now, ts ? ts : now); // "12s" / "5m" / "3h" / "2d" // Channel messages: "Sender 5m" on one line above the text, which keeps // the bubble two lines tall -- matters with the keyboard up. diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index bc6bdecb..fb660084 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -19,18 +19,8 @@ static void onWifiKb(lv_event_t* e) { } static lv_obj_t* wifiField(lv_obj_t* parent, const char* placeholder, bool password) { - lv_obj_t* ta = lv_textarea_create(parent); - lv_textarea_set_one_line(ta, true); - lv_textarea_set_placeholder_text(ta, placeholder); + lv_obj_t* ta = textField(parent, placeholder); lv_textarea_set_password_mode(ta, password); - lv_obj_set_width(ta, LV_PCT(100)); - lv_obj_set_height(ta, 34); // one 20 px line: 2*1 border + 2*6 pad + 20 (as the compose field) - lv_obj_set_style_border_width(ta, 1, 0); - lv_obj_set_style_pad_ver(ta, 6, 0); - lv_obj_set_style_pad_hor(ta, 10, 0); - lv_obj_set_scrollbar_mode(ta, LV_SCROLLBAR_MODE_OFF); - lv_obj_set_style_border_color(ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); - lv_obj_set_style_border_width(ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); lv_obj_add_event_cb(ta, onWifiField, LV_EVENT_CLICKED, NULL); return ta; } diff --git a/examples/companion_radio/ui-new/LocatorScreen.h b/examples/companion_radio/ui-new/LocatorScreen.h index 97ae041d..e83fa609 100644 --- a/examples/companion_radio/ui-new/LocatorScreen.h +++ b/examples/companion_radio/ui-new/LocatorScreen.h @@ -202,13 +202,13 @@ public: for (int idx = 0; _target_n < TARGET_MAX; idx++) { ContactInfo c; if (!the_mesh.getContactByIdx(idx, c)) break; - if (!(c.flags & 0x01)) continue; + if (!contactctl::favourite(c)) continue; addPersonTarget(c.id.pub_key, c.name, /*require_position=*/false, true); } for (int idx = 0; _target_n < TARGET_MAX; idx++) { ContactInfo c; if (!the_mesh.getContactByIdx(idx, c)) break; - addPersonTarget(c.id.pub_key, c.name, /*require_position=*/true, (c.flags & 0x01) != 0); + addPersonTarget(c.id.pub_key, c.name, /*require_position=*/true, contactctl::favourite(c)); } WaypointStore& wp = _task->waypoints(); for (int i = 0; i < wp.count() && _target_n < TARGET_MAX; i++) { diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index 0d0db2ce..1e02f0af 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -679,8 +679,8 @@ class MessagesScreen : public UIScreen { // Flip a contact's/room's favourite flag and refresh the menu label. The list // rebuild is deferred to menu close (see the channel Fav toggle for why). void toggleContactFav(const ContactInfo& ci) { - bool now_fav = !(ci.flags & 0x01); - if (!the_mesh.setContactFavourite(ci.id.pub_key, now_fav)) return; + bool now_fav = !contactctl::favourite(ci); + if (!contactctl::setFavourite(ci.id.pub_key, now_fav)) return; snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), now_fav ? "Fav: ON" : "Fav: OFF"); } @@ -712,7 +712,7 @@ class MessagesScreen : public UIScreen { for (int i = 0; i < total; i++) { if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue; if (c.type != (rooms ? ADV_TYPE_ROOM : ADV_TYPE_CHAT)) continue; - bool fav = (c.flags & 0x01) != 0; + bool fav = contactctl::favourite(c); if (fav_only && !fav) continue; uint8_t k = rooms ? 0 : _history.dmHistCountForContact(c.id.pub_key); if (k > 127) k = 127; @@ -1234,7 +1234,7 @@ public: uint8_t dm_unread = _task->getDMUnread(c.id.pub_key); bool dm_overflow = dm_unread > 0 && _task->getDMUnreadOverflow(c.id.pub_key); int bw = dm_unread > 0 ? display.unreadBadgeWidth(dm_unread, dm_overflow) + 2 : 0; - int sw = (c.flags & 0x01) ? favStarWidth(display) : 0; + int sw = contactctl::favourite(c) ? favStarWidth(display) : 0; // See the channel/DM history bodies' identical comment: suppress this // row's own marquee while a context menu covers it, so the two don't // fight over DisplayDriver's single shared marquee slot. @@ -1963,7 +1963,7 @@ public: bool have = the_mesh.getContactByIdx(_sorted[_contact_sel], ci); bool logged_in = have && isRoomLoggedIn(ci.id.pub_key); snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), - (have && (ci.flags & 0x01)) ? "Fav: ON" : "Fav: OFF"); + (have && contactctl::favourite(ci)) ? "Fav: ON" : "Fav: OFF"); { int pinned_slot = have ? _task->findFavouriteSlot(ci.id.pub_key) : -1; if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1); else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial"); } @@ -1987,7 +1987,7 @@ public: if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1); else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial"); snprintf(_ctx_fav_item, sizeof(_ctx_fav_item), - (ci.flags & 0x01) ? "Fav: ON" : "Fav: OFF"); + contactctl::favourite(ci) ? "Fav: ON" : "Fav: OFF"); _ctx_menu.begin("Contact options", 3); _ctx_menu.addItem("Mark as read"); _ctx_menu.addValueItem(_ctx_notif_item); diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 96639f0f..b1d0c61a 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -356,7 +356,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel { const Entry* e = selected(); if (!e || !e->has_key) return; bool now_fav = !e->fav; - if (!the_mesh.setContactFavourite(e->pub_key, now_fav)) return; + if (!contactctl::setFavourite(e->pub_key, now_fav)) return; snprintf(_fav_label, sizeof(_fav_label), now_fav ? "Fav: ON" : "Fav: OFF"); refreshKeepingSelection(); }