mirror of
https://github.com/pelgraine/Meck.git
synced 2026-07-29 12:52:40 +02:00
T-Watch: standalone channel config screen (region, notifications, delete)
The SettingsScreen SUB_CHANNELS sub-screen drives every per-channel action from physical-keyboard hotkeys (Ent: scope, N: notif, T: tone, X/Hold: delete), so on the touch-only watches the channel list rendered but nothing was actionable. Replace it on MECK_TWATCH builds with a standalone WatchChannelConfigScreen following the WatchAlarmScreen / WatchNotesScreen conventions. Desktop builds are unchanged. LIST shows each channel with its region scope tag and notification state (tap to select, tap again or button to open, paged like Notes). DETAIL offers: Region -- opens the watch keyboard via a new TWKB_SCOPE purpose, pre-filled with the current scope, committed through the same setChannel + saveChannels path as desktop (empty submission clears); Clear region -- reverts to the device default directly; Notify -- tap-cycles All -> Mentions -> Off with savePrefs, matching the desktop N-key order. Delete is button-driven rather than touch: a short press (PWR on the S3, boot button on the Plus) arms it, a second press within 3s confirms. Channel 0 remains protected. The tone picker is deliberately absent -- the watch has no audio path. Settings hands off via a _wantsWatchChannels flag polled by UITask::loop (mirrors the _needsTextVKB handshake); Back returns to Settings via gotoSettingsScreen. Keyboard results route through the existing consumeSendRequest purpose dispatch. Note: deleteChannel replicates SettingsScreen's compaction verbatim, including its existing quirk of not shifting channel_notif[] -- kept faithful rather than silently diverging; fix both together if desired. Files: WatchChannelConfigScreen.h (new, both variant dirs), TWatchComposeScreens.h (both variant dirs, TWKB_SCOPE), SettingsScreen.h, UITask.h, UITask.cpp, main.cpp
This commit is contained in:
@@ -1130,6 +1130,7 @@ static uint32_t _atoi(const char* sp) {
|
||||
#endif
|
||||
#if defined(MECK_TWATCH)
|
||||
#include "WatchNotesScreen.h" // After UITask.h -- needs NodePrefs
|
||||
#include "WatchChannelConfigScreen.h" // After UITask.h -- needs NodePrefs, the_mesh
|
||||
#endif
|
||||
#if defined(MECK_TWATCH) && HAS_GPS
|
||||
#include "WatchMapScreen.h" // After BLE -- PNGdec headers conflict with BLE if included earlier
|
||||
@@ -1385,6 +1386,13 @@ static void lastHeardToggleContact() {
|
||||
if (wn) wn->handleTap(x, y);
|
||||
return 0;
|
||||
}
|
||||
// Channel config screen: list rows, detail action rows and the footer are
|
||||
// all tap targets, in the same logical coords as the alarm/notes screens.
|
||||
if (ui_task.isOnWatchChannelConfigScreen()) {
|
||||
WatchChannelConfigScreen* wc = (WatchChannelConfigScreen*)ui_task.getWatchChannelConfigScreen();
|
||||
if (wc) wc->handleTap(x, y);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
#if defined(MECK_TWATCH)
|
||||
// Watch: tiles open on long-press and pages change on swipe, so a plain tap
|
||||
|
||||
@@ -341,6 +341,7 @@ private:
|
||||
|
||||
// T5S3: signal UITask to open VKB when entering text edit mode
|
||||
bool _needsTextVKB;
|
||||
bool _wantsWatchChannels; // Watch: hand off to WatchChannelConfigScreen (polled by UITask)
|
||||
|
||||
// 4G modem state (runtime cache of config)
|
||||
#ifdef HAS_4G_MODEM
|
||||
@@ -698,7 +699,7 @@ public:
|
||||
_editMode(EDIT_NONE), _editPos(0), _editPickerIdx(0),
|
||||
_editFloat(0), _editInt(0), _fontPickerOriginal(0), _confirmAction(0),
|
||||
_onboarding(false), _subScreen(SUB_NONE), _savedTopCursor(0),
|
||||
_radioChanged(false), _needsTextVKB(false) {
|
||||
_radioChanged(false), _needsTextVKB(false), _wantsWatchChannels(false) {
|
||||
memset(_editBuf, 0, sizeof(_editBuf));
|
||||
#ifdef HAS_SDCARD
|
||||
_savedExportCursor = 0;
|
||||
@@ -939,6 +940,8 @@ public:
|
||||
// T5S3 VKB integration for text editing (channel name, device name, freq, APN)
|
||||
bool needsTextVKB() const { return _needsTextVKB; }
|
||||
void clearTextNeedsVKB() { _needsTextVKB = false; }
|
||||
bool wantsWatchChannels() const { return _wantsWatchChannels; }
|
||||
void clearWantsWatchChannels() { _wantsWatchChannels = false; }
|
||||
const char* getEditBuf() const { return _editBuf; }
|
||||
SettingsRowType getCurrentRowType() const { return _rows[_cursor].type; }
|
||||
void submitEditText(const char* text) {
|
||||
@@ -3798,12 +3801,18 @@ public:
|
||||
Serial.println("Settings: entered Contacts sub-screen");
|
||||
break;
|
||||
case ROW_CHANNELS_SUBMENU:
|
||||
#if defined(MECK_TWATCH)
|
||||
// Watch: the desktop sub-screen's per-channel actions are keyboard
|
||||
// hotkeys; hand off to the standalone WatchChannelConfigScreen.
|
||||
_wantsWatchChannels = true; // UITask::loop() polls and opens it
|
||||
#else
|
||||
_savedTopCursor = _cursor;
|
||||
_subScreen = SUB_CHANNELS;
|
||||
_cursor = 0;
|
||||
_scrollTop = 0;
|
||||
rebuildRows();
|
||||
Serial.println("Settings: entered Channels sub-screen");
|
||||
#endif
|
||||
break;
|
||||
|
||||
case ROW_RXLOG:
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
#endif
|
||||
#if defined(MECK_TWATCH)
|
||||
#include "WatchNotesScreen.h" // After UITask.h -- needs NodePrefs
|
||||
#include "WatchChannelConfigScreen.h" // After UITask.h -- needs NodePrefs, the_mesh
|
||||
#endif
|
||||
#ifdef TWATCH_COMPOSE_ENABLED
|
||||
#include "TWatchComposeScreens.h"
|
||||
@@ -1798,6 +1799,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
#if defined(MECK_TWATCH)
|
||||
// LittleFS ("/maps" partition) is mounted in setup() before UITask::begin().
|
||||
watch_notes_screen = new WatchNotesScreen(this, &rtc_clock, node_prefs);
|
||||
watch_channel_cfg_screen = new WatchChannelConfigScreen(this, node_prefs);
|
||||
#endif
|
||||
#ifdef TWATCH_COMPOSE_ENABLED
|
||||
tw_picker = new TWatchChannelPicker(_display);
|
||||
@@ -2613,6 +2615,13 @@ if (curr) curr->poll();
|
||||
if (watch_notes_screen) setCurrScreen(watch_notes_screen);
|
||||
else gotoHomeScreen();
|
||||
if (nerr) showAlert(nerr, 1200);
|
||||
} else if (purpose == TWatchKeyboardScreen::TWKB_SCOPE) {
|
||||
WatchChannelConfigScreen* wc = (WatchChannelConfigScreen*)watch_channel_cfg_screen;
|
||||
const char* serr = wc ? wc->applyComposedScope(sendText) : "No channel cfg";
|
||||
kb->clearOutBuf();
|
||||
if (watch_channel_cfg_screen) setCurrScreen(watch_channel_cfg_screen);
|
||||
else gotoHomeScreen();
|
||||
if (serr) showAlert(serr, 1200);
|
||||
} else { // TWKB_ADMIN_PASSWORD or TWKB_ADMIN_CLI
|
||||
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)getRepeaterAdminScreen();
|
||||
if (admin) {
|
||||
@@ -2642,6 +2651,27 @@ if (curr) curr->poll();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (curr == watch_channel_cfg_screen && watch_channel_cfg_screen != nullptr) {
|
||||
WatchChannelConfigScreen* wc = (WatchChannelConfigScreen*)watch_channel_cfg_screen;
|
||||
if (wc->wantsKeyboard()) {
|
||||
wc->clearWantKeyboard();
|
||||
openTWatchKeyboard(TWatchKeyboardScreen::TWKB_SCOPE, 0);
|
||||
((TWatchKeyboardScreen*)tw_keyboard)->setInitialText(wc->getEditText());
|
||||
} else if (wc->wantsExit()) {
|
||||
wc->clearWantExit();
|
||||
gotoSettingsScreen();
|
||||
}
|
||||
}
|
||||
else if (curr == settings_screen && settings_screen != nullptr) {
|
||||
SettingsScreen* ss = (SettingsScreen*)settings_screen;
|
||||
if (ss->wantsWatchChannels()) {
|
||||
ss->clearWantsWatchChannels();
|
||||
if (watch_channel_cfg_screen) {
|
||||
((WatchChannelConfigScreen*)watch_channel_cfg_screen)->enter();
|
||||
setCurrScreen(watch_channel_cfg_screen);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_display != NULL && _display->isOn()) {
|
||||
|
||||
@@ -144,6 +144,7 @@ class UITask : public AbstractUITask {
|
||||
#endif
|
||||
#if defined(MECK_TWATCH)
|
||||
UIScreen* watch_notes_screen; // LittleFS note pad (shared NotesScreen needs SD)
|
||||
UIScreen* watch_channel_cfg_screen; // Per-channel region/notif/delete (touch UI)
|
||||
#endif
|
||||
UIScreen* _screenBeforeLock = nullptr;
|
||||
bool _locked = false;
|
||||
@@ -325,6 +326,7 @@ public:
|
||||
#endif
|
||||
#if defined(MECK_TWATCH)
|
||||
bool isOnWatchNotesScreen() const { return curr == watch_notes_screen; }
|
||||
bool isOnWatchChannelConfigScreen() const { return curr == watch_channel_cfg_screen; }
|
||||
#endif
|
||||
bool isOnMinesweeperScreen() const { return curr == minesweeper_screen; }
|
||||
bool isOnMapScreen() const { return curr == map_screen; }
|
||||
@@ -424,6 +426,7 @@ public:
|
||||
#endif
|
||||
#if defined(MECK_TWATCH)
|
||||
UIScreen* getWatchNotesScreen() const { return watch_notes_screen; }
|
||||
UIScreen* getWatchChannelConfigScreen() const { return watch_channel_cfg_screen; }
|
||||
#endif
|
||||
UIScreen* getMinesweeperScreen() const { return minesweeper_screen; }
|
||||
UIScreen* getMapScreen() const { return map_screen; }
|
||||
|
||||
@@ -417,7 +417,7 @@ class TWatchKeyboardScreen : public UIScreen {
|
||||
uint8_t _channelIdx;
|
||||
|
||||
public:
|
||||
enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE };
|
||||
enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE };
|
||||
|
||||
private:
|
||||
Purpose _purpose;
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// WatchChannelConfigScreen -- per-channel settings for the T-Watch S3 / S3 Plus.
|
||||
//
|
||||
// The SettingsScreen SUB_CHANNELS sub-screen drives every per-channel action
|
||||
// from physical-keyboard hotkeys on the selected row (Ent: edit region scope,
|
||||
// N: cycle notifications, T: tone, X/Hold: delete), none of which exist on the
|
||||
// touch-only watch. This standalone screen replaces it on MECK_TWATCH builds,
|
||||
// following the WatchAlarmScreen / WatchNotesScreen conventions.
|
||||
//
|
||||
// LIST mode shows each channel with its region scope tag and notification
|
||||
// state; tap a row to select it, tap again (or PWR/Enter) to open it. DETAIL
|
||||
// mode offers: Region (opens the watch keyboard via TWKB_SCOPE, pre-filled
|
||||
// with the current scope), Clear region (reverts to the device default),
|
||||
// Notify (tap-cycles All -> Mentions -> Off, mirroring the desktop order),
|
||||
// and Delete via the button (channel 1+ only): a short press of the PWR key
|
||||
// (S3) or the boot button (S3 Plus) arms it, a second press within 3s
|
||||
// confirms. The tone picker is deliberately absent -- no audio path.
|
||||
//
|
||||
// Exit: the footer Back returns to the Settings screen (via a flag polled by
|
||||
// UITask::loop); the universal top-strip tap still goes home.
|
||||
//
|
||||
// Geometry: 240x240 physical, UI_ZOOM=2, so display.width()/height() = 120.
|
||||
// =============================================================================
|
||||
|
||||
// NOTE on include order: mirrors WatchNotesScreen.h -- this header uses
|
||||
// NodePrefs (NOTIF_* constants, channel_notif[]) and the_mesh, which the two
|
||||
// call sites (UITask.cpp, main.cpp) already have in scope before including it.
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/ChannelDetails.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/LGFXDisplay.h>
|
||||
|
||||
class UITask;
|
||||
|
||||
#ifndef MAX_GROUP_CHANNELS
|
||||
#define MAX_GROUP_CHANNELS 20
|
||||
#endif
|
||||
|
||||
#define WCC_ROWS 4 // list rows per page
|
||||
|
||||
class WatchChannelConfigScreen : public UIScreen {
|
||||
public:
|
||||
enum Mode { LIST, DETAIL };
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _node_prefs;
|
||||
|
||||
uint8_t _chIdx[MAX_GROUP_CHANNELS]; // populated channel slots, in order
|
||||
int _numCh = 0;
|
||||
|
||||
Mode _mode = LIST;
|
||||
int _sel = 0; // selected list row (index into _chIdx)
|
||||
int _scrollTop = 0;
|
||||
int _detail = -1; // channel slot open in DETAIL (value from _chIdx)
|
||||
bool _confirmDelete = false;
|
||||
unsigned long _confirmAt = 0;
|
||||
|
||||
// Keyboard / exit handshake -- UITask::loop() polls these (UITask is only
|
||||
// forward declared here, so this screen cannot call it directly).
|
||||
bool _wantKeyboard = false;
|
||||
bool _wantExit = false;
|
||||
char _editScope[31]; // pre-fill text handed to the keyboard
|
||||
|
||||
// Layout (logical pixels)
|
||||
static const int HEADER_H = 14;
|
||||
static const int ROW_H = 20;
|
||||
static const int FOOTER_Y = 100;
|
||||
|
||||
void rebuildList() {
|
||||
// Scan ALL slots -- the companion app may write non-contiguously, and
|
||||
// gaps can appear after deletion (same scan as SettingsScreen).
|
||||
_numCh = 0;
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
_chIdx[_numCh++] = i;
|
||||
}
|
||||
}
|
||||
if (_sel >= _numCh) _sel = _numCh > 0 ? _numCh - 1 : 0;
|
||||
if (_scrollTop > _sel) _scrollTop = _sel;
|
||||
}
|
||||
|
||||
const char* notifLabel(uint8_t idx) const {
|
||||
uint8_t n = _node_prefs->channel_notif[idx];
|
||||
return (n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "@" : "All";
|
||||
}
|
||||
|
||||
// Replicates SettingsScreen::deleteChannel (kept in sync manually). Note:
|
||||
// channel_notif[] is not shifted during compaction -- this matches the
|
||||
// desktop behaviour exactly.
|
||||
void deleteChannel(uint8_t idx) {
|
||||
ChannelDetails empty;
|
||||
memset(&empty, 0, sizeof(empty));
|
||||
int total = 0;
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
total = i + 1;
|
||||
}
|
||||
}
|
||||
for (int i = idx; i < total - 1; i++) {
|
||||
ChannelDetails next;
|
||||
if (the_mesh.getChannel(i + 1, next)) {
|
||||
the_mesh.setChannel(i, next);
|
||||
}
|
||||
}
|
||||
the_mesh.setChannel(total - 1, empty);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("WatchChannelCfg: Deleted channel at idx %d, compacted %d channels\n", idx, total);
|
||||
}
|
||||
|
||||
void ensureVisible() {
|
||||
if (_sel < _scrollTop) _scrollTop = _sel;
|
||||
if (_sel >= _scrollTop + WCC_ROWS) _scrollTop = _sel - WCC_ROWS + 1;
|
||||
}
|
||||
|
||||
public:
|
||||
WatchChannelConfigScreen(UITask* task, NodePrefs* node_prefs)
|
||||
: _task(task), _node_prefs(node_prefs) {
|
||||
_editScope[0] = 0;
|
||||
}
|
||||
|
||||
void enter() {
|
||||
rebuildList();
|
||||
_mode = LIST;
|
||||
_detail = -1;
|
||||
_confirmDelete = false;
|
||||
_wantKeyboard = false;
|
||||
_wantExit = false;
|
||||
}
|
||||
|
||||
// --- handshake (polled from UITask::loop) ---
|
||||
bool wantsKeyboard() const { return _wantKeyboard; }
|
||||
void clearWantKeyboard() { _wantKeyboard = false; }
|
||||
bool wantsExit() const { return _wantExit; }
|
||||
void clearWantExit() { _wantExit = false; }
|
||||
const char* getEditText() const { return _editScope; }
|
||||
|
||||
// Save the keyboard result as the open channel's region scope. An empty
|
||||
// string clears it back to the device default (same as Clear). Returns
|
||||
// NULL on success, else an error message.
|
||||
const char* applyComposedScope(const char* txt) {
|
||||
if (_detail < 0) return "No channel open";
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel((uint8_t)_detail, ch)) return "Channel gone";
|
||||
if (txt == nullptr) txt = "";
|
||||
strncpy(ch.scope_name, txt, sizeof(ch.scope_name));
|
||||
ch.scope_name[30] = '\0';
|
||||
the_mesh.setChannel((uint8_t)_detail, ch);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("WatchChannelCfg: Channel %d scope set to '%s'\n",
|
||||
_detail, ch.scope_name[0] ? ch.scope_name : "(device default)");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering. 120x120 logical.
|
||||
// ---------------------------------------------------------------------------
|
||||
private:
|
||||
void renderList(DisplayDriver& display) {
|
||||
LGFXDisplay* d = (LGFXDisplay*)&display;
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 4, "Channels");
|
||||
|
||||
if (_numCh == 0) {
|
||||
d->setRawColor(0x7BEF);
|
||||
display.drawTextCentered(display.width() / 2, 48, "No channels");
|
||||
}
|
||||
|
||||
char buf[40];
|
||||
int visible = _numCh - _scrollTop;
|
||||
if (visible > WCC_ROWS) visible = WCC_ROWS;
|
||||
for (int i = 0; i < visible; i++) {
|
||||
uint8_t idx = _chIdx[_scrollTop + i];
|
||||
int y = HEADER_H + i * ROW_H;
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel(idx, ch)) continue;
|
||||
|
||||
if (_scrollTop + i == _sel) {
|
||||
d->setRawColor(0x231D); // same blue as the home tile
|
||||
d->drawRoundRect(1, y, display.width() - 2, ROW_H - 2, 3);
|
||||
}
|
||||
d->setRawColor(0xFFFF);
|
||||
char nameBuf[15];
|
||||
strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1);
|
||||
nameBuf[sizeof(nameBuf) - 1] = 0;
|
||||
display.setCursor(5, y + 2);
|
||||
display.print(nameBuf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "[%s] %s",
|
||||
ch.scope_name[0] ? ch.scope_name : "*", notifLabel(idx));
|
||||
d->setRawColor(0x7BEF);
|
||||
d->printSmallFont(5, y + 13, buf);
|
||||
}
|
||||
|
||||
// Footer: Back (left) and, when the list overflows a page, More (right).
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, FOOTER_Y, 58, 18, 3);
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(20, FOOTER_Y + 8, "Back");
|
||||
if (_numCh > WCC_ROWS) {
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(61, FOOTER_Y, 58, 18, 3);
|
||||
d->setRawColor(0xC618);
|
||||
d->printSmallFont(78, FOOTER_Y + 8, "More");
|
||||
}
|
||||
}
|
||||
|
||||
void renderDetail(DisplayDriver& display) {
|
||||
LGFXDisplay* d = (LGFXDisplay*)&display;
|
||||
char buf[44];
|
||||
|
||||
ChannelDetails ch;
|
||||
if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) {
|
||||
_mode = LIST;
|
||||
return;
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
char nameBuf[18];
|
||||
strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1);
|
||||
nameBuf[sizeof(nameBuf) - 1] = 0;
|
||||
display.drawTextCentered(display.width() / 2, 3, nameBuf);
|
||||
|
||||
// Region row -- opens the keyboard.
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, 16, display.width() - 2, 18, 3);
|
||||
snprintf(buf, sizeof(buf), "Region: %s",
|
||||
ch.scope_name[0] ? ch.scope_name : "(default)");
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(6, 24, buf);
|
||||
|
||||
// Clear-region row -- dim when there is nothing to clear.
|
||||
d->setRawColor(ch.scope_name[0] ? 0x231D : 0x18C3);
|
||||
d->drawRoundRect(1, 36, display.width() - 2, 18, 3);
|
||||
d->setRawColor(ch.scope_name[0] ? 0xC618 : 0x4208);
|
||||
d->printSmallFont(6, 44, "Clear region (use default)");
|
||||
|
||||
// Notify row -- tap cycles All -> Mentions -> Off.
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, 56, display.width() - 2, 18, 3);
|
||||
uint8_t n = _node_prefs->channel_notif[(uint8_t)_detail];
|
||||
snprintf(buf, sizeof(buf), "Notify: %s",
|
||||
(n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "Mentions" : "All");
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(6, 64, buf);
|
||||
|
||||
// Delete row -- status only; deletion is driven by the button (see
|
||||
// handleInput). Channel 0 (public) cannot be deleted, same as desktop.
|
||||
bool canDel = (_detail > 0);
|
||||
d->setRawColor(canDel ? 0x231D : 0x18C3);
|
||||
d->drawRoundRect(1, 76, display.width() - 2, 18, 3);
|
||||
d->setRawColor(canDel ? (_confirmDelete ? 0xF800 : 0xC618) : 0x4208);
|
||||
d->printSmallFont(6, 84, !canDel ? "Delete (public ch)"
|
||||
: _confirmDelete ? "Delete? press again" : "Button: delete");
|
||||
|
||||
// Footer: Back to the list.
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, FOOTER_Y, 58, 18, 3);
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(20, FOOTER_Y + 8, "Back");
|
||||
}
|
||||
|
||||
public:
|
||||
int render(DisplayDriver& display) override {
|
||||
if (_confirmDelete && millis() - _confirmAt > 3000) _confirmDelete = false;
|
||||
switch (_mode) {
|
||||
case LIST: renderList(display); return 500;
|
||||
case DETAIL: renderDetail(display); return 500;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
// Physical tap in logical coordinates. The top-strip home tap is handled by
|
||||
// main.cpp before this is called. Returns true if the tap was consumed.
|
||||
bool handleTap(int lx, int ly) {
|
||||
if (_mode == LIST) {
|
||||
if (ly >= FOOTER_Y) {
|
||||
if (lx < 60) { // Back -> Settings
|
||||
_wantExit = true;
|
||||
} else if (_numCh > WCC_ROWS) { // More: next page (wraps)
|
||||
_scrollTop += WCC_ROWS;
|
||||
if (_scrollTop >= _numCh) _scrollTop = 0;
|
||||
_sel = _scrollTop;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ly >= HEADER_H) {
|
||||
int i = (ly - HEADER_H) / ROW_H;
|
||||
int idx = _scrollTop + i;
|
||||
if (i < WCC_ROWS && idx < _numCh) {
|
||||
if (idx == _sel) { // second tap on the row: open it
|
||||
_detail = _chIdx[idx];
|
||||
_mode = DETAIL;
|
||||
_confirmDelete = false;
|
||||
} else {
|
||||
_sel = idx; // first tap: select
|
||||
ensureVisible();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// DETAIL
|
||||
if (ly >= FOOTER_Y) { // Back -> list
|
||||
_confirmDelete = false;
|
||||
_mode = LIST;
|
||||
rebuildList();
|
||||
return true;
|
||||
}
|
||||
ChannelDetails ch;
|
||||
if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) {
|
||||
_mode = LIST;
|
||||
return true;
|
||||
}
|
||||
if (ly >= 16 && ly < 34) { // Region -> keyboard, pre-filled
|
||||
strncpy(_editScope, ch.scope_name, sizeof(_editScope));
|
||||
_editScope[30] = '\0';
|
||||
_wantKeyboard = true;
|
||||
return true;
|
||||
}
|
||||
if (ly >= 36 && ly < 54) { // Clear region
|
||||
if (ch.scope_name[0]) {
|
||||
ch.scope_name[0] = '\0';
|
||||
the_mesh.setChannel((uint8_t)_detail, ch);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("WatchChannelCfg: Channel %d scope cleared\n", _detail);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ly >= 56 && ly < 74) { // Notify: cycle All -> @ -> Off
|
||||
uint8_t cur = _node_prefs->channel_notif[(uint8_t)_detail];
|
||||
_node_prefs->channel_notif[(uint8_t)_detail] = (cur + 1) % 3;
|
||||
the_mesh.savePrefs();
|
||||
return true;
|
||||
}
|
||||
return true; // swallow other taps (incl. delete row)
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// The PMU short press (S3) arrives as KEY_ENTER; the S3 Plus boot button
|
||||
// arrives as KEY_NEXT.
|
||||
if (_mode == LIST && c == KEY_ENTER) {
|
||||
if (_sel < _numCh) {
|
||||
_detail = _chIdx[_sel];
|
||||
_mode = DETAIL;
|
||||
_confirmDelete = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_mode == DETAIL && (c == KEY_ENTER || c == KEY_NEXT)) {
|
||||
// Button click deletes the open channel (channel 0 is protected):
|
||||
// first press arms, a second press within 3s confirms. Back is the
|
||||
// touch footer.
|
||||
if (_detail > 0) {
|
||||
if (_confirmDelete) {
|
||||
deleteChannel((uint8_t)_detail);
|
||||
_confirmDelete = false;
|
||||
_detail = -1;
|
||||
rebuildList();
|
||||
_mode = LIST;
|
||||
} else {
|
||||
_confirmDelete = true;
|
||||
_confirmAt = millis();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -417,7 +417,7 @@ class TWatchKeyboardScreen : public UIScreen {
|
||||
uint8_t _channelIdx;
|
||||
|
||||
public:
|
||||
enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE };
|
||||
enum Purpose { TWKB_CHANNEL, TWKB_DM, TWKB_ADMIN_PASSWORD, TWKB_ADMIN_CLI, TWKB_PATH, TWKB_NOTE, TWKB_SCOPE };
|
||||
|
||||
private:
|
||||
Purpose _purpose;
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
#pragma once
|
||||
|
||||
// =============================================================================
|
||||
// WatchChannelConfigScreen -- per-channel settings for the T-Watch S3 / S3 Plus.
|
||||
//
|
||||
// The SettingsScreen SUB_CHANNELS sub-screen drives every per-channel action
|
||||
// from physical-keyboard hotkeys on the selected row (Ent: edit region scope,
|
||||
// N: cycle notifications, T: tone, X/Hold: delete), none of which exist on the
|
||||
// touch-only watch. This standalone screen replaces it on MECK_TWATCH builds,
|
||||
// following the WatchAlarmScreen / WatchNotesScreen conventions.
|
||||
//
|
||||
// LIST mode shows each channel with its region scope tag and notification
|
||||
// state; tap a row to select it, tap again (or PWR/Enter) to open it. DETAIL
|
||||
// mode offers: Region (opens the watch keyboard via TWKB_SCOPE, pre-filled
|
||||
// with the current scope), Clear region (reverts to the device default),
|
||||
// Notify (tap-cycles All -> Mentions -> Off, mirroring the desktop order),
|
||||
// and Delete via the button (channel 1+ only): a short press of the PWR key
|
||||
// (S3) or the boot button (S3 Plus) arms it, a second press within 3s
|
||||
// confirms. The tone picker is deliberately absent -- no audio path.
|
||||
//
|
||||
// Exit: the footer Back returns to the Settings screen (via a flag polled by
|
||||
// UITask::loop); the universal top-strip tap still goes home.
|
||||
//
|
||||
// Geometry: 240x240 physical, UI_ZOOM=2, so display.width()/height() = 120.
|
||||
// =============================================================================
|
||||
|
||||
// NOTE on include order: mirrors WatchNotesScreen.h -- this header uses
|
||||
// NodePrefs (NOTIF_* constants, channel_notif[]) and the_mesh, which the two
|
||||
// call sites (UITask.cpp, main.cpp) already have in scope before including it.
|
||||
#include <Arduino.h>
|
||||
#include <string.h>
|
||||
#include <MeshCore.h>
|
||||
#include <helpers/ChannelDetails.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/LGFXDisplay.h>
|
||||
|
||||
class UITask;
|
||||
|
||||
#ifndef MAX_GROUP_CHANNELS
|
||||
#define MAX_GROUP_CHANNELS 20
|
||||
#endif
|
||||
|
||||
#define WCC_ROWS 4 // list rows per page
|
||||
|
||||
class WatchChannelConfigScreen : public UIScreen {
|
||||
public:
|
||||
enum Mode { LIST, DETAIL };
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
NodePrefs* _node_prefs;
|
||||
|
||||
uint8_t _chIdx[MAX_GROUP_CHANNELS]; // populated channel slots, in order
|
||||
int _numCh = 0;
|
||||
|
||||
Mode _mode = LIST;
|
||||
int _sel = 0; // selected list row (index into _chIdx)
|
||||
int _scrollTop = 0;
|
||||
int _detail = -1; // channel slot open in DETAIL (value from _chIdx)
|
||||
bool _confirmDelete = false;
|
||||
unsigned long _confirmAt = 0;
|
||||
|
||||
// Keyboard / exit handshake -- UITask::loop() polls these (UITask is only
|
||||
// forward declared here, so this screen cannot call it directly).
|
||||
bool _wantKeyboard = false;
|
||||
bool _wantExit = false;
|
||||
char _editScope[31]; // pre-fill text handed to the keyboard
|
||||
|
||||
// Layout (logical pixels)
|
||||
static const int HEADER_H = 14;
|
||||
static const int ROW_H = 20;
|
||||
static const int FOOTER_Y = 100;
|
||||
|
||||
void rebuildList() {
|
||||
// Scan ALL slots -- the companion app may write non-contiguously, and
|
||||
// gaps can appear after deletion (same scan as SettingsScreen).
|
||||
_numCh = 0;
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
_chIdx[_numCh++] = i;
|
||||
}
|
||||
}
|
||||
if (_sel >= _numCh) _sel = _numCh > 0 ? _numCh - 1 : 0;
|
||||
if (_scrollTop > _sel) _scrollTop = _sel;
|
||||
}
|
||||
|
||||
const char* notifLabel(uint8_t idx) const {
|
||||
uint8_t n = _node_prefs->channel_notif[idx];
|
||||
return (n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "@" : "All";
|
||||
}
|
||||
|
||||
// Replicates SettingsScreen::deleteChannel (kept in sync manually). Note:
|
||||
// channel_notif[] is not shifted during compaction -- this matches the
|
||||
// desktop behaviour exactly.
|
||||
void deleteChannel(uint8_t idx) {
|
||||
ChannelDetails empty;
|
||||
memset(&empty, 0, sizeof(empty));
|
||||
int total = 0;
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
total = i + 1;
|
||||
}
|
||||
}
|
||||
for (int i = idx; i < total - 1; i++) {
|
||||
ChannelDetails next;
|
||||
if (the_mesh.getChannel(i + 1, next)) {
|
||||
the_mesh.setChannel(i, next);
|
||||
}
|
||||
}
|
||||
the_mesh.setChannel(total - 1, empty);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("WatchChannelCfg: Deleted channel at idx %d, compacted %d channels\n", idx, total);
|
||||
}
|
||||
|
||||
void ensureVisible() {
|
||||
if (_sel < _scrollTop) _scrollTop = _sel;
|
||||
if (_sel >= _scrollTop + WCC_ROWS) _scrollTop = _sel - WCC_ROWS + 1;
|
||||
}
|
||||
|
||||
public:
|
||||
WatchChannelConfigScreen(UITask* task, NodePrefs* node_prefs)
|
||||
: _task(task), _node_prefs(node_prefs) {
|
||||
_editScope[0] = 0;
|
||||
}
|
||||
|
||||
void enter() {
|
||||
rebuildList();
|
||||
_mode = LIST;
|
||||
_detail = -1;
|
||||
_confirmDelete = false;
|
||||
_wantKeyboard = false;
|
||||
_wantExit = false;
|
||||
}
|
||||
|
||||
// --- handshake (polled from UITask::loop) ---
|
||||
bool wantsKeyboard() const { return _wantKeyboard; }
|
||||
void clearWantKeyboard() { _wantKeyboard = false; }
|
||||
bool wantsExit() const { return _wantExit; }
|
||||
void clearWantExit() { _wantExit = false; }
|
||||
const char* getEditText() const { return _editScope; }
|
||||
|
||||
// Save the keyboard result as the open channel's region scope. An empty
|
||||
// string clears it back to the device default (same as Clear). Returns
|
||||
// NULL on success, else an error message.
|
||||
const char* applyComposedScope(const char* txt) {
|
||||
if (_detail < 0) return "No channel open";
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel((uint8_t)_detail, ch)) return "Channel gone";
|
||||
if (txt == nullptr) txt = "";
|
||||
strncpy(ch.scope_name, txt, sizeof(ch.scope_name));
|
||||
ch.scope_name[30] = '\0';
|
||||
the_mesh.setChannel((uint8_t)_detail, ch);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("WatchChannelCfg: Channel %d scope set to '%s'\n",
|
||||
_detail, ch.scope_name[0] ? ch.scope_name : "(device default)");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering. 120x120 logical.
|
||||
// ---------------------------------------------------------------------------
|
||||
private:
|
||||
void renderList(DisplayDriver& display) {
|
||||
LGFXDisplay* d = (LGFXDisplay*)&display;
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, 4, "Channels");
|
||||
|
||||
if (_numCh == 0) {
|
||||
d->setRawColor(0x7BEF);
|
||||
display.drawTextCentered(display.width() / 2, 48, "No channels");
|
||||
}
|
||||
|
||||
char buf[40];
|
||||
int visible = _numCh - _scrollTop;
|
||||
if (visible > WCC_ROWS) visible = WCC_ROWS;
|
||||
for (int i = 0; i < visible; i++) {
|
||||
uint8_t idx = _chIdx[_scrollTop + i];
|
||||
int y = HEADER_H + i * ROW_H;
|
||||
ChannelDetails ch;
|
||||
if (!the_mesh.getChannel(idx, ch)) continue;
|
||||
|
||||
if (_scrollTop + i == _sel) {
|
||||
d->setRawColor(0x231D); // same blue as the home tile
|
||||
d->drawRoundRect(1, y, display.width() - 2, ROW_H - 2, 3);
|
||||
}
|
||||
d->setRawColor(0xFFFF);
|
||||
char nameBuf[15];
|
||||
strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1);
|
||||
nameBuf[sizeof(nameBuf) - 1] = 0;
|
||||
display.setCursor(5, y + 2);
|
||||
display.print(nameBuf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "[%s] %s",
|
||||
ch.scope_name[0] ? ch.scope_name : "*", notifLabel(idx));
|
||||
d->setRawColor(0x7BEF);
|
||||
d->printSmallFont(5, y + 13, buf);
|
||||
}
|
||||
|
||||
// Footer: Back (left) and, when the list overflows a page, More (right).
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, FOOTER_Y, 58, 18, 3);
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(20, FOOTER_Y + 8, "Back");
|
||||
if (_numCh > WCC_ROWS) {
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(61, FOOTER_Y, 58, 18, 3);
|
||||
d->setRawColor(0xC618);
|
||||
d->printSmallFont(78, FOOTER_Y + 8, "More");
|
||||
}
|
||||
}
|
||||
|
||||
void renderDetail(DisplayDriver& display) {
|
||||
LGFXDisplay* d = (LGFXDisplay*)&display;
|
||||
char buf[44];
|
||||
|
||||
ChannelDetails ch;
|
||||
if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) {
|
||||
_mode = LIST;
|
||||
return;
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
char nameBuf[18];
|
||||
strncpy(nameBuf, ch.name, sizeof(nameBuf) - 1);
|
||||
nameBuf[sizeof(nameBuf) - 1] = 0;
|
||||
display.drawTextCentered(display.width() / 2, 3, nameBuf);
|
||||
|
||||
// Region row -- opens the keyboard.
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, 16, display.width() - 2, 18, 3);
|
||||
snprintf(buf, sizeof(buf), "Region: %s",
|
||||
ch.scope_name[0] ? ch.scope_name : "(default)");
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(6, 24, buf);
|
||||
|
||||
// Clear-region row -- dim when there is nothing to clear.
|
||||
d->setRawColor(ch.scope_name[0] ? 0x231D : 0x18C3);
|
||||
d->drawRoundRect(1, 36, display.width() - 2, 18, 3);
|
||||
d->setRawColor(ch.scope_name[0] ? 0xC618 : 0x4208);
|
||||
d->printSmallFont(6, 44, "Clear region (use default)");
|
||||
|
||||
// Notify row -- tap cycles All -> Mentions -> Off.
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, 56, display.width() - 2, 18, 3);
|
||||
uint8_t n = _node_prefs->channel_notif[(uint8_t)_detail];
|
||||
snprintf(buf, sizeof(buf), "Notify: %s",
|
||||
(n == NOTIF_NONE) ? "Off" : (n == NOTIF_MENTIONS) ? "Mentions" : "All");
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(6, 64, buf);
|
||||
|
||||
// Delete row -- status only; deletion is driven by the button (see
|
||||
// handleInput). Channel 0 (public) cannot be deleted, same as desktop.
|
||||
bool canDel = (_detail > 0);
|
||||
d->setRawColor(canDel ? 0x231D : 0x18C3);
|
||||
d->drawRoundRect(1, 76, display.width() - 2, 18, 3);
|
||||
d->setRawColor(canDel ? (_confirmDelete ? 0xF800 : 0xC618) : 0x4208);
|
||||
d->printSmallFont(6, 84, !canDel ? "Delete (public ch)"
|
||||
: _confirmDelete ? "Delete? press again" : "Button: delete");
|
||||
|
||||
// Footer: Back to the list.
|
||||
d->setRawColor(0x231D);
|
||||
d->drawRoundRect(1, FOOTER_Y, 58, 18, 3);
|
||||
d->setRawColor(0xFFFF);
|
||||
d->printSmallFont(20, FOOTER_Y + 8, "Back");
|
||||
}
|
||||
|
||||
public:
|
||||
int render(DisplayDriver& display) override {
|
||||
if (_confirmDelete && millis() - _confirmAt > 3000) _confirmDelete = false;
|
||||
switch (_mode) {
|
||||
case LIST: renderList(display); return 500;
|
||||
case DETAIL: renderDetail(display); return 500;
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
// Physical tap in logical coordinates. The top-strip home tap is handled by
|
||||
// main.cpp before this is called. Returns true if the tap was consumed.
|
||||
bool handleTap(int lx, int ly) {
|
||||
if (_mode == LIST) {
|
||||
if (ly >= FOOTER_Y) {
|
||||
if (lx < 60) { // Back -> Settings
|
||||
_wantExit = true;
|
||||
} else if (_numCh > WCC_ROWS) { // More: next page (wraps)
|
||||
_scrollTop += WCC_ROWS;
|
||||
if (_scrollTop >= _numCh) _scrollTop = 0;
|
||||
_sel = _scrollTop;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ly >= HEADER_H) {
|
||||
int i = (ly - HEADER_H) / ROW_H;
|
||||
int idx = _scrollTop + i;
|
||||
if (i < WCC_ROWS && idx < _numCh) {
|
||||
if (idx == _sel) { // second tap on the row: open it
|
||||
_detail = _chIdx[idx];
|
||||
_mode = DETAIL;
|
||||
_confirmDelete = false;
|
||||
} else {
|
||||
_sel = idx; // first tap: select
|
||||
ensureVisible();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// DETAIL
|
||||
if (ly >= FOOTER_Y) { // Back -> list
|
||||
_confirmDelete = false;
|
||||
_mode = LIST;
|
||||
rebuildList();
|
||||
return true;
|
||||
}
|
||||
ChannelDetails ch;
|
||||
if (_detail < 0 || !the_mesh.getChannel((uint8_t)_detail, ch)) {
|
||||
_mode = LIST;
|
||||
return true;
|
||||
}
|
||||
if (ly >= 16 && ly < 34) { // Region -> keyboard, pre-filled
|
||||
strncpy(_editScope, ch.scope_name, sizeof(_editScope));
|
||||
_editScope[30] = '\0';
|
||||
_wantKeyboard = true;
|
||||
return true;
|
||||
}
|
||||
if (ly >= 36 && ly < 54) { // Clear region
|
||||
if (ch.scope_name[0]) {
|
||||
ch.scope_name[0] = '\0';
|
||||
the_mesh.setChannel((uint8_t)_detail, ch);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("WatchChannelCfg: Channel %d scope cleared\n", _detail);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (ly >= 56 && ly < 74) { // Notify: cycle All -> @ -> Off
|
||||
uint8_t cur = _node_prefs->channel_notif[(uint8_t)_detail];
|
||||
_node_prefs->channel_notif[(uint8_t)_detail] = (cur + 1) % 3;
|
||||
the_mesh.savePrefs();
|
||||
return true;
|
||||
}
|
||||
return true; // swallow other taps (incl. delete row)
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// The PMU short press (S3) arrives as KEY_ENTER; the S3 Plus boot button
|
||||
// arrives as KEY_NEXT.
|
||||
if (_mode == LIST && c == KEY_ENTER) {
|
||||
if (_sel < _numCh) {
|
||||
_detail = _chIdx[_sel];
|
||||
_mode = DETAIL;
|
||||
_confirmDelete = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_mode == DETAIL && (c == KEY_ENTER || c == KEY_NEXT)) {
|
||||
// Button click deletes the open channel (channel 0 is protected):
|
||||
// first press arms, a second press within 3s confirms. Back is the
|
||||
// touch footer.
|
||||
if (_detail > 0) {
|
||||
if (_confirmDelete) {
|
||||
deleteChannel((uint8_t)_detail);
|
||||
_confirmDelete = false;
|
||||
_detail = -1;
|
||||
rebuildList();
|
||||
_mode = LIST;
|
||||
} else {
|
||||
_confirmDelete = true;
|
||||
_confirmAt = millis();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user