mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VeBrxKyHHxWsmttVkUdBrj
515 lines
22 KiB
C++
515 lines
22 KiB
C++
#pragma once
|
|
// Clock tools (Home > Clock): alarm, countdown timer, stopwatch and this
|
|
// month's calendar -- ui-new's Tools > Clock on a touch screen (the calendar
|
|
// as ui-new's e-ink has it under the clock). The alarm (NodePrefs alarm_*) and the
|
|
// countdown run in the Core's ClockEngine, so they fire on any screen and while
|
|
// the display sleeps; the stopwatch is view state here, as in ui-new. Whatever
|
|
// fires brings up a full-screen card with Dismiss (silent: the L2 speaker has
|
|
// no driver yet).
|
|
//
|
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp after NavMap.h.
|
|
|
|
namespace clockview {
|
|
|
|
enum : uint8_t { TAB_ALARM, TAB_TIMER, TAB_STOPWATCH, TAB_CALENDAR, TAB_COUNT };
|
|
enum : uint8_t { ACT_START_STOP, ACT_RESET };
|
|
|
|
static uint8_t s_tab = TAB_ALARM;
|
|
static uint8_t s_timer_h = 0, s_timer_m = 5, s_timer_s = 0; // countdown to start
|
|
static bool s_sw_running = false;
|
|
static uint32_t s_sw_start = 0, s_sw_accum = 0;
|
|
static bool s_shown_running = false; // timer state the screen was built for
|
|
static lv_obj_t* s_big = nullptr; // timer remaining / stopwatch reading
|
|
static lv_obj_t* s_go_lbl = nullptr; // Start / Stop
|
|
static lv_obj_t* s_ring = nullptr; // "Timer done" card on the top layer
|
|
static lv_obj_t* s_ring_lbl = nullptr;
|
|
static lv_obj_t* s_alarm_sw = nullptr; // follows a time change (setting it arms the alarm)
|
|
|
|
static struct tm s_cal_tm; // the day the calendar was drawn for (local)
|
|
static uint32_t s_cal_marks = 0; // its alarm days, bit (day - 1)
|
|
static lv_obj_t* s_cal = nullptr;
|
|
|
|
// The month as a grid, weeks from Monday: the weekday names, today in an
|
|
// accent box, the alarm's days with an accent bar under them, the weekend
|
|
// dimmer.
|
|
static void onCalendarDraw(lv_event_t* e) {
|
|
lv_obj_t* o = (lv_obj_t*)lv_event_get_target(e);
|
|
lv_layer_t* layer = lv_event_get_layer(e);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(o, &a);
|
|
const struct tm& t = s_cal_tm;
|
|
static const char* const NAMES[] = { "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" };
|
|
const int W = a.x2 - a.x1 + 1, cw = W / 7, x0 = a.x1 + (W - 7 * cw) / 2;
|
|
const int head = 16, rows = calmath::weeks(t), ch = (a.y2 - a.y1 + 1 - head) / rows;
|
|
lv_draw_label_dsc_t td;
|
|
lv_draw_label_dsc_init(&td);
|
|
td.align = LV_TEXT_ALIGN_CENTER;
|
|
td.font = THEME_FONT_SMALL;
|
|
for (int c = 0; c < 7; c++) {
|
|
td.color = lv_color_hex(theme::TEXT_MUTED);
|
|
td.text = NAMES[c];
|
|
lv_area_t la = { x0 + c * cw, a.y1, x0 + (c + 1) * cw - 1, a.y1 + head - 1 };
|
|
lv_draw_label(layer, &td, &la);
|
|
}
|
|
td.font = THEME_FONT_BODY;
|
|
const int fh = lv_font_get_line_height(td.font), col0 = calmath::firstCol(t), n = calmath::daysIn(t.tm_year, t.tm_mon);
|
|
char buf[3];
|
|
for (int day = 1; day <= n; day++) {
|
|
const int k = col0 + day - 1, cx = x0 + (k % 7) * cw, cy = a.y1 + head + (k / 7) * ch;
|
|
const bool today = day == t.tm_mday;
|
|
if (today) {
|
|
lv_draw_rect_dsc_t rd;
|
|
lv_draw_rect_dsc_init(&rd);
|
|
rd.radius = theme::RADIUS_SM;
|
|
rd.bg_color = lv_color_hex(theme::ACCENT);
|
|
const int bh = ch - 2 < fh + 2 ? ch - 2 : fh + 2, by = cy + (ch - bh) / 2;
|
|
lv_area_t ra = { cx + 4, by, cx + cw - 5, by + bh - 1 };
|
|
lv_draw_rect(layer, &rd, &ra);
|
|
}
|
|
snprintf(buf, sizeof(buf), "%d", day);
|
|
td.text = buf;
|
|
td.color = lv_color_hex(today ? theme::BG : k % 7 >= 5 ? theme::TEXT_MUTED : theme::TEXT);
|
|
lv_area_t la = { cx, cy + (ch - fh) / 2, cx + cw - 1, cy + (ch + fh) / 2 };
|
|
lv_draw_label(layer, &td, &la);
|
|
if (s_cal_marks & (1UL << (day - 1))) { // under the number
|
|
lv_draw_rect_dsc_t md;
|
|
lv_draw_rect_dsc_init(&md);
|
|
md.radius = 1;
|
|
md.bg_color = lv_color_hex(today ? theme::BG : theme::ACCENT);
|
|
lv_area_t ma = { cx + cw / 2 - 6, cy + ch - 2, cx + cw / 2 + 6, cy + ch - 1 };
|
|
lv_draw_rect(layer, &md, &ma);
|
|
}
|
|
}
|
|
}
|
|
|
|
static uint32_t swElapsed() { return s_sw_accum + (s_sw_running ? millis() - s_sw_start : 0); }
|
|
|
|
// Countdown: rounds up, so "00:01" shows until it actually fires.
|
|
static void fmtRemaining(char* b, int n, uint32_t ms) {
|
|
uint32_t t = (ms + 999) / 1000;
|
|
if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60));
|
|
else snprintf(b, n, "%02lu:%02lu", (unsigned long)(t / 60), (unsigned long)(t % 60));
|
|
}
|
|
static void fmtStopwatch(char* b, int n, uint32_t ms) {
|
|
uint32_t t = ms / 1000;
|
|
if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60));
|
|
else snprintf(b, n, "%02lu:%02lu.%lu", (unsigned long)(t / 60), (unsigned long)(t % 60), (unsigned long)(ms / 100 % 10));
|
|
}
|
|
|
|
// A number wheel, 0..n-1 in a loop with the middle row selected: an infinite
|
|
// lv_roller's look and touch, drawn by hand. lv_roller measures its whole
|
|
// option text (repeated three times) on every style change, layout pass and
|
|
// frame: some 40 of the ~100 ms this screen took to open went on that.
|
|
struct Wheel {
|
|
uint8_t n;
|
|
int32_t pos; // the selected row, in 1/256 rows: dragging moves it between rows
|
|
int32_t vel; // the finger's recent speed, px per input read (smoothed)
|
|
int32_t drag; // px the finger has moved since it went down
|
|
};
|
|
static const int32_t WHEEL_LINE = 4; // between rows
|
|
static char s_nums[60][3]; // "00".."59"
|
|
|
|
static int32_t wheelPitch() { return lv_font_get_line_height(THEME_FONT_TITLE) + WHEEL_LINE; }
|
|
static Wheel* wheelOf(lv_obj_t* o) { return (Wheel*)lv_obj_get_user_data(o); }
|
|
static int wheelValue(lv_obj_t* o) {
|
|
const Wheel* w = wheelOf(o);
|
|
int32_t r = (w->pos + 128) >> 8;
|
|
return (int)(((r % w->n) + w->n) % w->n);
|
|
}
|
|
|
|
static void wheelAnim(void* o, int32_t v) {
|
|
wheelOf((lv_obj_t*)o)->pos = v;
|
|
lv_obj_invalidate((lv_obj_t*)o);
|
|
}
|
|
static void wheelAnimDone(lv_anim_t* a) { // back to the first loop, so pos never grows
|
|
Wheel* w = wheelOf((lv_obj_t*)a->var);
|
|
w->pos = (int32_t)wheelValue((lv_obj_t*)a->var) << 8;
|
|
}
|
|
// Rolls to row `to` (whole rows from where it is now) and says so.
|
|
static void wheelGo(lv_obj_t* o, int32_t to) {
|
|
Wheel* w = wheelOf(o);
|
|
int32_t from = w->pos, end = to * 256;
|
|
lv_anim_delete(o, wheelAnim);
|
|
lv_anim_t a;
|
|
lv_anim_init(&a);
|
|
lv_anim_set_var(&a, o);
|
|
lv_anim_set_exec_cb(&a, wheelAnim);
|
|
lv_anim_set_values(&a, from, end);
|
|
int32_t rows = LV_ABS(end - from) >> 8;
|
|
lv_anim_set_duration(&a, LV_MIN(200 + rows * 20, 600));
|
|
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
|
|
lv_anim_set_completed_cb(&a, wheelAnimDone);
|
|
w->pos = end; // the value is the target already: read by the event below
|
|
lv_obj_send_event(o, LV_EVENT_VALUE_CHANGED, NULL);
|
|
w->pos = from;
|
|
lv_anim_start(&a);
|
|
}
|
|
|
|
static void wheelDraw(lv_event_t* e) {
|
|
lv_obj_t* o = lv_event_get_current_target_obj(e);
|
|
const Wheel* w = wheelOf(o);
|
|
lv_layer_t* layer = lv_event_get_layer(e);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(o, &a);
|
|
int32_t pitch = wheelPitch(), mid = (a.y1 + a.y2 + 1) / 2;
|
|
int32_t h_main = lv_font_get_line_height(THEME_FONT_TITLE), h_sel = lv_font_get_line_height(THEME_FONT_LARGE);
|
|
int32_t band_h = (h_main + h_sel) / 2 + WHEEL_LINE;
|
|
lv_area_t band = { a.x1, mid - band_h / 2, a.x2, mid - band_h / 2 + band_h - 1 };
|
|
|
|
lv_draw_rect_dsc_t rd;
|
|
lv_draw_rect_dsc_init(&rd);
|
|
rd.bg_color = lv_color_hex(theme::SURFACE_2);
|
|
lv_draw_rect(layer, &rd, &band);
|
|
|
|
// Above the band, the band, below it: the band's rows in the larger font.
|
|
const lv_area_t clips[3] = { { a.x1, a.y1, a.x2, band.y1 - 1 }, band, { a.x1, band.y2 + 1, a.x2, a.y2 } };
|
|
const lv_area_t clip_ori = layer->_clip_area;
|
|
int32_t base = w->pos >> 8, frac = w->pos & 255;
|
|
int32_t span = lv_area_get_height(&a) / 2 / pitch + 2;
|
|
for (int c = 0; c < 3; c++) {
|
|
if (!lv_area_intersect(&layer->_clip_area, &clip_ori, &clips[c])) continue;
|
|
bool sel = c == 1;
|
|
lv_draw_label_dsc_t td;
|
|
lv_draw_label_dsc_init(&td);
|
|
td.font = sel ? THEME_FONT_LARGE : THEME_FONT_TITLE;
|
|
td.color = lv_color_hex(sel ? theme::ACCENT : theme::TEXT_MUTED);
|
|
td.align = LV_TEXT_ALIGN_CENTER;
|
|
int32_t fh = sel ? h_sel : h_main;
|
|
for (int32_t k = -span; k <= span; k++) {
|
|
int32_t y = mid + ((k << 8) - frac) * pitch / 256; // the row's middle
|
|
if (y + fh < layer->_clip_area.y1 || y - fh > layer->_clip_area.y2) continue;
|
|
td.text = s_nums[(((base + k) % w->n) + w->n) % w->n];
|
|
lv_area_t ta = { a.x1, y - fh / 2, a.x2, y - fh / 2 + fh - 1 };
|
|
lv_draw_label(layer, &td, &ta);
|
|
}
|
|
}
|
|
layer->_clip_area = clip_ori;
|
|
}
|
|
|
|
static void wheelEvent(lv_event_t* e) {
|
|
lv_obj_t* o = lv_event_get_current_target_obj(e);
|
|
Wheel* w = wheelOf(o);
|
|
lv_indev_t* indev = lv_indev_active();
|
|
switch (lv_event_get_code(e)) {
|
|
case LV_EVENT_PRESSED:
|
|
lv_anim_delete(o, wheelAnim);
|
|
w->vel = w->drag = 0;
|
|
break;
|
|
case LV_EVENT_PRESSING: {
|
|
lv_point_t v = { 0, 0 };
|
|
if (indev) lv_indev_get_vect(indev, &v);
|
|
w->vel = (w->vel + v.y) / 2;
|
|
w->drag += LV_ABS(v.y);
|
|
if (v.y) {
|
|
w->pos -= v.y * 256 / wheelPitch();
|
|
lv_obj_invalidate(o);
|
|
}
|
|
break;
|
|
}
|
|
case LV_EVENT_RELEASED:
|
|
case LV_EVENT_PRESS_LOST: {
|
|
int32_t pitch = wheelPitch(), to;
|
|
if (w->drag > 4) { // a throw carries on for ~10 reads' worth (as lv_roller's), then the nearest row
|
|
to = (w->pos - w->vel * 10 * 256 / pitch + 128) >> 8;
|
|
} else { // a tap: that row to the middle
|
|
lv_point_t p = { 0, 0 };
|
|
if (indev) lv_indev_get_point(indev, &p);
|
|
lv_area_t a;
|
|
lv_obj_get_coords(o, &a);
|
|
int32_t dy = p.y - (a.y1 + a.y2 + 1) / 2;
|
|
to = ((w->pos + 128) >> 8) + (dy + (dy < 0 ? -pitch / 2 : pitch / 2)) / pitch;
|
|
}
|
|
wheelGo(o, to);
|
|
break;
|
|
}
|
|
case LV_EVENT_DELETE:
|
|
lv_anim_delete(o, wheelAnim);
|
|
lv_free(w);
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
static lv_obj_t* wheel(lv_obj_t* parent, uint8_t n, int sel, lv_event_cb_t cb, uintptr_t which) {
|
|
if (!s_nums[0][0]) for (int i = 0; i < 60; i++) snprintf(s_nums[i], sizeof(s_nums[i]), "%02d", i);
|
|
Wheel* w = (Wheel*)lv_malloc(sizeof(Wheel));
|
|
*w = { n, (int32_t)(sel % n) * 256, 0, 0 };
|
|
lv_obj_t* o = lv_obj_create(parent);
|
|
styleSurface(o, theme::SURFACE);
|
|
lv_obj_set_style_radius(o, theme::RADIUS_SM, 0);
|
|
lv_obj_remove_flag(o, (lv_obj_flag_t)(LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_SCROLL_CHAIN));
|
|
lv_obj_set_size(o, 60, 84); // three rows; leaves room for the button below
|
|
lv_obj_set_user_data(o, w);
|
|
lv_obj_add_event_cb(o, wheelDraw, LV_EVENT_DRAW_MAIN, NULL);
|
|
lv_obj_add_event_cb(o, wheelEvent, LV_EVENT_ALL, NULL);
|
|
lv_obj_add_event_cb(o, cb, LV_EVENT_VALUE_CHANGED, (void*)which);
|
|
return o;
|
|
}
|
|
|
|
static lv_obj_t* colon(lv_obj_t* parent) { return label(parent, ":", THEME_FONT_LARGE, theme::TEXT_MUTED); }
|
|
|
|
} // namespace clockview
|
|
|
|
static void onOpenClock(lv_event_t* e) { (void)e; s_ui->showClock(); }
|
|
static void onClockTab(lv_event_t* e) { s_ui->clockTab((int)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onClockAct(lv_event_t* e) { s_ui->clockAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onRingDismiss(lv_event_t* e) { (void)e; s_ui->dismissRing(); }
|
|
|
|
// Alarm: hour / minute wheels, on switch, repeat. Saved on every change.
|
|
static void onAlarmWheel(lv_event_t* e) {
|
|
s_ui->setAlarm((int)(uintptr_t)lv_event_get_user_data(e), clockview::wheelValue((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onAlarmSwitch(lv_event_t* e) {
|
|
s_ui->setAlarm(2, lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0);
|
|
}
|
|
static void onAlarmRepeat(lv_event_t* e) {
|
|
s_ui->setAlarm(3, choiceSelected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onTimerWheel(lv_event_t* e) {
|
|
int v = clockview::wheelValue((lv_obj_t*)lv_event_get_target(e));
|
|
switch ((uintptr_t)lv_event_get_user_data(e)) {
|
|
case 0: clockview::s_timer_h = (uint8_t)v; break;
|
|
case 1: clockview::s_timer_m = (uint8_t)v; break;
|
|
default: clockview::s_timer_s = (uint8_t)v; break;
|
|
}
|
|
}
|
|
|
|
void UITask::showClock() {
|
|
_screen = SCR_CLOCK;
|
|
buildClock();
|
|
}
|
|
|
|
void UITask::clockTab(int tab) {
|
|
if (tab < 0 || tab >= clockview::TAB_COUNT) return;
|
|
clockview::s_tab = (uint8_t)tab;
|
|
buildClock();
|
|
}
|
|
|
|
void UITask::buildClock() {
|
|
using namespace clockview;
|
|
lv_obj_t* body = newScreen("Clock", true);
|
|
lv_obj_set_style_pad_row(body, 6, 0);
|
|
lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE);
|
|
s_big = s_go_lbl = s_alarm_sw = s_cal = nullptr;
|
|
|
|
// Tabs
|
|
lv_obj_t* tabs = centerRow(body);
|
|
static const char* const NAMES[TAB_COUNT] = { "Alarm", "Timer", "Stopwatch", "Calendar" };
|
|
for (int t = 0; t < TAB_COUNT; t++) {
|
|
lv_obj_t* b = lv_button_create(tabs);
|
|
lv_obj_set_height(b, 32);
|
|
lv_obj_set_flex_grow(b, 1);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(t == s_tab ? theme::ACCENT_DIM : theme::SURFACE), 0);
|
|
lv_obj_add_event_cb(b, onClockTab, LV_EVENT_CLICKED, (void*)(uintptr_t)t);
|
|
lv_obj_center(label(b, NAMES[t], THEME_FONT_SMALL, theme::TEXT));
|
|
}
|
|
|
|
if (s_tab == TAB_ALARM) {
|
|
lv_obj_t* r = centerRow(body);
|
|
wheel(r, 24, _prefs->alarm_hour % 24, onAlarmWheel, 0);
|
|
colon(r);
|
|
wheel(r, 60, _prefs->alarm_min % 60, onAlarmWheel, 1);
|
|
lv_obj_t* col = lv_obj_create(r);
|
|
styleSurface(col, theme::BG);
|
|
lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_size(col, 120, LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_pad_row(col, 4, 0);
|
|
lv_obj_set_style_pad_left(col, 8, 0);
|
|
lv_obj_t* on = flexBox(col, LV_FLEX_FLOW_ROW); // the switch and its label: one line
|
|
lv_obj_set_flex_align(on, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_column(on, 8, 0);
|
|
lv_obj_set_style_pad_bottom(on, 4, 0);
|
|
lv_obj_t* sw = s_alarm_sw = lv_switch_create(on);
|
|
label(on, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_obj_set_size(sw, 50, 26);
|
|
if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED);
|
|
lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL);
|
|
label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
char opts[48];
|
|
int o = 0;
|
|
for (uint8_t i = 0; i < NodePrefs::ALARM_REPEAT_COUNT; i++)
|
|
o += snprintf(opts + o, sizeof(opts) - o, "%s%s", i ? "\n" : "", i ? NodePrefs::alarmRepeatLabel(i) : "Once");
|
|
lv_obj_t* dd = choiceCreate(col, opts, NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask), "Repeat");
|
|
lv_obj_set_width(dd, 112);
|
|
lv_obj_add_event_cb(dd, onAlarmRepeat, LV_EVENT_VALUE_CHANGED, NULL);
|
|
bool synced = rtc_clock.getCurrentTime() > 1000000000UL;
|
|
label(body, synced ? "Rings at this local time (Settings time zone)." : "The clock isn't set yet: the alarm waits for a time sync.",
|
|
THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
return;
|
|
}
|
|
|
|
if (s_tab == TAB_CALENDAR) {
|
|
struct tm t;
|
|
bool synced = localTime(_prefs, t);
|
|
if (!synced) {
|
|
label(body, "The clock isn't set yet: no date to show.", THEME_FONT_BODY, theme::TEXT_MUTED);
|
|
return;
|
|
}
|
|
static const char* const LONG_MONTHS[] = { "January", "February", "March", "April", "May", "June", "July",
|
|
"August", "September", "October", "November", "December" };
|
|
char title[24]; // the month on the header's right: the grid gets the height
|
|
snprintf(title, sizeof(title), "%s %d", LONG_MONTHS[t.tm_mon], t.tm_year + 1900);
|
|
if (_header) lv_obj_align(label(_header, title, THEME_FONT_TITLE, theme::TEXT_MUTED), LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
|
s_cal_tm = t;
|
|
s_cal_marks = calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask, _prefs->alarm_hour, _prefs->alarm_min);
|
|
s_cal = lv_obj_create(body);
|
|
lv_obj_remove_style_all(s_cal);
|
|
lv_obj_remove_flag(s_cal, LV_OBJ_FLAG_CLICKABLE);
|
|
fillWidth(s_cal);
|
|
lv_obj_set_flex_grow(s_cal, 1);
|
|
lv_obj_add_event_cb(s_cal, onCalendarDraw, LV_EVENT_DRAW_MAIN_END, NULL);
|
|
return;
|
|
}
|
|
|
|
if (s_tab == TAB_TIMER) {
|
|
s_shown_running = _core->clock.isTimerRunning();
|
|
if (s_shown_running) {
|
|
s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT);
|
|
lv_obj_set_width(s_big, LV_PCT(100));
|
|
lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_pad_ver(s_big, 18, 0);
|
|
} else {
|
|
lv_obj_t* r = centerRow(body);
|
|
wheel(r, 24, s_timer_h, onTimerWheel, 0);
|
|
colon(r);
|
|
wheel(r, 60, s_timer_m, onTimerWheel, 1);
|
|
colon(r);
|
|
wheel(r, 60, s_timer_s, onTimerWheel, 2);
|
|
}
|
|
} else { // stopwatch
|
|
s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT);
|
|
lv_obj_set_width(s_big, LV_PCT(100));
|
|
lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_pad_ver(s_big, 18, 0);
|
|
}
|
|
|
|
lv_obj_t* acts = centerRow(body);
|
|
lv_obj_t* go = lv_button_create(acts);
|
|
lv_obj_set_size(go, 150, 40);
|
|
lv_obj_set_style_shadow_width(go, 0, 0);
|
|
lv_obj_set_style_radius(go, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0);
|
|
lv_obj_add_event_cb(go, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_START_STOP);
|
|
s_go_lbl = label(go, "", THEME_FONT_BODY, theme::TEXT);
|
|
lv_obj_center(s_go_lbl);
|
|
stylePrimary(go);
|
|
if (s_tab == TAB_STOPWATCH) {
|
|
lv_obj_t* rs = lv_button_create(acts);
|
|
lv_obj_set_size(rs, 100, 40);
|
|
lv_obj_set_style_shadow_width(rs, 0, 0);
|
|
lv_obj_set_style_radius(rs, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(rs, lv_color_hex(theme::SURFACE), 0);
|
|
lv_obj_add_event_cb(rs, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_RESET);
|
|
lv_obj_center(label(rs, LV_SYMBOL_REFRESH " Reset", THEME_FONT_BODY, theme::TEXT));
|
|
}
|
|
refreshClock();
|
|
}
|
|
|
|
// From loop() while SCR_CLOCK is shown: the running readouts.
|
|
void UITask::refreshClock() {
|
|
using namespace clockview;
|
|
if (s_tab == TAB_CALENDAR) { // past midnight, or the alarm changed: a new day to box or mark
|
|
struct tm t;
|
|
if (s_cal && localTime(_prefs, t)
|
|
&& (t.tm_mday != s_cal_tm.tm_mday || t.tm_mon != s_cal_tm.tm_mon
|
|
|| calmath::alarmDays(t, _prefs->alarm_on, _prefs->alarm_repeat_mask, _prefs->alarm_hour, _prefs->alarm_min) != s_cal_marks))
|
|
buildClock();
|
|
return;
|
|
}
|
|
if (s_tab == TAB_TIMER) {
|
|
bool running = _core->clock.isTimerRunning();
|
|
if (running != s_shown_running) { buildClock(); return; } // started elsewhere / just fired
|
|
if (s_big) {
|
|
char b[16];
|
|
fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs());
|
|
setText(s_big, b);
|
|
}
|
|
if (s_go_lbl) setText(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start");
|
|
} else if (s_tab == TAB_STOPWATCH) {
|
|
if (s_big) {
|
|
char b[16];
|
|
fmtStopwatch(b, sizeof(b), swElapsed());
|
|
setText(s_big, b);
|
|
}
|
|
if (s_go_lbl) setText(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start");
|
|
}
|
|
}
|
|
|
|
void UITask::clockAction(uint8_t act) {
|
|
using namespace clockview;
|
|
if (s_tab == TAB_TIMER) {
|
|
if (_core->clock.isTimerRunning()) _core->clock.stopTimer();
|
|
else {
|
|
uint32_t ms = ((uint32_t)s_timer_h * 3600 + s_timer_m * 60 + s_timer_s) * 1000UL;
|
|
if (ms == 0) { showToast("Set a time first"); return; }
|
|
_core->clock.startTimer(ms);
|
|
}
|
|
buildClock();
|
|
} else if (s_tab == TAB_STOPWATCH) {
|
|
if (act == ACT_RESET) { s_sw_accum = 0; s_sw_start = millis(); }
|
|
else if (s_sw_running) { s_sw_accum += millis() - s_sw_start; s_sw_running = false; }
|
|
else { s_sw_start = millis(); s_sw_running = true; }
|
|
refreshClock();
|
|
}
|
|
}
|
|
|
|
// which: 0 hour, 1 minute, 2 on/off, 3 repeat preset.
|
|
void UITask::setAlarm(int which, int v) {
|
|
if (!_prefs) return;
|
|
switch (which) {
|
|
case 0: _prefs->alarm_hour = (uint8_t)v; _prefs->alarm_on = 1; break; // setting a time arms it
|
|
case 1: _prefs->alarm_min = (uint8_t)v; _prefs->alarm_on = 1; break;
|
|
case 2: _prefs->alarm_on = (uint8_t)v; break;
|
|
case 3: _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx((uint8_t)v); break;
|
|
}
|
|
_core->clock.onAlarmChanged();
|
|
prefsSave();
|
|
if (which < 2 && clockview::s_alarm_sw) lv_obj_add_state(clockview::s_alarm_sw, LV_STATE_CHECKED);
|
|
}
|
|
|
|
// ── Ring card (any screen) ────────────────────────────────────────────────────
|
|
|
|
void UITask::showRing(const char* text) {
|
|
using namespace clockview;
|
|
wake();
|
|
if (!s_ring) {
|
|
s_ring = lv_obj_create(lv_layer_top());
|
|
lv_obj_remove_style_all(s_ring);
|
|
lv_obj_set_size(s_ring, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_set_style_bg_color(s_ring, lv_color_hex(theme::BG), 0);
|
|
lv_obj_set_style_bg_opa(s_ring, LV_OPA_COVER, 0);
|
|
lv_obj_add_flag(s_ring, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_obj_set_flex_flow(s_ring, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(s_ring, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_row(s_ring, 14, 0);
|
|
label(s_ring, LV_SYMBOL_BELL, THEME_FONT_LARGE, theme::ACCENT);
|
|
s_ring_lbl = label(s_ring, "", THEME_FONT_LARGE, theme::TEXT);
|
|
lv_obj_t* b = lv_button_create(s_ring);
|
|
lv_obj_set_size(b, 200, 56);
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
lv_obj_set_style_radius(b, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT_DIM), 0);
|
|
lv_obj_add_event_cb(b, onRingDismiss, LV_EVENT_CLICKED, NULL);
|
|
lv_obj_center(label(b, "Dismiss", THEME_FONT_LARGE, theme::TEXT));
|
|
stylePrimary(b);
|
|
}
|
|
lv_label_set_text(s_ring_lbl, text);
|
|
lv_obj_remove_flag(s_ring, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_move_foreground(s_ring); // over the lock screen too
|
|
}
|
|
|
|
void UITask::dismissRing() {
|
|
stopMelody();
|
|
_core->clock.dismissRing();
|
|
hideRing();
|
|
}
|
|
|
|
void UITask::hideRing() {
|
|
if (clockview::s_ring) lv_obj_add_flag(clockview::s_ring, LV_OBJ_FLAG_HIDDEN);
|
|
if (_screen == SCR_CLOCK) buildClock(); // e.g. the timer's Start again
|
|
}
|