touch: beta_7 — app-drawer fit + visible scroll, Snake touch steering & icon

- App drawer: size tiles to fit so the bottom row's labels aren't clipped, and
  show a persistent scrollbar when the grid overflows (discoverable scroll).
- Snake: steer via the hardware swipe detector (works on the V4 cap-touch and
  the T-Deck), stop touch/swipe leaking to the screen behind the overlay, and
  gate the status bar while playing.
- Snake app-drawer icon drawn from shapes — the U+1F40D emoji isn't in the baked
  colour-emoji set, so it rendered as a tofu box.
- Add beta_7 release notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kaj SchittecatandClaude Opus 4.8 committed 2026-06-18 12:21:49 +02:00
1 parent bbcafa3088
commit 9e567e1789
2 files changed
+113 -4

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
# beta_7 — what's new (one user-facing note per non-blank, non-# line)
# --- Chat reliability ---
Fixed garbled and duplicated messages — chat text is no longer corrupted in transit, so messages arrive intact and duplicates are filtered out correctly (thanks to marcelverdult, issue #13)
Unread badges are accurate again: the phantom and stuck unread counts on chats and channels are fixed
More of your chat history is kept and reloaded across reboots, and it is saved more reliably than before
# --- New: Snake game ---
A Snake game is now in the app drawer — steer by swiping the screen or with the T-Deck trackball, with pause/resume and restart after a game over
While you play, touch stays inside the game and no longer leaks through to the screen behind it
# --- Battery & telemetry ---
Tap the battery in the status bar for a 24-hour history chart with an estimated time-left readout; battery level is logged every 5 minutes
New per-node telemetry window: poll a node's battery, temperature and humidity, watch them on a live chart, and keep a per-node log
# --- Map ---
New map night mode — the status bar and tab bar invert to match the dark map instead of glaring
A live zoom-level readout sits above the zoom slider, and the zoom button now shows a proper magnifier icon
Contacts have a new "Show on map" action that centres the map on that node
# --- Status bar & tools ---
A microSD activity LED lights whenever the card is accessed, including file-manager reads and copies
An activity spinner shows while a telemetry, admin, ping, trace, range or sightline request is in flight, kept clear of the clock
New option to hide the device name and move the clock to the left
Press and hold the status bar to save a full-screen screenshot to the SD card
# --- Files & images ---
The file browser can preview PNG and BMP images now, not just JPEG, and no longer shows a stale image when you open several in a row
Lock-screen wallpaper: open Files, view any JPEG on your SD card, and tap "Set as wallpaper" — no more slow scan or "no image found"
# --- App drawer ---
The app drawer fits all the apps again — the bottom row's labels are no longer cut off
A scrollbar clearly shows when there are more apps than fit, so it is obvious the page scrolls
# --- Bluetooth ---
You can set your own Bluetooth pairing code in Settings now, instead of being stuck with the default
# --- Storage & stability ---
Fixed map tiles failing to cache (with a false "storage full"), and black or missing tiles now re-download instead of staying blank
Replying to a message with @ no longer overwrites text you were already typing
Fixed an out-of-memory crash and made notification audio more reliable
# --- Community ---
Huge thanks to Dan Vybíral (Vybo) for the Snake game, battery history, per-node telemetry, map night mode, PNG/BMP image viewer and the status-bar tools (PR #12)
Thanks to Ruben Laban for the accurate unread-badge and chat-history-persistence fixes (PR #15)
+68 -4
View File
@@ -1920,6 +1920,19 @@ static void versionCheckService(unsigned long now) {
#endif
}
static void applySwipeGesture(int8_t swipe_x, int8_t swipe_y) {
// The Snake game owns the whole screen while it's open: steer with the swipe
// and return, so it can never reach the tab switcher / map pan / control
// centre behind the overlay. This reuses the per-board hardware swipe detector
// that drives tab switching (reliable on both the V4 cap-touch and the T-Deck
// GT911), since LVGL's own LV_EVENT_GESTURE doesn't fire dependably on the
// cap-touch — so this, not SnakeGame's gestureCb, is what makes touch steering
// actually work. swipe_x<0 == left, swipe_y<0 == up (see the control-center
// note below for the sign convention).
if (SnakeGame::isOpen()) {
if (swipe_x != 0) SnakeGame::steer(swipe_x < 0 ? -1 : 1, 0);
else if (swipe_y != 0) SnakeGame::steer(0, swipe_y < 0 ? -1 : 1);
return;
}
// A slider was just being dragged — its horizontal drag must NOT be read as a
// tab/back swipe (e.g. raising the volume slider rightward kept triggering the
// settings "swipe right = back"). Ignore swipes briefly after any slider touch.
@@ -21835,7 +21848,38 @@ static void addAppTile(lv_obj_t* parent, int x, int y, int w, int h,
lv_obj_set_style_border_width(chip_o, 1, LV_PART_MAIN);
lv_obj_set_style_border_opa(chip_o, LV_OPA_50, LV_PART_MAIN);
if (icon) {
if (act == APPACT_SNAKE) {
// The 🐍 emoji (U+1F40D) isn't in the baked colour-emoji set, so a plain
// label renders it as a tofu box. Draw the game's blocky snake from a few
// rounded cells instead — self-contained, on-theme, and scales with the chip.
lv_obj_t* box = lv_obj_create(chip_o);
lv_obj_remove_style_all(box);
lv_obj_clear_flag(box, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(box, 18, 12);
lv_obj_center(box);
// Tail bottom-left -> right -> up -> head top-right (cells share edges, so the
// body reads as one continuous snake).
static const struct { int8_t x, y; } seg[4] = { {0,6}, {6,6}, {6,0}, {12,0} };
for (int s = 0; s < 4; s++) {
lv_obj_t* cell = lv_obj_create(box);
lv_obj_remove_style_all(cell);
lv_obj_clear_flag(cell, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(cell, 6, 6);
lv_obj_set_pos(cell, seg[s].x, seg[s].y);
lv_obj_set_style_bg_color(cell, lv_color_hex(icon_col), LV_PART_MAIN);
lv_obj_set_style_bg_opa(cell, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(cell, 2, LV_PART_MAIN);
}
// Dark pupil on the head cell (the last segment, facing right).
lv_obj_t* eye = lv_obj_create(box);
lv_obj_remove_style_all(eye);
lv_obj_clear_flag(eye, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(eye, 2, 2);
lv_obj_set_pos(eye, 14, 1);
lv_obj_set_style_bg_color(eye, lv_color_hex(0x0E1216), LV_PART_MAIN);
lv_obj_set_style_bg_opa(eye, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_radius(eye, 1, LV_PART_MAIN);
} else if (icon) {
lv_obj_t* ic = lv_label_create(chip_o);
lv_label_set_text(ic, icon);
lv_obj_set_style_text_font(ic, &g_font_16, LV_PART_MAIN);
@@ -21904,8 +21948,14 @@ static void openAppDrawer() {
lv_obj_set_style_bg_color(s_appdrawer_root, lv_color_hex(COLOR_BG), LV_PART_MAIN);
lv_obj_set_style_bg_opa(s_appdrawer_root, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_scroll_dir(s_appdrawer_root, LV_DIR_VER); // grid scrolls top->bottom when it overflows
lv_obj_set_scrollbar_mode(s_appdrawer_root, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_style_pad_bottom(s_appdrawer_root, 10, LV_PART_MAIN); // room past the last row when scrolled
// A clearly visible scrollbar so users discover the lower rows. The mode is
// set after the grid is measured (ON when it overflows, OFF when it all fits).
lv_obj_set_style_bg_color(s_appdrawer_root, lv_color_hex(COLOR_ACCENT), LV_PART_SCROLLBAR);
lv_obj_set_style_bg_opa(s_appdrawer_root, LV_OPA_70, LV_PART_SCROLLBAR);
lv_obj_set_style_width(s_appdrawer_root, 5, LV_PART_SCROLLBAR);
lv_obj_set_style_radius(s_appdrawer_root, 3, LV_PART_SCROLLBAR);
lv_obj_set_style_pad_right(s_appdrawer_root, 2, LV_PART_SCROLLBAR);
lv_obj_move_foreground(s_appdrawer_root);
// First tile is "Command" (back to the command centre); the rest are the apps.
@@ -21927,7 +21977,7 @@ static void openAppDrawer() {
{ ">_", "Terminal", APPACT_TERMINAL, 0, 0x3DD27A }, // console green
{ LV_SYMBOL_DIRECTORY, "Files", APPACT_FILES, 0, 0xE6BE4A }, // folder gold
#endif
{ "\xF0\x9F\x90\x8D", "Snake", APPACT_SNAKE, 0, 0x53C06B }, // snake game
{ nullptr, "Snake", APPACT_SNAKE, 0, 0x53C06B }, // snake game (icon drawn from APPACT_SNAKE, not a glyph)
{ LV_SYMBOL_POWER, "Power", APPACT_POWER, 0, 0xE05544 }, // power red
};
const int n = (int)(sizeof(tiles) / sizeof(tiles[0]));
@@ -21948,13 +21998,23 @@ static void openAppDrawer() {
const int avail = (sh - STATUSBAR_H - TABBAR_H) - top - 8; // content area minus top inset + bottom margin
int tile_h = (avail - (rows - 1) * gap) / rows;
if (tile_h > 84) tile_h = 84;
if (tile_h < 60) tile_h = 60; // floor: overflow scrolls instead of shrinking to tiny tiles
if (tile_h < 54) tile_h = 54; // floor: overflow scrolls instead of shrinking to tiny tiles
// (54, not 60, so the V4's 4-row grid fits on screen rather than
// clipping the bottom row's label by a few px)
for (int i = 0; i < n; i++) {
const int col = i % cols, row = i / cols;
addAppTile(s_appdrawer_root, pad + col * (tile_w + gap), top + row * (tile_h + gap),
tile_w, tile_h, tiles[i].icon, tiles[i].label, tiles[i].act, tiles[i].badge,
tiles[i].color);
}
// Keep the scrollbar permanently visible only when the grid actually overflows
// the drawer area (so it's discoverable); when it all fits, hide it so there's
// no misleading full-height thumb.
const int grid_h = top + rows * tile_h + (rows - 1) * gap + 10 /*pad_bottom*/;
const int visible = sh - STATUSBAR_H - TABBAR_H;
lv_obj_set_scrollbar_mode(s_appdrawer_root,
grid_h > visible ? LV_SCROLLBAR_MODE_ON : LV_SCROLLBAR_MODE_OFF);
}
// Status-bar long-hold (3 s) -> full-screen screenshot to /screenshots on SD.
@@ -21978,6 +22038,10 @@ static void statusBarHoldCb(lv_event_t* e) {
static void statusBarTapCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
// The status bar lives on lv_layer_sys, ABOVE the Snake overlay, so it stays
// tappable while the game is up. Swallow the tap so it can't open the control
// centre over the game.
if (SnakeGame::isOpen()) return;
if (s_sb_shot_done) { s_sb_shot_done = false; return; } // this press was a screenshot hold
// While a settings detail sheet is open the bar shows its Back chevron + title,
// so a tap means "go back" rather than opening the control center.