feat: home screen ordering — reorder all screens from Settings

NodePrefs: add page_order[11] (1-indexed bit-index, 0=end/legacy).
DataStore: persist page_order appended after use_lemon_font.

UITask/HomeScreen:
- bitToPage(): maps stable bit indices 0-10 to HomePage enum values
- buildVisibleOrder(): respects page_order when initialised, else
  falls back to default enum sequence
- navPage() and navigation dots use buildVisibleOrder()

SettingsScreen Home Pages section:
- Add Settings (bit 9) and Messages (bit 10) — show as "always"
- LEFT/RIGHT on any home page item moves it earlier/later in order
- ENTER toggles ON/OFF for pages that can be disabled
- Position number shown before label once custom order is active
- ensurePageOrderInit() seeds default order on first reorder action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-22 23:33:06 +02:00
co-authored by Claude Sonnet 4.6
parent 7519740d72
commit bb94a6f81f
4 changed files with 191 additions and 39 deletions
+4
View File
@@ -289,6 +289,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h)); file.read((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h));
if (file.available()) { if (file.available()) {
file.read((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font)); file.read((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font));
if (file.available()) {
file.read((uint8_t *)_prefs.page_order, sizeof(_prefs.page_order));
}
} }
} }
} }
@@ -383,6 +386,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.auto_lock, sizeof(_prefs.auto_lock)); file.write((uint8_t *)&_prefs.auto_lock, sizeof(_prefs.auto_lock));
file.write((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h)); file.write((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h));
file.write((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font)); file.write((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font));
file.write((uint8_t *)_prefs.page_order, sizeof(_prefs.page_order));
file.close(); file.close();
} }
+3
View File
@@ -79,4 +79,7 @@ struct NodePrefs { // persisted to file
static const int DM_MELODY_TABLE_MAX = 16; static const int DM_MELODY_TABLE_MAX = 16;
DmMelodyEntry dm_melody[DM_MELODY_TABLE_MAX]; DmMelodyEntry dm_melody[DM_MELODY_TABLE_MAX];
uint8_t use_lemon_font; // 0=default Adafruit font, 1=Lemon font (Unicode, pixel-accurate wrap) uint8_t use_lemon_font; // 0=default Adafruit font, 1=Lemon font (Unicode, pixel-accurate wrap)
// Home screen page order: each byte = bit-index+1 (see HP_* bit positions + 9=Settings, 10=Messages).
// 0 = end of list (also uninitialized legacy). hasCustomOrder iff page_order[0] in 1..11.
uint8_t page_order[11];
}; };
+123 -21
View File
@@ -30,6 +30,7 @@ class SettingsScreen : public UIScreen {
#if UI_SENSORS_PAGE == 1 #if UI_SENSORS_PAGE == 1
HOME_SENSORS, HOME_SENSORS,
#endif #endif
HOME_SETTINGS, HOME_QUICK_MSG,
HOME_TOOLS, HOME_SHUTDOWN, HOME_TOOLS, HOME_SHUTDOWN,
// Radio section // Radio section
SECTION_RADIO, SECTION_RADIO,
@@ -137,9 +138,9 @@ class SettingsScreen : public UIScreen {
} }
bool isHomePage(int item) const { bool isHomePage(int item) const {
return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO || return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO ||
item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS || item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS ||
item == HOME_SHUTDOWN item == HOME_SHUTDOWN || item == HOME_SETTINGS || item == HOME_QUICK_MSG
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
|| item == HOME_GPS || item == HOME_GPS
#endif #endif
@@ -155,41 +156,126 @@ class SettingsScreen : public UIScreen {
if (item == HOME_RADIO) return HP_RADIO; if (item == HOME_RADIO) return HP_RADIO;
if (item == HOME_BT) return HP_BLUETOOTH; if (item == HOME_BT) return HP_BLUETOOTH;
if (item == HOME_ADVERT) return HP_ADVERT; if (item == HOME_ADVERT) return HP_ADVERT;
if (item == HOME_TOOLS) return HP_TOOLS; if (item == HOME_TOOLS) return HP_TOOLS;
if (item == HOME_SHUTDOWN) return HP_SHUTDOWN; if (item == HOME_SHUTDOWN) return HP_SHUTDOWN;
// HOME_SETTINGS and HOME_QUICK_MSG have no mask bit — always visible
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
if (item == HOME_GPS) return HP_GPS; if (item == HOME_GPS) return HP_GPS;
#endif #endif
#if UI_SENSORS_PAGE == 1 #if UI_SENSORS_PAGE == 1
if (item == HOME_SENSORS) return HP_SENSORS; if (item == HOME_SENSORS) return HP_SENSORS;
#endif #endif
return 0; return 0;
} }
const char* homePageLabel(int item) const { const char* homePageLabel(int item) const {
if (item == HOME_CLOCK) return "Clock"; if (item == HOME_CLOCK) return "Clock";
if (item == HOME_RECENT) return "Recent"; if (item == HOME_RECENT) return "Recent";
if (item == HOME_RADIO) return "Radio"; if (item == HOME_RADIO) return "Radio";
if (item == HOME_BT) return "Bluetooth"; if (item == HOME_BT) return "Bluetooth";
if (item == HOME_ADVERT) return "Advert"; if (item == HOME_ADVERT) return "Advert";
if (item == HOME_TOOLS) return "Tools"; if (item == HOME_TOOLS) return "Tools";
if (item == HOME_SHUTDOWN) return "Shutdown"; if (item == HOME_SHUTDOWN) return "Shutdown";
if (item == HOME_SETTINGS) return "Settings";
if (item == HOME_QUICK_MSG) return "Messages";
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
if (item == HOME_GPS) return "GPS"; if (item == HOME_GPS) return "GPS";
#endif #endif
#if UI_SENSORS_PAGE == 1 #if UI_SENSORS_PAGE == 1
if (item == HOME_SENSORS) return "Sensors"; if (item == HOME_SENSORS) return "Sensors";
#endif #endif
return ""; return "";
} }
bool homePageVisible(int item, const NodePrefs* p) const { bool homePageVisible(int item, const NodePrefs* p) const {
if (item == HOME_SETTINGS || item == HOME_QUICK_MSG) return true;
uint16_t bit = homePageBit(item); uint16_t bit = homePageBit(item);
if (!bit) return false; if (!bit) return false;
uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : HP_ALL; uint16_t mask = (p && p->home_pages_mask) ? p->home_pages_mask : HP_ALL;
return (mask & bit) != 0; return (mask & bit) != 0;
} }
bool homePageToggleable(int item) const {
return item != HOME_SETTINGS && item != HOME_QUICK_MSG;
}
// Returns the bit-index (0-10) used in page_order for this SettingItem, or -1.
int homePageBitIndex(int item) const {
if (item == HOME_CLOCK) return 0;
if (item == HOME_RECENT) return 1;
if (item == HOME_RADIO) return 2;
if (item == HOME_BT) return 3;
if (item == HOME_ADVERT) return 4;
#if ENV_INCLUDE_GPS == 1
if (item == HOME_GPS) return 5;
#endif
#if UI_SENSORS_PAGE == 1
if (item == HOME_SENSORS) return 6;
#endif
if (item == HOME_TOOLS) return 7;
if (item == HOME_SHUTDOWN) return 8;
if (item == HOME_SETTINGS) return 9;
if (item == HOME_QUICK_MSG) return 10;
return -1;
}
// Returns 1-based position of item in page_order, or 0 if no custom order / not found.
int homePagePosition(int item, const NodePrefs* p) const {
if (!p || p->page_order[0] < 1 || p->page_order[0] > 11) return 0;
int bit = homePageBitIndex(item);
if (bit < 0) return 0;
for (int i = 0; i < 11; i++) {
uint8_t v = p->page_order[i];
if (v < 1 || v > 11) break;
if ((int)(v - 1) == bit) return i + 1;
}
return 0;
}
// Initialises page_order to the default display sequence if not already set.
void ensurePageOrderInit(NodePrefs* p) const {
if (!p) return;
if (p->page_order[0] >= 1 && p->page_order[0] <= 11) return;
// Default: CLOCK RECENT RADIO BT ADVERT [GPS] [SENSORS] SETTINGS TOOLS MESSAGES SHUTDOWN
int j = 0;
p->page_order[j++] = 0 + 1; // CLOCK
p->page_order[j++] = 1 + 1; // RECENT
p->page_order[j++] = 2 + 1; // RADIO
p->page_order[j++] = 3 + 1; // BLUETOOTH
p->page_order[j++] = 4 + 1; // ADVERT
#if ENV_INCLUDE_GPS == 1
p->page_order[j++] = 5 + 1; // GPS
#endif
#if UI_SENSORS_PAGE == 1
p->page_order[j++] = 6 + 1; // SENSORS
#endif
p->page_order[j++] = 9 + 1; // SETTINGS
p->page_order[j++] = 7 + 1; // TOOLS
p->page_order[j++] = 10 + 1; // MESSAGES (QUICK_MSG)
p->page_order[j++] = 8 + 1; // SHUTDOWN
while (j < 11) p->page_order[j++] = 0;
}
// Swaps item's page_order slot with its neighbour in the given direction (-1=earlier, +1=later).
void movePageInOrder(int item, int delta, NodePrefs* p) {
ensurePageOrderInit(p);
int bit = homePageBitIndex(item);
if (bit < 0) return;
int cur = -1, total = 0;
for (int i = 0; i < 11; i++) {
uint8_t v = p->page_order[i];
if (v < 1 || v > 11) break;
if ((int)(v - 1) == bit) cur = i;
total++;
}
if (cur < 0) return;
int next = cur + delta;
if (next < 0 || next >= total) return;
uint8_t tmp = p->page_order[cur];
p->page_order[cur] = p->page_order[next];
p->page_order[next] = tmp;
}
bool isMsgSlot(int item) const { bool isMsgSlot(int item) const {
return item >= MSG_SLOT_0 && item <= MSG_SLOT_9; return item >= MSG_SLOT_0 && item <= MSG_SLOT_9;
} }
@@ -269,9 +355,17 @@ class SettingsScreen : public UIScreen {
uint8_t v = p ? p->notif_melody_ch : 0; uint8_t v = p ? p->notif_melody_ch : 0;
display.print(L[v < 3 ? v : 0]); } display.print(L[v < 3 ? v : 0]); }
} else if (isHomePage(item)) { } else if (isHomePage(item)) {
int pos = homePagePosition(item, p);
if (pos > 0) {
char pb[4]; snprintf(pb, sizeof(pb), "%d ", pos);
display.print(pb);
}
display.print(homePageLabel(item)); display.print(homePageLabel(item));
display.setCursor(VAL_X, y); display.setCursor(VAL_X, y);
display.print(homePageVisible(item, p) ? "ON" : "OFF"); if (!homePageToggleable(item))
display.print("always");
else
display.print(homePageVisible(item, p) ? "ON" : "OFF");
} else if (item == TX_POWER) { } else if (item == TX_POWER) {
display.print("TX Pwr"); display.print("TX Pwr");
char buf[8]; char buf[8];
@@ -452,10 +546,18 @@ public:
p->notif_melody_ch = (p->notif_melody_ch + (left ? 2 : 1)) % 3; p->notif_melody_ch = (p->notif_melody_ch + (left ? 2 : 1)) % 3;
_dirty = true; return true; _dirty = true; return true;
} }
if (isHomePage(_selected) && p && (left || right || enter)) { if (isHomePage(_selected) && p) {
p->home_pages_mask ^= homePageBit(_selected); if (left || right) {
_dirty = true; movePageInOrder(_selected, left ? -1 : 1, p);
return true; _dirty = true;
return true;
}
if (enter && homePageToggleable(_selected)) {
p->home_pages_mask ^= homePageBit(_selected);
_dirty = true;
return true;
}
return enter;
} }
if (_selected == TX_POWER && p) { if (_selected == TX_POWER && p) {
if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; } if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; }
+61 -18
View File
@@ -174,6 +174,35 @@ class HomeScreen : public UIScreen {
return -1; // SETTINGS, QUICK_MSG always visible return -1; // SETTINGS, QUICK_MSG always visible
} }
// Maps page_order bit-index (0-10) back to the HomePage enum value for this build.
// Returns -1 if the page is not compiled in.
int bitToPage(int bit) const {
switch (bit) {
case 0: return CLOCK;
case 1: return RECENT;
case 2: return RADIO;
case 3: return BLUETOOTH;
case 4: return ADVERT;
case 5:
#if ENV_INCLUDE_GPS == 1
return GPS;
#else
return -1;
#endif
case 6:
#if UI_SENSORS_PAGE == 1
return SENSORS;
#else
return -1;
#endif
case 7: return TOOLS;
case 8: return SHUTDOWN;
case 9: return SETTINGS;
case 10: return QUICK_MSG;
default: return -1;
}
}
bool isPageVisible(int page) const { bool isPageVisible(int page) const {
int bit = pageBit(page); int bit = pageBit(page);
if (bit < 0) return true; if (bit < 0) return true;
@@ -181,12 +210,31 @@ class HomeScreen : public UIScreen {
return (mask >> bit) & 1; return (mask >> bit) & 1;
} }
int navPage(int from, int dir) const { // Build ordered list of all visible pages, respecting page_order when set.
for (int i = 1; i < (int)Count; i++) { // Returns count; out[] receives HomePage enum values.
int next = ((from + dir * i) % (int)Count + (int)Count) % (int)Count; int buildVisibleOrder(int* out) const {
if (isPageVisible(next)) return next; int n = 0;
bool custom = _node_prefs && _node_prefs->page_order[0] >= 1 && _node_prefs->page_order[0] <= 11;
if (custom) {
for (int i = 0; i < 11; i++) {
uint8_t v = _node_prefs->page_order[i];
if (v < 1 || v > 11) break;
int pg = bitToPage(v - 1);
if (pg >= 0 && pg < (int)Count && isPageVisible(pg)) out[n++] = pg;
}
} else {
for (int pg = 0; pg < (int)Count; pg++)
if (isPageVisible(pg)) out[n++] = pg;
} }
return from; return n;
}
int navPage(int from, int dir) const {
int order[11]; int n = buildVisibleOrder(order);
if (n == 0) return from;
int cur = 0;
for (int i = 0; i < n; i++) if (order[i] == from) { cur = i; break; }
return order[((cur + dir) % n + n) % n];
} }
int renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { int renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
@@ -354,20 +402,15 @@ public:
// curr page indicator — hidden on CLOCK page (full screen used for dashboard) // curr page indicator — hidden on CLOCK page (full screen used for dashboard)
if (_page != CLOCK) { if (_page != CLOCK) {
int vis_count = 0, curr_vis = 0; int order[11]; int n = buildVisibleOrder(order);
for (int i = 0; i < (int)Count; i++) { int curr_vis = 0;
if (!isPageVisible(i)) continue; for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; }
if (i == _page) curr_vis = vis_count;
vis_count++;
}
int y = 14; int y = 14;
int x = display.width() / 2 - 5 * (vis_count - 1); int x = display.width() / 2 - 5 * (n - 1);
int vi = 0; for (int i = 0; i < n; i++) {
for (int i = 0; i < (int)Count; i++) { if (i == curr_vis) display.fillRect(x-1, y-1, 3, 3);
if (!isPageVisible(i)) continue; else display.fillRect(x, y, 1, 1);
if (vi == curr_vis) display.fillRect(x-1, y-1, 3, 3); x += 10;
else display.fillRect(x, y, 1, 1);
x += 10; vi++;
} }
} }