feat(ui): consolidate backups and refine e-paper UX

Move backup export/import into one workflow, improve narrow settings controls, and make T-Deck Pro/Max dropdowns and lock state e-paper appropriate.

Refs #559
This commit is contained in:
Michael A. Cojocari committed 2026-09-22 20:41:58 -04:00
1 parent 6af981fe57
commit 4d31c7c35f
2 files changed
+171 -119

No files matched your search

+1 -1
View File
@@ -513,7 +513,7 @@ bool touchPrefsGetScrollReverse(); // invert trackball/scrollball di
void touchPrefsSetScrollReverse(bool on);
bool touchPrefsGetEdgeScroll(); // push cursor past edge to scroll content (default false)
void touchPrefsSetEdgeScroll(bool on);
bool touchPrefsGetLockOnScreenOff(); // idle screen-off auto-locks; only a deliberate hold wakes (default false)
bool touchPrefsGetLockOnScreenOff(); // idle screen-off auto-locks (default false; forced on for Pro/Max e-paper)
void touchPrefsSetLockOnScreenOff(bool on);
bool touchPrefsGetGlanceWhenLocked(); // "at a glance" also fires while manually/idle locked, not just unlocked+dimmed (default false)
void touchPrefsSetGlanceWhenLocked(bool on);
+170 -118
View File
@@ -49,7 +49,9 @@ static void* wadaMp3Scratch() { return s_wada_mp3_scratch; }
#include <driver/rtc_io.h> // rtc_gpio_pullup_en — hold the wake pin's level in deep sleep
#include "assets/lockscreen_placeholder_jpg.h" // seeded to SPIFFS /lock/placeholder.jpg on first boot (PNG decode is broken on this board)
#if CAP_LOCK_SCREEN
#include "assets/lockscreen_wallpaper_rgb565.h" // crisp pre-dithered default lock-screen wallpaper (no JPEG banding)
#if !defined(HAS_TDECK_PRO)
#include "assets/lockscreen_wallpaper_rgb565.h" // crisp pre-dithered default lock-screen wallpaper (no JPEG banding)
#endif
#if defined(TLORA_PAGER)
#include "assets/lockscreen_wallpaper_pager_rgb565.h" // native 480x222 crop/layout for this board's wide/short panel
#endif
@@ -5797,7 +5799,7 @@ static inline void navMarkDirty() {}
static unsigned long s_slider_touch_ms = 0; // last time a slider (volume, etc.) was dragged
static bool s_wake_swallow = false; // swallow the whole touch that wakes the screen (issue #4)
static bool s_lock_on_screen_off = false; // idle screen-off also engages the manual lock (cached pref)
static bool s_lock_on_screen_off = false; // cached pref; forced on for Pro/Max so e-paper shows a lock indicator
#if !defined(HAS_TANMATSU)
// Web UI mirror: a virtual pointer indev fed by a phone browser's taps over the
@@ -11970,46 +11972,6 @@ static void buildProfileSettings() {
y += SC(42);
}
// "Export settings" — write a MeshCore-app-compatible JSON backup (identity,
// radio/position, channels, contacts) to SD if a card is in, else internal
// flash. The file opens in the stock app / web client.
{
lv_obj_t* eb = lv_btn_create(body);
lv_obj_set_size(eb, lv_pct(100), SC(34));
lv_obj_set_pos(eb, 2, y);
styleButton(eb);
lv_obj_add_event_cb(eb, +[](lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
// Fixed, app-compatible name so the stock app / web client can read it back.
doExportBackupFile("meshcore-backup.json");
}, LV_EVENT_CLICKED, nullptr);
lv_obj_t* el = lv_label_create(eb);
lv_label_set_text(el, TR("Export settings"));
lv_obj_set_style_text_color(el, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(el, &g_font_14, LV_PART_MAIN);
lv_obj_center(el);
y += SC(42);
}
// "Import settings" — restore a MeshCore-app-compatible JSON backup from
// /meshcore-backup.json (SD if present, else internal). Replaces identity,
// channels and contacts, then reboots so radio settings take effect.
{
lv_obj_t* ib = lv_btn_create(body);
lv_obj_set_size(ib, lv_pct(100), SC(34));
lv_obj_set_pos(ib, 2, y);
styleButton(ib);
lv_obj_add_event_cb(ib, +[](lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
openBackupPicker();
}, LV_EVENT_CLICKED, nullptr);
lv_obj_t* il = lv_label_create(ib);
lv_label_set_text(il, TR("Import settings"));
lv_obj_set_style_text_color(il, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(il, &g_font_14, LV_PART_MAIN);
lv_obj_center(il);
y += SC(42);
}
}
// Single-character spam filter. A lot of mesh spam is a 1-byte payload — the cheapest
@@ -12097,17 +12059,34 @@ static void clampDropdownListCb(lv_event_t* e) {
// clamp keeps a list that opens upward from sliding under the tall app-page title bar.
static void styleDropdown(lv_obj_t* dd) {
lv_obj_set_style_text_font(dd, &g_font_12, LV_PART_MAIN);
#if defined(HAS_TDECK_PRO)
lv_obj_set_style_bg_color(dd, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_bg_opa(dd, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_text_color(dd, lv_color_black(), LV_PART_MAIN);
styleEpaperControlOutline(dd, LV_PART_MAIN);
#else
lv_obj_set_style_bg_color(dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_border_color(dd, lv_color_hex(COLOR_BORDER), LV_PART_MAIN);
#endif
if (lv_obj_t* list = lv_dropdown_get_list(dd)) { // created by the constructor, so it exists already
#if defined(HAS_TDECK_PRO)
lv_obj_set_style_bg_color(list, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_bg_opa(list, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_text_color(list, lv_color_black(), LV_PART_MAIN);
styleEpaperControlOutline(list, LV_PART_MAIN);
lv_obj_set_style_bg_color(list, lv_color_black(), LV_PART_SELECTED | LV_STATE_CHECKED);
lv_obj_set_style_bg_opa(list, LV_OPA_COVER, LV_PART_SELECTED | LV_STATE_CHECKED);
lv_obj_set_style_text_color(list, lv_color_white(), LV_PART_SELECTED | LV_STATE_CHECKED);
#else
lv_obj_set_style_bg_color(list, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(list, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(list, &g_font_12, LV_PART_MAIN);
// The highlighted option (LVGL draws it in LV_PART_SELECTED with the CHECKED state).
lv_obj_set_style_bg_color(list, lv_color_hex(COLOR_ACCENT), LV_PART_SELECTED | LV_STATE_CHECKED);
lv_obj_set_style_bg_opa(list, LV_OPA_COVER, LV_PART_SELECTED | LV_STATE_CHECKED);
lv_obj_set_style_text_color(list, lv_color_hex(COLOR_ON_ACCENT), LV_PART_SELECTED | LV_STATE_CHECKED);
#endif
lv_obj_set_style_text_font(list, &g_font_12, LV_PART_MAIN);
}
lv_obj_add_event_cb(dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr);
}
@@ -12297,17 +12276,8 @@ static void buildRadioSettings() {
lv_obj_set_size(g_set_modal.radio_preset_dd, lv_pct(100),SC(34));
lv_obj_set_pos(g_set_modal.radio_preset_dd, 2, y);
lv_dropdown_set_options(g_set_modal.radio_preset_dd, preset_opt_buf ? preset_opt_buf : "Custom (manual)");
lv_obj_set_style_text_font(g_set_modal.radio_preset_dd, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_bg_color(g_set_modal.radio_preset_dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(g_set_modal.radio_preset_dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_border_color(g_set_modal.radio_preset_dd, lv_color_hex(COLOR_BORDER), LV_PART_MAIN);
/* Dropdown list (the popup once tapped) */
lv_obj_t* preset_list = lv_dropdown_get_list(g_set_modal.radio_preset_dd);
lv_obj_set_style_bg_color(preset_list, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(preset_list, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(preset_list, &g_font_12, LV_PART_MAIN);
styleDropdown(g_set_modal.radio_preset_dd);
lv_obj_add_event_cb(g_set_modal.radio_preset_dd, radioPresetChangedCb, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_add_event_cb(g_set_modal.radio_preset_dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr);
y += SC(40);
}
@@ -12423,19 +12393,11 @@ static void buildRadioSettings() {
lv_obj_set_pos(dd, 2, y);
// Order MUST match TELEM_MODE_*: 0 = deny, 1 = allow-flags, 2 = allow-all.
lv_dropdown_set_options(dd, TR("Never\nChosen contacts only\nAnyone who asks"));
lv_obj_set_style_text_font(dd, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_bg_color(dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_border_color(dd, lv_color_hex(COLOR_BORDER), LV_PART_MAIN);
lv_obj_t* loclist = lv_dropdown_get_list(dd);
lv_obj_set_style_bg_color(loclist, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(loclist, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(loclist, &g_font_12, LV_PART_MAIN);
styleDropdown(dd);
uint8_t lm = prefs ? prefs->telemetry_mode_loc : TELEM_MODE_DENY;
if (lm > TELEM_MODE_ALLOW_ALL) lm = TELEM_MODE_DENY;
lv_dropdown_set_selected(dd, lm);
lv_obj_add_event_cb(dd, locTelemetryModeChangedCb, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_add_event_cb(dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr);
y += SC(40);
}
{
@@ -12493,19 +12455,11 @@ static void buildRadioSettings() {
lv_obj_set_size(dd, lv_pct(100), SC(34));
lv_obj_set_pos(dd, 2, y);
lv_dropdown_set_options(dd, TR("1 byte (legacy)\n2 bytes\n3 bytes"));
lv_obj_set_style_text_font(dd, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_bg_color(dd, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(dd, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_border_color(dd, lv_color_hex(COLOR_BORDER), LV_PART_MAIN);
lv_obj_t* phlist = lv_dropdown_get_list(dd);
lv_obj_set_style_bg_color(phlist, lv_color_hex(COLOR_PANEL), LV_PART_MAIN);
lv_obj_set_style_text_color(phlist, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(phlist, &g_font_12, LV_PART_MAIN);
styleDropdown(dd);
uint8_t phm = prefs ? prefs->path_hash_mode : 0;
if (phm > 2) phm = 0;
lv_dropdown_set_selected(dd, phm);
lv_obj_add_event_cb(dd, pathHashModeChangedCb, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_add_event_cb(dd, clampDropdownListCb, LV_EVENT_CLICKED, nullptr);
y += SC(40);
}
@@ -15726,10 +15680,17 @@ static void buildDeviceSettings(int sec) {
int h = settingsRowLabel(body, y, 6, TR("Lock when screen off"), COLOR_SUB, nullptr, 56);
lv_obj_t* sw = lv_switch_create(body);
lv_obj_align(sw, LV_ALIGN_TOP_RIGHT, 0, y);
#if defined(HAS_TDECK_PRO)
// E-paper retains its last frame when powered down. Pro/Max always paint
// the lock view first so the glass never looks live after timeout.
lv_obj_add_state(sw, LV_STATE_CHECKED);
lv_obj_add_state(sw, LV_STATE_DISABLED);
#else
#if defined(ESP32)
if (touchPrefsGetLockOnScreenOff()) lv_obj_add_state(sw, LV_STATE_CHECKED);
#endif
lv_obj_add_event_cb(sw, lockOnScreenOffToggleCb, LV_EVENT_VALUE_CHANGED, nullptr);
#endif
y += LV_MAX(40, h + 12);
}
#endif
@@ -15761,25 +15722,31 @@ static void buildDeviceSettings(int sec) {
{
y += settingsRowLabel(body, y, 0, TR("Custom UTC offset (hours)"), COLOR_SUB, &g_font_12, 0) + 2;
lv_obj_t* bminus = lv_btn_create(body);
lv_obj_t* offset_row = lv_obj_create(body);
lv_obj_remove_style_all(offset_row);
lv_obj_set_size(offset_row, lv_pct(100), SC(34));
lv_obj_set_pos(offset_row, 0, y);
lv_obj_set_flex_flow(offset_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(offset_row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(offset_row, 6, LV_PART_MAIN);
lv_obj_clear_flag(offset_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* bminus = lv_btn_create(offset_row);
lv_obj_set_size(bminus, SC(50), SC(34));
lv_obj_set_pos(bminus, 2, y);
styleButton(bminus);
lv_obj_add_event_cb(bminus, timeOffsetMinusCb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* lm = lv_label_create(bminus); lv_label_set_text(lm, "-1 h"); lv_obj_center(lm);
useChainedFont(lm);
s_time_offset_lbl = lv_label_create(body);
lv_obj_set_width(s_time_offset_lbl, 100);
s_time_offset_lbl = lv_label_create(offset_row);
lv_obj_set_flex_grow(s_time_offset_lbl, 1);
lv_obj_set_style_text_align(s_time_offset_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_text_color(s_time_offset_lbl, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(s_time_offset_lbl, &g_font_16, LV_PART_MAIN);
lv_obj_set_pos(s_time_offset_lbl, 58, y + 9);
timeOffsetLabelRefresh();
lv_obj_t* bplus = lv_btn_create(body);
lv_obj_t* bplus = lv_btn_create(offset_row);
lv_obj_set_size(bplus, SC(50), SC(34));
lv_obj_set_pos(bplus, 168, y);
styleButton(bplus);
lv_obj_add_event_cb(bplus, timeOffsetPlusCb, LV_EVENT_CLICKED, nullptr);
lv_obj_t* lp = lv_label_create(bplus); lv_label_set_text(lp, "+1 h"); lv_obj_center(lp);
@@ -41913,6 +41880,9 @@ static int8_t s_lock_clock_current = -1; // refresh immediately when cl
static lv_obj_t* s_lock_unread = nullptr; // envelope + unread count under the clock (issue #93)
static int s_lock_unread_n = -1; // last count drawn (redraw guard)
static unsigned long s_lock_unread_ms = 0; // 1 Hz poll limiter
#if defined(HAS_TDECK_PRO)
static bool s_lock_epaper_default = false; // built-in white lock view vs custom wallpaper
#endif
#if defined(TLORA_PAGER)
static lv_obj_t* s_lock_status = nullptr; // "Screen locked" -- tracked for lockscreenHide() cleanup
static lv_obj_t* s_lock_hint = nullptr; // unlock hint -- tracked for lockscreenHide() cleanup
@@ -41981,7 +41951,9 @@ static void lockscreenUpdateClock() {
mm = (int)((millis() / 60000u) % 60u);
}
lv_label_set_text(s_lock_clock, b);
#if defined(TLORA_PAGER) || defined(HAS_TDECK_PRO)
#if defined(HAS_TDECK_PRO)
const lv_color_t normal_color = s_lock_epaper_default ? lv_color_black() : lv_color_white();
#elif defined(TLORA_PAGER)
const lv_color_t normal_color = lv_color_hex(0xFFFFFFu);
#else
const lv_color_t normal_color = lv_color_hex(touchPrefsGetLockTextColor());
@@ -42078,25 +42050,29 @@ static void lockscreenShow() {
if (s_lock_root) { lv_obj_move_foreground(s_lock_root); return; }
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
char wpath[TOUCH_LOCK_WALLPAPER_MAXLEN];
touchPrefsGetLockWallpaper(wpath, sizeof wpath);
#if defined(HAS_TDECK_PRO)
s_lock_epaper_default = !strcmp(wpath, "/lock/placeholder.jpg");
#endif
s_lock_root = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(s_lock_root);
lv_obj_set_size(s_lock_root, sw, sh);
lv_obj_set_pos(s_lock_root, 0, 0);
#if defined(HAS_TDECK_PRO)
lv_obj_set_style_bg_color(s_lock_root,
s_lock_epaper_default ? lv_color_white() : lv_color_black(), LV_PART_MAIN);
#else
lv_obj_set_style_bg_color(s_lock_root, lv_color_black(), LV_PART_MAIN);
#endif
lv_obj_set_style_bg_opa(s_lock_root, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_clear_flag(s_lock_root, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(s_lock_root, LV_OBJ_FLAG_CLICKABLE); // absorb taps (no UI leak)
#if defined(TLORA_PAGER)
// Keep the lock overlay OUT of the keyboard/encoder nav focus group. The root
// is CLICKABLE (to absorb taps) and it lives on lv_layer_top, so on this
// no-touch board navMaybeRebuild() would otherwise collect it as the only
// focusable element in the overlay and focus it -- at which point navFocusCb's
// shared cursor would otherwise glow around the full-screen overlay instead
// of a real control. NAV_SKIP_FLAG keeps the passive lock view out of the
// navigation tree entirely.
// This is a passive input shield, not a control. Without NAV_SKIP_FLAG,
// keypad-nav boards collect the clickable full-screen root and draw a focus
// glow around the lock view.
lv_obj_add_flag(s_lock_root, NAV_SKIP_FLAG);
#endif
// Wallpaper, scaled to cover the screen (crop overflow, never letterbox).
int ww = 0, wh = 0;
@@ -42113,12 +42089,22 @@ static void lockscreenShow() {
lvglPsramFree(s_lock_wall);
s_lock_wall = nullptr;
}
char wpath[TOUCH_LOCK_WALLPAPER_MAXLEN];
touchPrefsGetLockWallpaper(wpath, sizeof wpath);
if (!strcmp(wpath, "/lock/placeholder.jpg")) {
// Default: the pre-dithered RGB565 embed, drawn straight from flash — crisp,
// with no JPEG round-trip to re-introduce gradient banding.
#if defined(TLORA_PAGER)
#if defined(HAS_TDECK_PRO)
// The shared default is a 320x240 landscape bitmap. Cover-scaling it onto
// this 240x320 portrait panel crops both ends of WADAMESH and magnifies its
// padlock. Draw a native monochrome wordmark instead: no crop, no giant icon.
lv_obj_t* brand = lv_label_create(s_lock_root);
lv_label_set_text(brand, "WADAMESH");
lv_obj_set_width(brand, sw - 24);
lv_obj_set_style_text_align(brand, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_text_font(brand, &lv_font_unscii_16, LV_PART_MAIN);
lv_obj_set_style_text_color(brand, lv_color_black(), LV_PART_MAIN);
lv_obj_set_style_text_letter_space(brand, 2, LV_PART_MAIN);
lv_obj_align(brand, LV_ALIGN_CENTER, 0, 6);
#elif defined(TLORA_PAGER)
// Native 480x222 crop of the same icon+wordmark, repositioned to the left
// (see lockscreen_wallpaper_pager_rgb565.h) -- the shared 320x240 art's
// cover-fill on this much wider/shorter panel zoomed it ~1.5x and center-
@@ -42161,7 +42147,11 @@ static void lockscreenShow() {
lv_obj_align(img, LV_ALIGN_CENTER, 0, 0);
}
#if defined(TLORA_PAGER) || defined(HAS_TDECK_PRO)
#if defined(HAS_TDECK_PRO)
// The built-in Pro/Max lock view is black on white for clean 1-bit e-paper.
// Custom wallpapers keep the prior white overlay text for contrast.
const lv_color_t col = s_lock_epaper_default ? lv_color_black() : lv_color_white();
#elif defined(TLORA_PAGER)
// Force a guaranteed-visible white here rather than the shared, user-
// customizable touchPrefsGetLockTextColor() -- on this board that pref was
// rendering noticeably dim/dark (reported/photographed against this panel's
@@ -42223,6 +42213,10 @@ static void lockscreenShow() {
useChainedFont(hint);
#if defined(HAS_TANMATSU)
lv_label_set_text(hint, TR("press Volume Down to unlock"));
#elif defined(HAS_TDECK_PRO)
// Pro/Max also define HAS_PAGER_KEYBOARD, but Backspace unlock is disabled
// for them: GPIO0 is the deliberate unlock control.
lv_label_set_text(hint, TR("press the button to unlock"));
#elif defined(HAS_PAGER_KEYBOARD)
// lockscreenShow() DOES run on this board -- lockscreenReveal() (peek on a
// Backspace tap, or the new-message notify flash while locked) is gated on
@@ -42231,16 +42225,26 @@ static void lockscreenShow() {
lv_label_set_text(hint, TR("hold Backspace to unlock"));
#elif defined(HAS_THINKNODE_M9)
lv_label_set_text(hint, TR("double-press d-pad center to unlock"));
#elif defined(HAS_TDECK_PRO)
lv_label_set_text(hint, TR("press the button to unlock"));
#elif defined(HAS_WIO_TRACKER_L2)
lv_label_set_text(hint, TR("hold the wake button to unlock"));
#else
lv_label_set_text(hint, TR("hold the trackball to unlock"));
#endif
lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN);
lv_obj_set_style_text_font(hint,
#if defined(HAS_TDECK_PRO)
&g_font_14,
#else
&g_font_12,
#endif
LV_PART_MAIN);
lv_obj_set_style_text_color(hint, col, LV_PART_MAIN);
#if defined(HAS_TDECK_PRO)
lv_obj_set_width(hint, sw - 24);
lv_obj_set_style_text_align(hint, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_text_opa(hint, LV_OPA_COVER, LV_PART_MAIN);
#else
lv_obj_set_style_text_opa(hint, LV_OPA_70, LV_PART_MAIN);
#endif
#if defined(TLORA_PAGER)
lv_obj_align(hint, LV_ALIGN_BOTTOM_RIGHT, -6, -8); // bottom-right corner, clear of the icon/clock column above
s_lock_hint = hint;
@@ -42291,7 +42295,7 @@ static void serviceLockscreen() {
unsigned long now = millis();
if (now - s_lock_unread_ms >= 1000) { s_lock_unread_ms = now; lockscreenUpdateUnread(); }
}
#endif // core lock screen (HAS_TDECK_GT911 || HAS_TANMATSU)
#endif // CAP_LOCK_SCREEN
#if CAP_SOUND_FILES // custom WAV notification sounds -- T-Deck/pager (SD or SPIFFS)
// ---- Notification-sound chooser (Settings -> Sound) ------------------------
@@ -42718,18 +42722,30 @@ static void doBackupImportChosen() {
touchPrefsFlush();
ESP.restart();
}
static void confirmBackupImport(const char* stored) {
if (!stored) return;
strncpy(s_backup_chosen, stored, sizeof(s_backup_chosen) - 1);
s_backup_chosen[sizeof(s_backup_chosen) - 1] = '\0';
showConfirm(TR("Import this backup?\nReplaces identity,\nchannels & contacts,\nthen reboots."), TR("Import"), doBackupImportChosen);
}
static void backupChosenCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
const char* stored = (const char*)lv_event_get_user_data(e);
if (!stored) return;
strncpy(s_backup_chosen, stored, sizeof(s_backup_chosen) - 1);
s_backup_chosen[sizeof(s_backup_chosen) - 1] = '\0';
backupPickerClose();
showConfirm(TR("Import this backup?\nReplaces identity,\nchannels & contacts,\nthen reboots."), TR("Import"), doBackupImportChosen);
confirmBackupImport(stored);
}
static void openBackupPicker() {
backupScan();
backupPickerClose();
if (s_backup_count == 0) {
if (g_lv.task) g_lv.task->showAlert(TR("No backups available to restore."), 2000);
return;
}
if (s_backup_count == 1) {
confirmBackupImport(s_backup_paths[0]);
return;
}
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
s_backup_picker = lv_obj_create(lv_layer_top());
@@ -42762,12 +42778,6 @@ static void openBackupPicker() {
lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN);
lv_obj_set_scroll_dir(list, LV_DIR_VER);
if (s_backup_count == 0) {
lv_obj_t* empty = lv_label_create(list);
lv_label_set_text(empty, TR("No .json backups found.\nExport one first, or copy a\nmeshcore-backup.json to the\nSD card or internal flash."));
lv_obj_set_style_text_color(empty, lv_color_hex(COLOR_SUB), LV_PART_MAIN);
lv_obj_set_style_text_font(empty, &g_font_12, LV_PART_MAIN);
}
for (int i = 0; i < s_backup_count; ++i) {
lv_obj_t* b = lv_btn_create(list);
lv_obj_set_width(b, lv_pct(100));
@@ -42788,6 +42798,12 @@ static void openBackupPicker() {
// with a Delete button, plus a Factory-reset action. The detail page is rebuilt
// after a delete so the list stays accurate.
static char s_backup_del_path[160] = {0};
static bool s_backup_append_timestamp = true;
static void backupAppendTimestampCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return;
s_backup_append_timestamp = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
}
// Rebuild the Backups detail page (after an export/delete). Deferred via
// lv_async_call so we never tear the sheet down from inside one of its own
@@ -42962,8 +42978,8 @@ static void buildBackupsSettings() {
const lv_coord_t cw = s_settings_content_w;
int y = 0;
// Create a fresh backup right here (goes to the SD card if one is in, else
// internal flash) and re-list so the new file shows up immediately.
// One full-backup action. With the option below checked it creates a unique
// snapshot; unchecked it writes the stock-app-compatible fixed filename.
{
lv_obj_t* eb = lv_btn_create(body);
lv_obj_set_size(eb, cw, SC(40));
@@ -42974,23 +42990,37 @@ static void buildBackupsSettings() {
lv_obj_set_style_text_color(eb, lv_color_hex(COLOR_ON_STATUS_OK), LV_PART_MAIN);
lv_obj_add_event_cb(eb, +[](lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
char fn[48]; backupMakeFilename(fn, sizeof fn);
doExportBackupFile(fn);
char timestamped[48];
const char* filename = "meshcore-backup.json";
if (s_backup_append_timestamp) {
backupMakeFilename(timestamped, sizeof timestamped);
filename = timestamped;
}
doExportBackupFile(filename);
lv_async_call(backupsRebuildAsyncCb, nullptr); // re-list once the event unwinds
}, LV_EVENT_CLICKED, nullptr);
lv_obj_t* el = lv_label_create(eb);
char eblbl[56]; snprintf(eblbl, sizeof eblbl, LV_SYMBOL_SAVE " %s", TR("Export new backup"));
char eblbl[56]; snprintf(eblbl, sizeof eblbl, LV_SYMBOL_SAVE " %s", TR("Export Backup"));
lv_label_set_text(el, eblbl);
lv_obj_set_style_text_font(el, &g_font_14, LV_PART_MAIN);
lv_obj_set_style_text_color(el, lv_color_hex(COLOR_ON_STATUS_OK), LV_PART_MAIN);
lv_obj_center(el);
y += SC(48);
lv_obj_t* append_cb = lv_checkbox_create(body);
lv_checkbox_set_text(append_cb, TR("Append timestamp"));
lv_obj_set_width(append_cb, cw);
lv_obj_set_pos(append_cb, 2, y);
lv_obj_set_style_text_color(append_cb, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_set_style_text_font(append_cb, &g_font_14, LV_PART_MAIN);
if (s_backup_append_timestamp) lv_obj_add_state(append_cb, LV_STATE_CHECKED);
lv_obj_add_event_cb(append_cb, backupAppendTimestampCb, LV_EVENT_VALUE_CHANGED, nullptr);
lv_obj_update_layout(append_cb);
y += lv_obj_get_height(append_cb) + SC(12);
}
#if defined(HAS_TANMATSU)
// Import a saved backup right here. The T-Deck / V4 reach the picker via the Identity page's
// "Import settings" button; the Tanmatsu surfaces it next to Export so the whole round-trip
// (export a .json, then restore it) lives on one page. Opens the same internal+SD .json picker.
// Import from every known internal or SD/SD_MMC JSON backup. The picker shows
// all discovered files, then confirms the selected restore before applying it.
{
lv_obj_t* ib = lv_btn_create(body);
lv_obj_set_size(ib, cw, SC(40));
@@ -43008,14 +43038,13 @@ static void buildBackupsSettings() {
lv_obj_center(il);
y += SC(48);
}
#endif
y += settingsRowLabel(body, y, 0, TR("Saved backups"), COLOR_SUB, &g_font_12, 0) + 4;
backupScan();
if (s_backup_count == 0) {
y += settingsRowLabel(body, y, 0,
TR("No backups yet. Tap Export new backup above to create one."),
TR("No backups yet. Use Export Backup above to create one."),
COLOR_SUB, &g_font_12, 0) + 6;
}
for (int i = 0; i < s_backup_count; ++i) {
@@ -58957,7 +58986,14 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
touchPrefsBegin();
uint16_t to_s = touchPrefsGetScreenTimeoutSecs();
_screen_timeout_ms = static_cast<uint32_t>(to_s) * 1000u;
#if defined(HAS_TDECK_PRO)
// Unlike an LCD, the e-paper keeps showing the previous live UI after power
// down. Always route timeout through lockScreen(), which paints an unambiguous
// retained lock frame before sleeping. Max inherits HAS_TDECK_PRO.
s_lock_on_screen_off = true;
#else
s_lock_on_screen_off = touchPrefsGetLockOnScreenOff();
#endif
#if defined(HAS_TDECK_KEYBOARD)
// Applied here rather than at tdeckKeyboardBegin(), which runs on the touch
// task before prefs are up. The driver re-checks this every poll, so it takes
@@ -60500,7 +60536,10 @@ static inline void touchScreenBacklight(bool on) {
maxSleepBannerShow(false);
display.turnOn();
} else {
maxSleepBannerShow(true);
// A hard lock already paints an explicit lock frame. Keep the generic
// "Asleep" banner for unlocked sleeps only; its tap-to-wake hint is false
// while touch is deliberately blocked by the lock.
maxSleepBannerShow(!(g_lv.task && g_lv.task->isManualLock()));
if (g_lv.ready) { lv_refr_now(nullptr); display.serviceRefresh(true); }
maxFrontlightOff();
display.turnOff();
@@ -60628,6 +60667,10 @@ void UITask::lockScreen() {
// before sleeping so a locked device never leaves the live UI on the glass.
_manual_lock = true;
_screen_off = false;
// The arrows live on lv_layer_sys above the lock overlay. Hide any controls
// inherited from the previous scrollable page before committing this retained
// frame; the fixed lock content itself has nothing to page-scroll.
epaperScrollControlsSync(true);
setCpuForScreen(true);
lockscreenShow();
lv_refr_now(nullptr);
@@ -62511,7 +62554,16 @@ void UITask::loop() {
{ // Front pads: heart toggles the front-light (Meck's shortcut on this board).
uint8_t key_id = 0;
if (display.takeFrontKeyPress(key_id)) {
if (key_id == 0) {
if (_manual_lock) {
// Front pads may reveal the retained lock view, but GPIO0 remains the
// deliberate unlock control. wakeScreen() would clear _manual_lock and
// leave the lock overlay stranded on screen.
if (_screen_off) lockscreenReveal();
if (key_id == 0) {
if (s_max_frontlight_on) maxFrontlightOff(); else maxFrontlightOn();
}
_last_input_ms = millis();
} else if (key_id == 0) {
if (_screen_off) wakeScreen();
if (s_max_frontlight_on) maxFrontlightOff(); else maxFrontlightOn();
_last_input_ms = millis();