feat(ui): v1.13 — collapsible Settings, Favourites Dial, GPX trail, schema fix

- SettingsScreen: collapsible sections (Enter toggles); all collapsed by default;
  vis[] filtered list drives render and UP/DOWN/Enter navigation
- NodePrefs: bump SCHEMA_SENTINEL 0xC0DE0003 → 0xC0DE0004; reset trail_units_idx
  in mismatch handler (was corrupted on upgrade from 0003 saves)
- UITask: shorten "No contacts available" → "No fav contacts" (OLED alert overflow)
- README: document Favourites Dial, GPS Trail + GPX download instructions,
  Mark-all-read, collapsible Settings; update USB/BLE export note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-26 15:49:50 +02:00
parent 755525761e
commit 439c726f1e
5 changed files with 148 additions and 44 deletions

View File

@@ -327,6 +327,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (_prefs.home_pages_mask != 0) {
_prefs.home_pages_mask |= NodePrefs::HP_FAVOURITES;
}
// 0xC0DE0003 → 0xC0DE0004: trail_units_idx added after trail_min_delta_idx.
// Saves from 0xC0DE0003 have the sentinel bytes where trail_units_idx sits,
// so rd() picks up 0x03 (low byte of the old sentinel) — reset to default 0.
_prefs.trail_units_idx = 0;
}
file.close();

View File

@@ -109,7 +109,7 @@ struct NodePrefs { // persisted to file
// 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 = 0xC0DE0003;
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0004;
// 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

View File

@@ -69,6 +69,10 @@ class SettingsScreen : public UIScreen {
int _scroll;
int _visible; // items fitting on screen; updated each render, used by handleInput
bool _dirty;
uint8_t _collapsed = 0x7F; // bit N set = section N collapsed (Display=0..Messages=6)
static const int MAX_VIS = 60;
uint8_t _vis[MAX_VIS]; // filtered list of visible SettingItem values
int _vis_count = 0;
#if AUTO_OFF_MILLIS > 0
static const uint16_t AUTO_OFF_OPTS[5];
@@ -140,6 +144,38 @@ class SettingsScreen : public UIScreen {
return "";
}
int sectionIndex(int item) const {
if (item == SECTION_DISPLAY) return 0;
if (item == SECTION_SOUND) return 1;
if (item == SECTION_HOME_PAGES) return 2;
if (item == SECTION_RADIO) return 3;
if (item == SECTION_SYSTEM) return 4;
if (item == SECTION_CONTACTS) return 5;
if (item == SECTION_MESSAGES) return 6;
return -1;
}
int visIndexOf(int item) const {
for (int i = 0; i < _vis_count; i++)
if (_vis[i] == (uint8_t)item) return i;
return 0;
}
void buildVis() {
_vis_count = 0;
int cur_sec = -1;
bool cur_collapsed = false;
for (int i = 0; i < (int)Count; i++) {
if (isSection(i)) {
cur_sec++;
cur_collapsed = (_collapsed >> cur_sec) & 1;
if (_vis_count < MAX_VIS) _vis[_vis_count++] = (uint8_t)i;
} else if (!cur_collapsed && _vis_count < MAX_VIS) {
_vis[_vis_count++] = (uint8_t)i;
}
}
}
bool isHomePage(int item) const {
return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO ||
item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS ||
@@ -299,25 +335,20 @@ class SettingsScreen : public UIScreen {
return item - MSG_SLOT_0;
}
int nextSelectable(int from) const {
for (int i = from + 1; i < (int)Count; i++)
if (!isSection(i)) return i;
return from;
}
int prevSelectable(int from) const {
for (int i = from - 1; i >= 0; i--)
if (!isSection(i)) return i;
return from;
}
void renderItem(DisplayDriver& display, int item, int y) {
NodePrefs* p = _task->getNodePrefs();
if (isSection(item)) {
int si = sectionIndex(item);
bool collapsed = (_collapsed >> si) & 1;
bool sel = (item == _selected);
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y, display.width(), 1);
display.setCursor(2, y + 2);
display.drawSelectionRow(0, y - 1, display.width(), display.lineStep(), sel);
display.setCursor(0, y);
display.print(sel ? ">" : " ");
display.setCursor(display.getCharWidth() + 2, y);
display.print(collapsed ? "+" : "-");
display.print(" ");
display.print(sectionName(item));
return;
}
@@ -474,10 +505,18 @@ class SettingsScreen : public UIScreen {
public:
SettingsScreen(UITask* task)
: _task(task), _selected(AUTO_LOCK), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {}
: _task(task), _selected(SECTION_DISPLAY), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {
buildVis();
}
void markClean() { _dirty = false; }
void markClean() {
_dirty = false;
_collapsed = 0x7F;
_selected = SECTION_DISPLAY;
buildVis();
_scroll = 0;
}
int render(DisplayDriver& display) override {
display.setTextSize(1);
@@ -494,8 +533,8 @@ public:
display.drawTextCentered(display.width() / 2, 0, "SETTINGS");
display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH());
for (int i = 0; i < _visible && (_scroll + i) < SettingItem::Count; i++) {
renderItem(display, _scroll + i, start_y + i * item_h);
for (int i = 0; i < _visible && (_scroll + i) < _vis_count; i++) {
renderItem(display, _vis[_scroll + i], start_y + i * item_h);
}
// scroll indicators
@@ -504,7 +543,7 @@ public:
display.setCursor(display.width() - display.getCharWidth(), start_y);
display.print("^");
}
if (_scroll + _visible < SettingItem::Count) {
if (_scroll + _visible < _vis_count) {
display.setColor(DisplayDriver::LIGHT);
display.setCursor(display.width() - display.getCharWidth(), start_y + (_visible - 1) * item_h);
display.print("v");
@@ -533,18 +572,20 @@ public:
}
if (c == KEY_UP) {
int prev = prevSelectable(_selected);
if (prev != _selected) {
_selected = prev;
if (_selected < _scroll) _scroll = _selected;
int vi = visIndexOf(_selected);
if (vi > 0) {
vi--;
_selected = _vis[vi];
if (vi < _scroll) _scroll = vi;
}
return true;
}
if (c == KEY_DOWN) {
int next = nextSelectable(_selected);
if (next != _selected) {
_selected = next;
if (_selected >= _scroll + _visible) _scroll = _selected - _visible + 1;
int vi = visIndexOf(_selected);
if (vi + 1 < _vis_count) {
vi++;
_selected = _vis[vi];
if (vi >= _scroll + _visible) _scroll = vi - _visible + 1;
}
return true;
}
@@ -558,6 +599,16 @@ public:
bool left = (c == KEY_LEFT || c == KEY_PREV);
bool enter = (c == KEY_ENTER);
if (enter && isSection(_selected)) {
int si = sectionIndex(_selected);
_collapsed ^= (1 << si);
buildVis();
int vi = visIndexOf(_selected);
if (vi < _scroll) _scroll = vi;
if (_visible > 0 && vi >= _scroll + _visible) _scroll = vi - _visible + 1;
return true;
}
#if FEAT_BRIGHTNESS_SETTING
if (_selected == BRIGHTNESS) {
uint8_t lvl = _task->getBrightnessLevel();

View File

@@ -180,7 +180,7 @@ class HomeScreen : public UIScreen {
}
}
if (_pin_count == 0) {
_task->showAlert("No contacts available", 1000);
_task->showAlert("No fav contacts", 1000);
_pin_target_slot = -1;
return;
}