From 5b40e91df3f21810dfe2ac9fd69c0c6598cc28fa Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 27 May 2026 17:33:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(contacts):=20channel=20favourites=20?= =?UTF-8?q?=E2=80=94=20filter=20in=20Settings=20+=20context=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add per-channel favourite marking and a Settings filter (all/fav) for the channel list, symmetric with the existing DM and Room filters. NodePrefs: - ch_fav_bitmask (uint64_t): bit i = channel i is marked favourite - ch_fav_only (uint8_t): 0=show all channels, 1=favourites only - SCHEMA_SENTINEL bumped to 0xC0DE0005 DataStore: save/load both new fields before the sentinel. SettingsScreen: new CH_FILTER item ('Channels: all/fav') in the Contacts section, positioned between DM_FILTER and ROOM_FILTER. QuickMsgScreen: - buildChannelList() skips non-favourite channels when ch_fav_only=1 - Channel context menu gains a 4th option 'Fav'/'Unfav' that toggles ch_fav_bitmask for the selected channel and rebuilds the list Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/NodePrefs.h | 5 +++- .../companion_radio/ui-new/QuickMsgScreen.h | 26 +++++++++++++++---- .../companion_radio/ui-new/SettingsScreen.h | 11 +++++++- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index e0fef75a..604903e9 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -105,11 +105,14 @@ struct NodePrefs { // persisted to file uint8_t trail_min_delta_idx; // indexes TrailStore::minDeltaMeters (5/10/25/100 m) uint8_t trail_units_idx; // indexes TrailStore::unitLabel (km/h, mph, min/km, min/mi) + uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite + uint8_t ch_fav_only; // 0=show all channels (default), 1=show favourites only + // Tail sentinel written at the end of /new_prefs. Bump the low byte when // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // older saves are detected on load and skipped (zero-init defaults kept). // High 24 bits identify the file format; low byte is the schema revision. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE0004; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0005; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 4a63d232..fc4d16d6 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -47,6 +47,7 @@ class QuickMsgScreen : public UIScreen { char _ctx_notif_item[22]; char _ctx_melody_item[20]; char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)" + char _ctx_ch_fav_item[12]; // "Fav" or "Unfav" char _pin_slot_labels[NodePrefs::FAVOURITES_COUNT][22]; // per-slot picker labels bool _pin_picker_active; // true while the slot-picker submenu is open bool _dm_direct_entry; // entered DM_HIST via Favourites shortcut; CANCEL returns home @@ -271,12 +272,14 @@ class QuickMsgScreen : public UIScreen { } void buildChannelList() { + NodePrefs* p = _task->getNodePrefs(); + bool fav_only = (p && p->ch_fav_only); _num_channels = 0; for (int i = 0; i < MAX_GROUP_CHANNELS; i++) { ChannelDetails ch; - if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { - _channel_indices[_num_channels++] = (uint8_t)i; - } + if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') continue; + if (fav_only && !(p->ch_fav_bitmask & (1ULL << i))) continue; + _channel_indices[_num_channels++] = (uint8_t)i; } } @@ -1083,10 +1086,19 @@ public: uint8_t nstate = chNotifState(ch_idx); setChNotifState(ch_idx, (nstate + 1) % 3); _ctx_dirty = true; - } else { + } else if (_ctx_menu.selectedIndex() == 2) { uint8_t slot = chNotifMelody(ch_idx); setChNotifMelody(ch_idx, (slot + 1) % 3); _ctx_dirty = true; + } else { + // Toggle favourite + NodePrefs* p2 = _task->getNodePrefs(); + if (p2) { + p2->ch_fav_bitmask ^= (1ULL << ch_idx); + _ctx_dirty = true; + buildChannelList(); // refilter if ch_fav_only is active + if (_channel_sel >= _num_channels) _channel_sel = _num_channels > 0 ? _num_channels - 1 : 0; + } } } if (res != PopupMenu::NONE && _ctx_dirty) { @@ -1124,10 +1136,14 @@ public: { static const char* ML[] = { "global", "M1", "M2" }; snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", ML[chNotifMelody(ch_idx)]); } - _ctx_menu.begin("Channel options", 3); + { NodePrefs* p2 = _task->getNodePrefs(); + bool is_fav = p2 && (p2->ch_fav_bitmask & (1ULL << ch_idx)); + snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Unfav" : "Fav"); } + _ctx_menu.begin("Channel options", 4); _ctx_menu.addItem("Mark all read"); _ctx_menu.addItem(_ctx_notif_item); _ctx_menu.addItem(_ctx_melody_item); + _ctx_menu.addItem(_ctx_ch_fav_item); _ctx_dirty = false; return true; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 803d4ab4..dd609e98 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -57,7 +57,7 @@ class SettingsScreen : public UIScreen { TIMEZONE, LOW_BAT, // Contacts section - SECTION_CONTACTS, DM_FILTER, ROOM_FILTER, + SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, // Messages section SECTION_MESSAGES, MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4, @@ -533,6 +533,10 @@ class SettingsScreen : public UIScreen { display.print("DM"); display.setCursor(display.valCol(), y); display.print((p && p->dm_show_all) ? "all" : "fav"); + } else if (item == CH_FILTER) { + display.print("Channels"); + display.setCursor(display.valCol(), y); + display.print((p && p->ch_fav_only) ? "fav" : "all"); } else if (item == ROOM_FILTER) { display.print("Rooms"); display.setCursor(display.valCol(), y); @@ -783,6 +787,11 @@ public: _dirty = true; return true; } + if (_selected == CH_FILTER && p && (left || right || enter)) { + p->ch_fav_only = p->ch_fav_only ? 0 : 1; + _dirty = true; + return true; + } if (_selected == ROOM_FILTER && p && (left || right || enter)) { p->room_fav_only = p->room_fav_only ? 0 : 1; _dirty = true;