mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(ui-new): message alert setting -- banner, corner envelope or none
Display > Message alert: Normal keeps the sliding banner; Compact shows an envelope with the unread count beside the battery (in the header line where there's no status row), blinking for 2 s; Off shows nothing. The blink only ever brings the next frame earlier, so it no longer holds back the UI's animations while it runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,6 +72,11 @@ the top of the screen and slide away after a moment; on e-ink they just
|
||||
appear and go. A message's lasts two seconds. Typing on the keyboard isn't
|
||||
interrupted.
|
||||
|
||||
Settings › Display › Message alert picks how a new message shows: **Normal**
|
||||
(the banner), **Compact** (an envelope and a count in the top-right corner,
|
||||
blinking for a moment, covering nothing else) or **Off** (no alert; the sound
|
||||
and waking the screen still follow their own settings).
|
||||
|
||||
The battery sits at the top right, as an icon, a percentage or the voltage
|
||||
(Settings › Display). A small bolt beside it means external power is
|
||||
connected over USB.
|
||||
|
||||
@@ -7,7 +7,7 @@ board supports are shown.
|
||||
|
||||
| Section | What's there |
|
||||
| ------- | ------------ |
|
||||
| **Display** | Brightness, auto-off, wake on message, lock screen, the lock clock's look and the [PIN](./lock.md), battery as icon / % / volts, time zone, clock format and seconds; on e-ink also rotation and full refresh |
|
||||
| **Display** | Brightness, auto-off, wake on message, the message alert (banner, corner envelope or off), lock screen, the lock clock's look and the [PIN](./lock.md), battery as icon / % / volts, time zone, clock format and seconds; on e-ink also rotation and full refresh |
|
||||
| **Sound** | Buzzer on / off / auto (quiet while the app is connected), volume, quiet hours, the melody for messages, channels and adverts |
|
||||
| **Home Pages** | Order of the home pages, and which are shown |
|
||||
| **Radio** | TX power, preset, frequency, SF / BW / CR, saved presets, Auto pwr, the scope list |
|
||||
|
||||
@@ -333,6 +333,7 @@ static void sanitize(NodePrefs& p) {
|
||||
if (!battery::validCurve(p.batt_curve_mv)) memset(p.batt_curve_mv, 0, sizeof(p.batt_curve_mv));
|
||||
flag(p.lock_compact, 0);
|
||||
flag(p.ble_off, 0);
|
||||
if (p.msg_alert > NodePrefs::MSG_ALERT_OFF) p.msg_alert = NodePrefs::MSG_ALERT_NORMAL;
|
||||
if (!p.ch_scope_follow) {
|
||||
for (uint8_t i = 0; i < NodePrefs::MAX_SCOPED_CHANNELS; i++)
|
||||
if (p.ch_scope_idx[i] == 0) p.ch_scope_idx[i] = NodePrefs::CH_SCOPE_DEFAULT;
|
||||
|
||||
@@ -532,6 +532,12 @@ struct NodePrefs { // persisted to file
|
||||
// default" on load (DataStore sanitize).
|
||||
uint8_t ch_scope_follow;
|
||||
|
||||
// ── New-message alert (ui-new) ─────────────────────────────────────────
|
||||
// How a message arriving shows: the banner from the top, an envelope with
|
||||
// a count in the corner, or nothing (the sound and the wake still happen).
|
||||
static const uint8_t MSG_ALERT_NORMAL = 0, MSG_ALERT_COMPACT = 1, MSG_ALERT_OFF = 2;
|
||||
uint8_t msg_alert;
|
||||
|
||||
// Single source of truth for the live-share option tables (shared by the Map
|
||||
// UI labels and the auto-send engine in UITask).
|
||||
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
|
||||
|
||||
@@ -131,6 +131,10 @@ static void optBattDisplay(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
static const char* L[] = { "Icon", "Percent", "Voltage" };
|
||||
snprintf(b, n, "%s", L[v < 3 ? v : 0]);
|
||||
}
|
||||
static void optMsgAlert(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
static const char* const L[] = { "Normal", "Compact", "Off" };
|
||||
snprintf(b, n, "%s", L[v < 3 ? v : 0]);
|
||||
}
|
||||
static void optLockLook(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", v ? "Compact" : "Big"); }
|
||||
static void optExpiry(uint8_t v, char* b, int n, const NodePrefs&) {
|
||||
snprintf(b, n, "%s", NodePrefs::contactExpiryLabel(v));
|
||||
@@ -196,6 +200,7 @@ static const Setting ALL[] = {
|
||||
IDX("Brightness", nullptr, SEC_DISPLAY, display_brightness, 5, optBrightness, applyDisplay),
|
||||
MAP("Screen off after", "Without a touch", SEC_DISPLAY, auto_off_secs, AUTO_OFF, optAutoOff, nullptr),
|
||||
SW("Wake on message", "Screen on for new messages", SEC_DISPLAY, msg_wake, nullptr),
|
||||
IDX("Message alert", "Banner, corner envelope or none", SEC_DISPLAY, msg_alert, 3, optMsgAlert, nullptr),
|
||||
SW("Lock screen", "Slide to unlock after sleep", SEC_DISPLAY, auto_lock, nullptr),
|
||||
IDX("Lock clock", "Big, or compact with the clock fields", SEC_DISPLAY, lock_compact, 2, optLockLook, nullptr),
|
||||
IDX("Battery display", "In the status bar", SEC_DISPLAY, batt_display_mode, 3, optBattDisplay, nullptr),
|
||||
@@ -234,6 +239,7 @@ static const int COUNT = (int)(sizeof(ALL) / sizeof(ALL[0]));
|
||||
static const struct { uint16_t offset; const char* text; } SHORT_LABELS[] = {
|
||||
{ NP_OFF(display_brightness), "Bright" }, { NP_OFF(auto_off_secs), "Auto off" },
|
||||
{ NP_OFF(msg_wake), "Msg wake" }, { NP_OFF(auto_lock), "Auto lock" },
|
||||
{ NP_OFF(msg_alert), "Msg alert" },
|
||||
{ NP_OFF(batt_display_mode), "Batt disp" }, { NP_OFF(low_batt_mv), "Low batt" },
|
||||
{ NP_OFF(gps_interval), "GPS pwr" }, { NP_OFF(clock_12h), "12h clock" },
|
||||
{ NP_OFF(clock_hide_seconds), "Seconds" }, { NP_OFF(units_imperial), "Imperial" },
|
||||
|
||||
@@ -4434,6 +4434,7 @@ void UITask::schemaSection(int i) {
|
||||
if (sec == settings::SEC_SOUND) buildSoundRows(card, true); // On / Off / Auto
|
||||
for (int k = i; k < settings::COUNT && settings::ALL[k].section == sec; k++) {
|
||||
if (settings::ALL[k].offset == offsetof(NodePrefs, lock_compact)) continue; // ui-new's lock look; the slide card has one
|
||||
if (settings::ALL[k].offset == offsetof(NodePrefs, msg_alert)) continue; // ui-new's alert; the L2 has its own
|
||||
schemaRow(card, k);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,6 +795,16 @@ class HomeScreen : public UIScreen {
|
||||
|
||||
int x = battLeftX;
|
||||
const int name_min = (reserve_left >= 0) ? reserve_left : display.getCharWidth() * 5;
|
||||
// Settings > Message alert Compact: the envelope and its count, first by the
|
||||
// battery; its place is kept through the blink so nothing beside it jumps.
|
||||
if (const int n = _task->msgBadgeCount()) {
|
||||
const int ix = x - mailCountWidth(display, n) - ind_gap - 2 * miniIconScale(display); // the icons' boxes pad theirs
|
||||
if (ix >= name_min) {
|
||||
if (_task->msgBadgeLit()) drawMailCount(display, ix, 0, ind_h, n);
|
||||
_task->msgBadgeInBar();
|
||||
x = ix;
|
||||
}
|
||||
}
|
||||
for (const Sicon& s : icons) {
|
||||
if (!s.active) continue;
|
||||
int ix = x - ind - ind_gap;
|
||||
@@ -2191,6 +2201,15 @@ void UITask::gotoMessagesScreen() {
|
||||
}
|
||||
|
||||
#ifdef SIM_PLATFORM
|
||||
void UITask::simMessage(const char* text) {
|
||||
UiEvent ev = {};
|
||||
ev.type = UiEventType::MessageArrived;
|
||||
ev.kind = UIEventType::channelMessage;
|
||||
ev.idx = 0;
|
||||
snprintf(ev.text, sizeof(ev.text), "%s", text);
|
||||
onMessageArrived(ev);
|
||||
}
|
||||
|
||||
void UITask::simCompose(const char* text) {
|
||||
gotoMessagesScreen();
|
||||
((MessagesScreen*)messages_screen)->simCompose(text);
|
||||
@@ -2301,7 +2320,8 @@ void UITask::clearAllDMUnread() { _core->clearAllDMUnread(); }
|
||||
|
||||
void UITask::showAlert(const char* text, int duration_millis) {
|
||||
snprintf(_alert, sizeof(_alert), "%s", text);
|
||||
if (!alertShowing()) _alert_t0 = millis(); // a banner already down only changes its text
|
||||
if (!alertShowing() || _alert_badge) _alert_t0 = millis(); // a banner already down only changes its text
|
||||
_alert_badge = false;
|
||||
_alert_expiry = (millis() + duration_millis) | 1; // 0 means none
|
||||
}
|
||||
|
||||
@@ -2360,7 +2380,14 @@ void UITask::onMessageArrived(const UiEvent& ev) {
|
||||
bool in_view = curr == messages_screen && _display && _display->isOn() && !_locked &&
|
||||
((ev.kind == UIEventType::contactMessage && ev.flag && isViewingDM(ev.key)) ||
|
||||
(ev.kind == UIEventType::channelMessage && ev.idx >= 0 && isViewingChannel((uint8_t)ev.idx)));
|
||||
if (!in_view) {
|
||||
const uint8_t style = _node_prefs ? _node_prefs->msg_alert : NodePrefs::MSG_ALERT_NORMAL;
|
||||
if (!in_view && style == NodePrefs::MSG_ALERT_COMPACT) {
|
||||
// The envelope counts the run: each message adds one and blinks it again.
|
||||
_alert_count = alertShowing() && _alert_badge ? _alert_count + 1 : 1;
|
||||
_alert_badge = true;
|
||||
_alert_t0 = millis();
|
||||
_alert_expiry = (millis() + BADGE_MS) | 1;
|
||||
} else if (!in_view && style == NodePrefs::MSG_ALERT_NORMAL) {
|
||||
char alert_buf[80];
|
||||
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", ev.text);
|
||||
showAlert(alert_buf, 2000);
|
||||
@@ -2409,6 +2436,7 @@ void UITask::userLedHandler() {
|
||||
// banner instead. Uses the shared wrap scratch (s_wrap_*) -- single-threaded
|
||||
// render path, same contract as the message views.
|
||||
void UITask::renderAlertOverlay() {
|
||||
if (_alert_badge) { renderMsgBadge(); return; }
|
||||
_display->setTextSize(1);
|
||||
const int lh = _display->getLineHeight();
|
||||
const int pad = 3;
|
||||
@@ -2436,6 +2464,21 @@ void UITask::renderAlertOverlay() {
|
||||
_display->drawTextCentered(W / 2, y + pad + i * lh, s_wrap_lines[i]);
|
||||
}
|
||||
|
||||
// Settings > Message alert Compact: an envelope and how many messages came. On
|
||||
// Home it sits among the status icons (HomeScreen::renderBatteryIndicator);
|
||||
// it blinks for a while after each one (not on e-ink), then holds till it goes.
|
||||
void UITask::renderMsgBadge() {
|
||||
if (_badge_in_bar) return; // among the Home status icons already
|
||||
// Elsewhere in the header's line, at its right, clear of the line under it.
|
||||
DisplayDriver& d = *_display;
|
||||
const int s = miniIconScale(d), h = d.headerH() - d.sepH(), w = mailCountWidth(d, _alert_count);
|
||||
const int x = d.width() - w - 2 * s;
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
d.fillRect(x - 2 * s, 0, w + 4 * s, h);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
if (msgBadgeLit()) drawMailCount(d, x, 0, h, _alert_count);
|
||||
}
|
||||
|
||||
void UITask::setCurrScreen(UIScreen* c) {
|
||||
// Fail safe on a null target: a screen pointer left uninitialised (member
|
||||
// declared + navigator wired, but the `new XScreen()` line forgotten in
|
||||
@@ -2673,6 +2716,14 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_compose(const char* text) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simCompose(text);
|
||||
}
|
||||
|
||||
// Sim tests: a channel message arrives, alerted in Settings > Message alert's
|
||||
// style (0 Normal, 1 Compact, 2 Off).
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_message(const char* text, int style) {
|
||||
if (!g_sim_ui_task_for_js) return;
|
||||
if (NodePrefs* p = g_sim_ui_task_for_js->getNodePrefs()) p->msg_alert = (uint8_t)style;
|
||||
g_sim_ui_task_for_js->simMessage(text);
|
||||
}
|
||||
|
||||
// Long-press counterpart -- the host page's own press-and-hold timer (see
|
||||
// web/index.html/mesh.html) calls this instead of sim_enqueue_key() once a
|
||||
// button/key has been held past the same ~1000ms threshold every real
|
||||
@@ -3357,6 +3408,7 @@ void UITask::loop() {
|
||||
// While the prompt is up the password keyboard replaces the lockscreen view
|
||||
PERF_T0();
|
||||
_display->startFrame();
|
||||
_badge_in_bar = false;
|
||||
_kb.beginFrame();
|
||||
int delay_millis = _kb.render(*_display);
|
||||
if (alertShowing()) renderAlertOverlay(); // "Wrong PIN", and a ringing alarm
|
||||
@@ -3364,10 +3416,11 @@ void UITask::loop() {
|
||||
_display->endFrame();
|
||||
PERF_T2();
|
||||
_next_refresh = millis() + delay_millis;
|
||||
if (alertSliding()) _next_refresh = millis() + 25;
|
||||
alertFrameCap();
|
||||
} else if (_locked && refreshDue() && home) {
|
||||
PERF_T0();
|
||||
_display->startFrame();
|
||||
_badge_in_bar = false;
|
||||
if (curr && curr != home && (millis() - ui_started_at < BOOT_SCREEN_MILLIS)) {
|
||||
// Boot splash is still up on a boot-locked device
|
||||
_next_refresh = millis() + curr->render(*_display);
|
||||
@@ -3379,7 +3432,7 @@ void UITask::loop() {
|
||||
// played its melody against a screen that never said what was ringing.
|
||||
if (alertShowing()) {
|
||||
renderAlertOverlay();
|
||||
if (alertSliding()) _next_refresh = millis() + 25;
|
||||
alertFrameCap();
|
||||
}
|
||||
PERF_T1();
|
||||
_display->endFrame();
|
||||
@@ -3387,6 +3440,7 @@ void UITask::loop() {
|
||||
} else if (!_locked && refreshDue() && curr) {
|
||||
PERF_T0();
|
||||
_display->startFrame();
|
||||
_badge_in_bar = false;
|
||||
_kb.beginFrame();
|
||||
int delay_millis = curr->render(*_display);
|
||||
// Skip the alert overlay (new-message toast) while the keyboard is the
|
||||
@@ -3402,7 +3456,7 @@ void UITask::loop() {
|
||||
// history scrollbar reserve — don't stay stuck behind the alert. Unchanged
|
||||
// frames are skipped by the display CRC, so e-ink isn't thrashed.
|
||||
_next_refresh = millis() + delay_millis;
|
||||
if (alertSliding()) _next_refresh = millis() + 25; // the slide, a frame at a time
|
||||
alertFrameCap(); // the slide or the blink, a frame at a time
|
||||
if ((int32_t)(_next_refresh - _alert_expiry) > 0) _next_refresh = _alert_expiry;
|
||||
} else {
|
||||
_next_refresh = millis() + delay_millis;
|
||||
|
||||
@@ -80,6 +80,12 @@ class UITask : public UITaskBase, public UiCoreHost {
|
||||
KeyboardWidget _kb; // shared across all screens — only one active at a time
|
||||
unsigned long _alert_expiry;
|
||||
unsigned long _alert_t0 = 0; // when the banner began to come down
|
||||
// Settings > Message alert Compact: the alert is an envelope and a count
|
||||
// in the top-right corner instead of the banner (renderMsgBadge()).
|
||||
bool _alert_badge = false;
|
||||
int _alert_count = 0; // messages in this run of the envelope
|
||||
bool _badge_in_bar = false; // this frame's Home status bar drew it
|
||||
static const int BADGE_BLINK_MS = 350, BADGE_BLINK_FOR = 2100, BADGE_MS = 3000;
|
||||
int _last_notif_ch_idx;
|
||||
uint8_t _last_notif_dm_prefix[4];
|
||||
bool _last_notif_dm_valid;
|
||||
@@ -244,6 +250,7 @@ private:
|
||||
// Shared by the normal render path and the lock screen (so a ringing
|
||||
// alarm's label is visible while locked).
|
||||
void renderAlertOverlay();
|
||||
void renderMsgBadge();
|
||||
|
||||
public:
|
||||
// Lock now, from the Home Power panel (unlocking stays the gesture / PIN prompt).
|
||||
@@ -276,6 +283,7 @@ public:
|
||||
void gotoMessagesScreen();
|
||||
#ifdef SIM_PLATFORM
|
||||
void simCompose(const char* text); // sim tests: a channel post's keyboard holding `text`
|
||||
void simMessage(const char* text); // sim tests: a channel message arrives
|
||||
#endif
|
||||
void openContactDM(const ContactInfo& ci);
|
||||
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
||||
@@ -355,12 +363,30 @@ public:
|
||||
// Wrap-safe (millis() rolls over after ~49.7 days). _next_refresh 0 = draw
|
||||
// on the next pass; _alert_expiry 0 = no alert.
|
||||
bool refreshDue() const { return !_next_refresh || (int32_t)(millis() - _next_refresh) >= 0; }
|
||||
// The banner is on its way in or out (not on e-ink): redraw every frame.
|
||||
bool alertSliding() const {
|
||||
return alertShowing() && !_display->isEink() &&
|
||||
((int32_t)(millis() - _alert_t0) < 150 || (int32_t)(_alert_expiry - millis()) < 150);
|
||||
// The banner on its way in or out, or the envelope blinking (not on e-ink):
|
||||
// the ms to the next frame it needs, 0 = none sooner than the screen's own.
|
||||
int alertFrameMs() const {
|
||||
if (!alertShowing() || _display->isEink()) return 0;
|
||||
const int el = (int)(millis() - _alert_t0);
|
||||
if (_alert_badge) return el < BADGE_BLINK_FOR ? BADGE_BLINK_MS - el % BADGE_BLINK_MS : 0;
|
||||
return (el < 150 || (int32_t)(_alert_expiry - millis()) < 150) ? 25 : 0;
|
||||
}
|
||||
// Brings the next redraw forward to the alert's next frame, never later than
|
||||
// the screen asked for (its own animation keeps its pace under the blink).
|
||||
void alertFrameCap() {
|
||||
const int f = alertFrameMs();
|
||||
if (f && (int32_t)(_next_refresh - (millis() + f)) > 0) _next_refresh = millis() + f;
|
||||
}
|
||||
bool alertShowing() const { return _alert_expiry && (int32_t)(_alert_expiry - millis()) > 0; }
|
||||
// Message alert Compact: how many messages the envelope counts (0 = none
|
||||
// up), whether it is lit (it blinks a while after each), and the Home status
|
||||
// bar saying it drew it among its icons, so the overlay stays out.
|
||||
int msgBadgeCount() const { return alertShowing() && _alert_badge ? _alert_count : 0; }
|
||||
bool msgBadgeLit() const {
|
||||
const int el = (int)(millis() - _alert_t0);
|
||||
return _display->isEink() || el >= BADGE_BLINK_FOR || (el / BADGE_BLINK_MS) % 2 == 0;
|
||||
}
|
||||
void msgBadgeInBar() { _badge_in_bar = true; }
|
||||
// Clock tools engine API (ui-core/ClockEngine.h) — ClockToolsScreen drives
|
||||
// these; the engine runs from tickCore() so it fires regardless of the screen.
|
||||
void onAlarmChanged();
|
||||
|
||||
@@ -549,6 +549,15 @@ MINI_ICON(ICON_CHART, 9, // ascending bars — Home › Status
|
||||
packRow("..#.#.#.#"),
|
||||
packRow("#.#.#.#.#"),
|
||||
packRow("#########"));
|
||||
MINI_ICON(ICON_MAIL, 11, // envelope -- the compact new-message alert
|
||||
packRow("###########"),
|
||||
packRow("##.......##"),
|
||||
packRow("#.#.....#.#"),
|
||||
packRow("#..#...#..#"),
|
||||
packRow("#...#.#...#"),
|
||||
packRow("#....#....#"),
|
||||
packRow("#.........#"),
|
||||
packRow("###########"));
|
||||
#elif MINI_ICONS_LARGE
|
||||
MINI_ICON(ICON_MUTE, 8, // speaker + cross (sound off)
|
||||
packRow("...#...."),
|
||||
@@ -631,6 +640,14 @@ MINI_ICON(ICON_CHART, 7, // ascending bars — Home › Status
|
||||
packRow("..#.#.#"),
|
||||
packRow("#.#.#.#"),
|
||||
packRow("#######"));
|
||||
MINI_ICON(ICON_MAIL, 9, // envelope -- the compact new-message alert
|
||||
packRow("#########"),
|
||||
packRow("##.....##"),
|
||||
packRow("#.#...#.#"),
|
||||
packRow("#..#.#..#"),
|
||||
packRow("#...#...#"),
|
||||
packRow("#.......#"),
|
||||
packRow("#########"));
|
||||
#else
|
||||
MINI_ICON(ICON_MUTE, 6, // speaker + cross (sound off)
|
||||
packRow("..#..."),
|
||||
@@ -706,8 +723,31 @@ MINI_ICON(ICON_CHART, 5, // ascending bars — Home › Status
|
||||
packRow("#.#.#"),
|
||||
packRow("#.#.#"),
|
||||
packRow("#####"));
|
||||
MINI_ICON(ICON_MAIL, 7, // envelope -- the compact new-message alert
|
||||
packRow("#######"),
|
||||
packRow("##...##"),
|
||||
packRow("#.#.#.#"),
|
||||
packRow("#..#..#"),
|
||||
packRow("#######"));
|
||||
#endif
|
||||
|
||||
// Settings > Message alert Compact: an envelope and the count beside it in
|
||||
// mini digits, centred in a box box_h tall from y -- the status icons' row,
|
||||
// or a header's line.
|
||||
inline int mailCountWidth(DisplayDriver& d, int n) {
|
||||
const int s = miniIconScale(d);
|
||||
return ICON_MAIL.w * s + 2 * s + miniIconNumberWidth(d, n);
|
||||
}
|
||||
inline void drawMailCount(DisplayDriver& d, int x, int y, int box_h, int n) {
|
||||
const int s = miniIconScale(d);
|
||||
miniIconDrawTop(d, x, y + (box_h - ICON_MAIL.h * s) / 2, ICON_MAIL);
|
||||
x += ICON_MAIL.w * s + 2 * s;
|
||||
const int dy = y + (box_h - ICON_DIGIT_0.h * s) / 2;
|
||||
if (n > 99) n = 99;
|
||||
if (n >= 10) { miniIconDrawTop(d, x, dy, *MINI_ICON_DIGITS[n / 10]); x += ICON_DIGIT_0.w * s + s; }
|
||||
miniIconDrawTop(d, x, dy, *MINI_ICON_DIGITS[n % 10]);
|
||||
}
|
||||
|
||||
// Home-carousel page glyphs — a uniform 5x5 set, deliberately smaller than the
|
||||
// menu/status icons above, used in place of the page-indicator dots. One per
|
||||
// HomePage; see UITask HomeScreen::pageIcon().
|
||||
|
||||
Reference in New Issue
Block a user