feat(ui): on-device channel management, remote admin tool, per-language keyboards

Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
  MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
  Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
  the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
  onChannelRemoved sequence previously duplicated across the two
  CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.

Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
  the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
  with common get/set fields (name, radio profile, tx power, repeat, advert
  intervals, ...) plus a free-text "Custom command..." fallback for anything
  else. A field row fetches the current value, opens it pre-filled for
  editing, and sends the change — falling back to a blank editor if the
  fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
  Messages: saved on a confirmed admin-level login, forgotten on a failed
  one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
  a reply reaches the UI without touching the existing BLE/app CLI-terminal
  path (queueMessage's should_display gate is untouched).

Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.

Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-12 19:40:53 +02:00
parent 62e82e8740
commit f399298fa6
21 changed files with 1117 additions and 214 deletions

View File

@@ -130,12 +130,13 @@ static const int QUICK_MSGS_MAX = 10;
#include "FullscreenMsgView.h"
#include "SensorPlaceholders.h"
#include "SettingsScreen.h"
#include "MessageHistory.h" // RAM history rings (DM + channel) used by QuickMsgScreen
#include "QuickMsgScreen.h"
#include "MessageHistory.h" // RAM history rings (DM + channel) used by MessagesScreen
#include "MessagesScreen.h"
// ── Custom screens (separate files to ease upstream merges) ───────────────────
#include "RingtoneEditorScreen.h"
#include "BotScreen.h"
#include "AdminScreen.h"
#include "NearbyScreen.h"
#include "DashboardConfigScreen.h"
#include "AutoAdvertScreen.h"
@@ -1345,7 +1346,7 @@ public:
return true;
}
if (c == KEY_ENTER && _page == HomePage::QUICK_MSG) {
_task->gotoQuickMsgScreen();
_task->gotoMessagesScreen();
return true;
}
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
@@ -1442,10 +1443,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
splash = new SplashScreen(this);
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
settings = new SettingsScreen(this, &_kb);
quick_msg = new QuickMsgScreen(this, &_kb);
messages_screen = new MessagesScreen(this, &_kb);
tools_screen = new ToolsScreen(this);
ringtone_edit = new RingtoneEditorScreen(this, node_prefs);
bot_screen = new BotScreen(this, node_prefs, &_kb);
admin_screen = new AdminScreen(this);
nearby_screen = new NearbyScreen(this);
dashboard_config = new DashboardConfigScreen(this, node_prefs);
auto_advert_screen = new AutoAdvertScreen(this, node_prefs);
@@ -1467,6 +1469,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
void UITask::gotoSettingsScreen() { setCurrScreen(settings); }
void UITask::gotoToolsScreen() { setCurrScreen(tools_screen); }
void UITask::gotoBotScreen() { setCurrScreen(bot_screen); }
void UITask::gotoAdminScreen() { setCurrScreen(admin_screen); } // AdminScreen::onShow() resets it
void UITask::gotoNearbyScreen() { setCurrScreen(nearby_screen); }
void UITask::gotoDashboardConfig() { setCurrScreen(dashboard_config); }
void UITask::gotoTrailScreen() { setCurrScreen(trail_screen); }
@@ -1667,60 +1670,64 @@ bool UITask::isMelodyPlaying() {
#endif
}
void UITask::gotoQuickMsgScreen() {
((QuickMsgScreen*)quick_msg)->reset();
setCurrScreen(quick_msg);
void UITask::gotoMessagesScreen() {
((MessagesScreen*)messages_screen)->reset();
setCurrScreen(messages_screen);
}
void UITask::openContactDM(const ContactInfo& ci) {
((QuickMsgScreen*)quick_msg)->reset();
((QuickMsgScreen*)quick_msg)->enterDM(ci);
setCurrScreen(quick_msg);
((MessagesScreen*)messages_screen)->reset();
((MessagesScreen*)messages_screen)->enterDM(ci);
setCurrScreen(messages_screen);
}
void UITask::shareToMessage(const char* text) {
((QuickMsgScreen*)quick_msg)->startShare(text);
setCurrScreen(quick_msg);
((MessagesScreen*)messages_screen)->startShare(text);
setCurrScreen(messages_screen);
}
void UITask::pickLocShareTarget() {
((QuickMsgScreen*)quick_msg)->startPickTarget();
setCurrScreen(quick_msg);
((MessagesScreen*)messages_screen)->startPickTarget();
setCurrScreen(messages_screen);
}
void UITask::pickBotChannelTarget() {
((QuickMsgScreen*)quick_msg)->startPickBotChannel();
setCurrScreen(quick_msg);
((MessagesScreen*)messages_screen)->startPickBotChannel();
setCurrScreen(messages_screen);
}
void UITask::pickBotRoomTarget() {
((QuickMsgScreen*)quick_msg)->startPickBotRoom();
setCurrScreen(quick_msg);
((MessagesScreen*)messages_screen)->startPickBotRoom();
setCurrScreen(messages_screen);
}
int UITask::getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const {
return ((QuickMsgScreen*)quick_msg)->getRecentDMContacts(out, max);
return ((MessagesScreen*)messages_screen)->getRecentDMContacts(out, max);
}
void UITask::addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp) {
_last_notif_ch_idx = (int)channel_idx;
((QuickMsgScreen*)quick_msg)->addChannelMsg(channel_idx, text, timestamp);
((MessagesScreen*)messages_screen)->addChannelMsg(channel_idx, text, timestamp);
}
int UITask::getChannelUnreadCount() const {
return ((QuickMsgScreen*)quick_msg)->getTotalChannelUnread();
return ((MessagesScreen*)messages_screen)->getTotalChannelUnread();
}
void UITask::onMsgAck(uint32_t ack_crc) {
((QuickMsgScreen*)quick_msg)->markDmDelivered(ack_crc);
((MessagesScreen*)messages_screen)->markDmDelivered(ack_crc);
}
void UITask::onChannelRelayed(uint32_t seq) {
((QuickMsgScreen*)quick_msg)->markChannelRelayed(seq);
((MessagesScreen*)messages_screen)->markChannelRelayed(seq);
}
void UITask::onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
((QuickMsgScreen*)quick_msg)->onRoomLoginResult(pub_key, success, permissions);
// Only one on-device login can be in flight at a time (MyMesh::ui_pending_login
// is a single slot) -- route the result to whichever of the two screens that
// can trigger a login is currently active, rather than always MessagesScreen.
if (curr == admin_screen) ((AdminScreen*)admin_screen)->onRoomLoginResult(pub_key, success, permissions);
else ((MessagesScreen*)messages_screen)->onRoomLoginResult(pub_key, success, permissions);
// Unlike the keypress-driven showAlert() calls elsewhere, this fires from a
// background mesh response with no keypress to schedule a redraw — without
// forcing one, the alert's short expiry can lapse before the next scheduled
@@ -1728,8 +1735,13 @@ void UITask::onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t per
_next_refresh = 0;
}
void UITask::onAdminReply(const uint8_t* pub_key, const char* text) {
((AdminScreen*)admin_screen)->onAdminReply(pub_key, text);
_next_refresh = 0; // same reasoning as onRoomLoginResult above
}
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp) {
((QuickMsgScreen*)quick_msg)->addDMMsg(pub_key, outgoing, text, sender_timestamp);
((MessagesScreen*)messages_screen)->addDMMsg(pub_key, outgoing, text, sender_timestamp);
}
int UITask::getDMUnreadTotal() const {
@@ -1790,7 +1802,7 @@ void UITask::msgRead(int msgcount) {
if (msgcount == 0) {
_room_unread = 0;
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
((QuickMsgScreen*)quick_msg)->clearAllChannelUnread();
((MessagesScreen*)messages_screen)->clearAllChannelUnread();
}
}
@@ -2028,7 +2040,7 @@ bool UITask::dequeueKey(char& c) {
void UITask::loop() {
// Background delivery: resend pending on-device DMs whose ACK timed out, and
// finalise the ✗ marker — runs regardless of which screen is active.
((QuickMsgScreen*)quick_msg)->tickDmResends();
((MessagesScreen*)messages_screen)->tickDmResends();
#if UI_HAS_JOYSTICK
uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION;
int ev = user_btn.check();