mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): favourites as a Home card, left of the main page
The six slots with unread badges on a card of their own (swiped to, no clock), redrawn when messages arrive; the Favourites tile and screen are gone, showFavourites() opens the card. The page dots sit at the bottom on every card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -255,6 +255,7 @@ User list, second batch (2026-09-26):
|
||||
- [ ] Emoji, ideally the full range (glyphs could live on the SD card).
|
||||
- [x] Tap-to-wake toggle (Settings > Display & power > WAKE, NVS, on by
|
||||
default; off: only the top button wakes it).
|
||||
- [ ] Favourites as their own screen in the menu rather than a tile.
|
||||
- [x] Favourites as their own screen in the menu rather than a tile: a
|
||||
Home card left of the main page (six slots, unread badges).
|
||||
- [ ] WiFi indicator icon in the status bar.
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ void UITask::roomPoll() {
|
||||
if (o == RoomSessions::NONE) return;
|
||||
bool mine = s_login_wait && memcmp(key, s_login_key, 4) == 0;
|
||||
if (mine) s_login_wait = false;
|
||||
bool picking = (_screen == SCR_CHATS || _screen == SCR_FAVS) && !_nav_overlay && !locked(); // where it was tapped
|
||||
bool picking = (_screen == SCR_CHATS || _screen == SCR_HOME) && !_nav_overlay && !locked(); // where it was tapped
|
||||
if (o == RoomSessions::LOGGED_IN) {
|
||||
showToast("Logged in", 1200);
|
||||
if (mine && picking) openDM(s_login_key);
|
||||
|
||||
@@ -55,7 +55,6 @@ static void onNodeName(lv_event_t* e) { (void)e; s_ui->nodeNamePopup(); }
|
||||
static void onNodeNameKb(lv_event_t* e) { s_ui->nodeNameDone(lv_event_get_code(e) == LV_EVENT_READY); }
|
||||
static void onPowerRow(lv_event_t* e) { s_ui->powerPopup((uintptr_t)lv_event_get_user_data(e) != 0); }
|
||||
static void onPowerGo(lv_event_t* e) { s_ui->shutdown((uintptr_t)lv_event_get_user_data(e) != 0); }
|
||||
static void onOpenFavourites(lv_event_t* e) { (void)e; s_ui->showFavourites(); }
|
||||
static void onFavTap(lv_event_t* e) { s_ui->favTap((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
static void onFavHold(lv_event_t* e) {
|
||||
lv_indev_wait_release(lv_indev_active()); // the hold isn't also a tap that opens it
|
||||
@@ -448,22 +447,15 @@ void UITask::setAccent(int idx) {
|
||||
pinRowRefresh(); // the page, kept at its bottom where this row is
|
||||
}
|
||||
|
||||
// ── Favourites dial ───────────────────────────────────────────────────────────
|
||||
// ── Favourites card (Home, left of the main page) ─────────────────────────────
|
||||
|
||||
void UITask::showFavourites() {
|
||||
_screen = SCR_FAVS;
|
||||
buildFavourites();
|
||||
home::s_page = home::FAVS;
|
||||
showHome();
|
||||
}
|
||||
|
||||
void UITask::buildFavourites() {
|
||||
lv_obj_t* body = newScreen("Favourites", true);
|
||||
lv_obj_t* grid = lv_obj_create(body);
|
||||
styleSurface(grid, theme::BG);
|
||||
lv_obj_remove_flag(grid, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(grid, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
|
||||
lv_obj_set_style_pad_column(grid, theme::GAP, 0);
|
||||
lv_obj_set_style_pad_row(grid, theme::GAP, 0);
|
||||
// The six slots, three to a row, into `grid` (a wrapping row).
|
||||
void UITask::favGrid(lv_obj_t* grid) {
|
||||
int w = (lv_display_get_horizontal_resolution(NULL) - 2 * theme::PAD - 2 * theme::GAP) / 3;
|
||||
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) {
|
||||
char name[33];
|
||||
@@ -471,7 +463,7 @@ void UITask::buildFavourites() {
|
||||
bool used = contactctl::favName(_prefs, s, name, sizeof(name), &c);
|
||||
bool chan = favslots::kind(_prefs, s) == NodePrefs::FAV_KIND_CHANNEL;
|
||||
lv_obj_t* b = lv_button_create(grid);
|
||||
lv_obj_set_size(b, w, 70);
|
||||
lv_obj_set_size(b, w, 76);
|
||||
lv_obj_set_style_pad_all(b, 4, 0);
|
||||
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
@@ -484,19 +476,26 @@ void UITask::buildFavourites() {
|
||||
lv_obj_add_event_cb(b, onFavTap, LV_EVENT_CLICKED, (void*)(uintptr_t)s);
|
||||
lv_obj_add_event_cb(b, onFavHold, LV_EVENT_LONG_PRESSED, (void*)(uintptr_t)s);
|
||||
const char* icon = !used ? LV_SYMBOL_PLUS : chan ? "#" : c.type == ADV_TYPE_ROOM ? LV_SYMBOL_HOME : UI_SYMBOL_USERS;
|
||||
lv_obj_align(label(b, icon, THEME_FONT_LARGE, used ? theme::ACCENT : theme::TEXT_MUTED), LV_ALIGN_TOP_MID, 0, 8);
|
||||
lv_obj_align(label(b, icon, THEME_FONT_LARGE, used ? theme::ACCENT : theme::TEXT_MUTED), LV_ALIGN_TOP_MID, 0, 10);
|
||||
lv_obj_t* n = label(b, used ? name : "Add", THEME_FONT_SMALL, used ? theme::TEXT : theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(n, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_style_text_align(n, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_width(n, w - 10);
|
||||
lv_obj_align(n, LV_ALIGN_BOTTOM_MID, 0, -6);
|
||||
lv_obj_align(n, LV_ALIGN_BOTTOM_MID, 0, -8);
|
||||
if (used) {
|
||||
int unread = chan ? _core->history.chUnread(_prefs->favourite_contacts[s][0])
|
||||
: (c.type == ADV_TYPE_CHAT ? _core->dmUnread(c.id.pub_key) : 0);
|
||||
badge(b, unread, false);
|
||||
}
|
||||
}
|
||||
label(body, "Tap to open. Hold to change or remove.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_t* hint = label(grid, "Tap to open. Hold to change or remove.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_set_width(hint, LV_PCT(100));
|
||||
lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_CENTER, 0);
|
||||
}
|
||||
|
||||
// A slot changed: the card redrawn if it's up.
|
||||
void UITask::favRefresh() {
|
||||
if (_screen == SCR_HOME && home::s_page == home::FAVS) setHomePage(home::FAVS);
|
||||
}
|
||||
|
||||
void UITask::favTap(int slot) {
|
||||
@@ -524,7 +523,7 @@ void UITask::favAction(uint8_t act) {
|
||||
favslots::clear(_prefs, slot);
|
||||
the_mesh.savePrefs();
|
||||
navClosePopup();
|
||||
buildFavourites();
|
||||
favRefresh();
|
||||
}
|
||||
|
||||
// What to put in a slot: channels, then contacts and rooms (favourites first).
|
||||
@@ -571,7 +570,7 @@ void UITask::favPick(int code) {
|
||||
else favslots::pinChannel(_prefs, s_fav_slot, (uint8_t)code);
|
||||
the_mesh.savePrefs();
|
||||
navClosePopup();
|
||||
if (_screen == SCR_FAVS) buildFavourites();
|
||||
favRefresh();
|
||||
}
|
||||
|
||||
// "Pin" in an options popup: six slot buttons with who is in them now.
|
||||
|
||||
@@ -771,7 +771,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
int UITask::screenDepth(Screen s) {
|
||||
switch (s) {
|
||||
case SCR_HOME: return 0;
|
||||
case SCR_CHATS: case SCR_NEARBY: case SCR_MAP: case SCR_SETTINGS: case SCR_FAVS:
|
||||
case SCR_CHATS: case SCR_NEARBY: case SCR_MAP: case SCR_SETTINGS:
|
||||
case SCR_COMPASS: case SCR_CLOCK: case SCR_BOT: case SCR_REPEATER: case SCR_ADMIN_PICK:
|
||||
case SCR_DIAG: case SCR_GPS: return 1;
|
||||
case SCR_THREAD: case SCR_CONTACTS: case SCR_CHANNEL_EDIT: case SCR_NODE:
|
||||
@@ -787,7 +787,6 @@ void UITask::back() {
|
||||
case SCR_CONTACTS: showChats(); break;
|
||||
case SCR_CHANNEL_EDIT: showChats(); break;
|
||||
case SCR_BOT: if (_nav_overlay) navClosePopup(); else showHome(); break;
|
||||
case SCR_FAVS: if (_nav_overlay) navClosePopup(); else showHome(); break;
|
||||
case SCR_DIAG: if (_nav_overlay) navClosePopup(); else showHome(); break;
|
||||
case SCR_ADMIN_PICK: showHome(); break;
|
||||
case SCR_OTA: otaLeave(); break;
|
||||
@@ -860,7 +859,6 @@ static void onOpenNodesMap(lv_event_t* e) { (void)e; s_ui->openMap(false); }
|
||||
static void onOpenNav(lv_event_t* e) { (void)e; s_ui->openMap(true); }
|
||||
static void onOpenClock(lv_event_t* e); // ClockScreen.h
|
||||
static void onOpenBot(lv_event_t* e); // BotScreen.h
|
||||
static void onOpenFavourites(lv_event_t* e); // DeviceScreen.h
|
||||
static void onOpenCompass(lv_event_t* e); // CompassScreen.h
|
||||
static void onOpenDiag(lv_event_t* e); // DiagScreen.h
|
||||
static void onOpenGps(lv_event_t* e); // GpsScreen.h
|
||||
@@ -870,8 +868,9 @@ static void onOpenAdminPick(lv_event_t* e); // AdminScreen.h
|
||||
static void onNodeName(lv_event_t* e);
|
||||
static void onPowerRow(lv_event_t* e);
|
||||
|
||||
// Home apps, in order; PER_PAGE to a page, further pages are swiped to (with
|
||||
// dots underneath), so new apps don't squeeze the row.
|
||||
// Home: the favourites card first (swiped to, left of the main page), then
|
||||
// the apps, PER_PAGE to a page, further pages swiped to (dots underneath), so
|
||||
// new apps don't squeeze the row.
|
||||
namespace home {
|
||||
struct App { const char* icon; const char* text; lv_event_cb_t cb; bool unread; };
|
||||
static const App APPS[] = {
|
||||
@@ -880,23 +879,26 @@ static const App APPS[] = {
|
||||
{ UI_SYMBOL_MAP, "Map", onOpenNav, false },
|
||||
{ LV_SYMBOL_SETTINGS, "Settings", onOpenSettings, false },
|
||||
// Page 2: tools
|
||||
{ UI_SYMBOL_STAR, "Favourites", onOpenFavourites, false },
|
||||
{ UI_SYMBOL_COMPASS, "Compass", onOpenCompass, false },
|
||||
{ UI_SYMBOL_CLOCK, "Clock", onOpenClock, false },
|
||||
{ LV_SYMBOL_CHARGE, "Bot", onOpenBot, false },
|
||||
{ LV_SYMBOL_GPS, "GPS", onOpenGps, false },
|
||||
// Page 3: the device and the network (the map's tools and Nearby's advert
|
||||
// are in those screens, not tiles of their own)
|
||||
{ LV_SYMBOL_LOOP, "Repeater", onOpenRepeater, false },
|
||||
{ UI_SYMBOL_KEY, "Admin", onOpenAdminPick, false },
|
||||
{ UI_SYMBOL_CHART, "Diagnostics", onOpenDiag, false },
|
||||
{ LV_SYMBOL_GPS, "GPS", onOpenGps, false },
|
||||
};
|
||||
static const int COUNT = sizeof(APPS) / sizeof(APPS[0]);
|
||||
static const int PER_PAGE = 4;
|
||||
static const int PAGES = (COUNT + PER_PAGE - 1) / PER_PAGE;
|
||||
static lv_obj_t* s_row = nullptr; // the current page's tiles
|
||||
static const int FAVS = 0; // page 0: the favourites card
|
||||
static const int MAIN = 1; // the first page of apps, where Home opens
|
||||
static const int PAGES = 1 + (COUNT + PER_PAGE - 1) / PER_PAGE;
|
||||
static lv_obj_t* s_row = nullptr; // the current page's tiles (or favourites)
|
||||
static lv_obj_t* s_dots = nullptr;
|
||||
static int s_page = 0; // kept across rebuilds (the home screen is rebuilt on return)
|
||||
static lv_obj_t* s_top = nullptr; // clock, date and name: hidden on the favourites card
|
||||
static int s_page = MAIN; // kept across rebuilds (the home screen is rebuilt on return)
|
||||
static int s_fav_sig = -1; // unread total the favourites card was drawn with
|
||||
|
||||
// A horizontal swipe anywhere on Home turns the page (the tiles row alone is
|
||||
// too small a target), tracked from the touch itself: LVGL's gesture detector
|
||||
@@ -941,9 +943,15 @@ void UITask::buildHome() {
|
||||
lv_obj_t* body = newScreen(NULL, false);
|
||||
lv_obj_set_flex_align(body, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
|
||||
_home_clock = label(body, "--:--", THEME_FONT_CLOCK, theme::TEXT);
|
||||
_home_date = label(body, "", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
lv_obj_t* name = label(body, the_mesh.getNodeName(), THEME_FONT_BODY, theme::ACCENT);
|
||||
home::s_top = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(home::s_top);
|
||||
lv_obj_remove_flag(home::s_top, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(home::s_top, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(home::s_top, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(home::s_top, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
_home_clock = label(home::s_top, "--:--", THEME_FONT_CLOCK, theme::TEXT);
|
||||
_home_date = label(home::s_top, "", THEME_FONT_BODY, theme::TEXT_MUTED);
|
||||
lv_obj_t* name = label(home::s_top, the_mesh.getNodeName(), THEME_FONT_BODY, theme::ACCENT);
|
||||
lv_obj_set_style_pad_bottom(name, 4, 0);
|
||||
|
||||
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE); // a drag is a page swipe, not a scroll
|
||||
@@ -953,11 +961,14 @@ void UITask::buildHome() {
|
||||
lv_obj_set_size(home::s_row, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(home::s_row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(home::s_row, theme::GAP, 0);
|
||||
lv_obj_set_style_pad_row(home::s_row, theme::GAP, 0);
|
||||
home::s_dots = nullptr;
|
||||
if (home::PAGES > 1) {
|
||||
home::s_dots = lv_obj_create(body);
|
||||
lv_obj_remove_style_all(home::s_dots);
|
||||
lv_obj_add_flag(home::s_dots, LV_OBJ_FLAG_IGNORE_LAYOUT); // at the bottom whichever card is up
|
||||
lv_obj_set_size(home::s_dots, LV_SIZE_CONTENT, 22);
|
||||
lv_obj_align(home::s_dots, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_set_flex_flow(home::s_dots, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(home::s_dots, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(home::s_dots, 12, 0);
|
||||
@@ -999,7 +1010,16 @@ void UITask::setHomePage(int page) {
|
||||
home::s_page = page;
|
||||
lv_obj_clean(home::s_row);
|
||||
_home_unread = nullptr;
|
||||
for (int i = page * home::PER_PAGE; i < (page + 1) * home::PER_PAGE; i++) {
|
||||
bool favs = page == home::FAVS;
|
||||
if (favs) lv_obj_add_flag(home::s_top, LV_OBJ_FLAG_HIDDEN);
|
||||
else lv_obj_remove_flag(home::s_top, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_flex_flow(home::s_row, favs ? LV_FLEX_FLOW_ROW_WRAP : LV_FLEX_FLOW_ROW);
|
||||
if (favs) {
|
||||
home::s_fav_sig = homeUnreadTotal();
|
||||
favGrid(home::s_row); // DeviceScreen.h
|
||||
}
|
||||
int first = (page - home::MAIN) * home::PER_PAGE;
|
||||
for (int i = first; !favs && i < first + home::PER_PAGE; i++) {
|
||||
if (i < home::COUNT) {
|
||||
const home::App& a = home::APPS[i];
|
||||
homeTile(home::s_row, a.icon, a.text, a.cb, a.unread ? &_home_unread : NULL);
|
||||
@@ -1016,6 +1036,10 @@ void UITask::setHomePage(int page) {
|
||||
refreshHome();
|
||||
}
|
||||
|
||||
int UITask::homeUnreadTotal() {
|
||||
return _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread();
|
||||
}
|
||||
|
||||
void UITask::refreshHome() {
|
||||
if (!_home_clock) return;
|
||||
struct tm ti;
|
||||
@@ -1029,8 +1053,12 @@ void UITask::refreshHome() {
|
||||
lv_label_set_text(_home_clock, "--:--");
|
||||
lv_label_set_text(_home_date, "time not synced");
|
||||
}
|
||||
if (home::s_page == home::FAVS && home::s_row && homeUnreadTotal() != home::s_fav_sig) {
|
||||
setHomePage(home::FAVS); // new messages: the favourites' badges redrawn
|
||||
return;
|
||||
}
|
||||
if (!_home_unread) return;
|
||||
int unread = _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread();
|
||||
int unread = homeUnreadTotal();
|
||||
if (unread > 0) lv_label_set_text_fmt(_home_unread, "%d", unread);
|
||||
else lv_label_set_text(_home_unread, "");
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ public:
|
||||
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)ci; (void)from_picker; }
|
||||
|
||||
private:
|
||||
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_FAVS, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS };
|
||||
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS };
|
||||
|
||||
void buildStatusBar();
|
||||
void refreshStatusBar();
|
||||
@@ -342,7 +342,9 @@ private:
|
||||
void lockScreen();
|
||||
void refreshLock();
|
||||
void lockPoll();
|
||||
void buildFavourites();
|
||||
void favGrid(lv_obj_t* grid);
|
||||
void favRefresh();
|
||||
int homeUnreadTotal();
|
||||
void buildDiag();
|
||||
void refreshDiag();
|
||||
void buildCompass();
|
||||
|
||||
Reference in New Issue
Block a user