From 54a70d09e263d67947e6d693d660907a33809c52 Mon Sep 17 00:00:00 2001 From: "DESKTOP-PUHFO60\\dsx2016" Date: Sun, 18 Oct 2026 18:58:21 +0800 Subject: [PATCH] Add Pomodoro UI with 1/3/5/10 presets and custom keypad. Replace Beep/WAV list screen with a timer; pick duration via presets or Set numpad (1-99 min), and keep selectable alarm sounds. Co-authored-by: Cursor --- CHANGELOG.md | 11 + README.md | 10 +- include/pomodoro.h | 25 ++ include/ui.h | 5 +- platformio.ini | 2 +- src/main.cpp | 25 +- src/pomodoro.cpp | 143 +++++++++++ src/ui.cpp | 623 ++++++++++++++++++++++++++++++--------------- 8 files changed, 620 insertions(+), 224 deletions(-) create mode 100644 include/pomodoro.h create mode 100644 src/pomodoro.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 44453e8..1c67b01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 2026-10-18 — Pomodoro presets + custom minutes + +- Duration presets: 1 / 3 / 5 / 10 minutes; `Set` opens numeric keypad (1–99). +- Keep `+/-`, Start/Pause/Reset, sound picker, speaker + volume. + +## 2026-10-18 — Pomodoro timer UI + +- Tag baseline `v1.0-beep-wav` (Beep + WAV list UI). +- New Pomodoro screen with Start/Pause/Reset and selectable alarm sound. +- Keep speaker toggle + volume slider; TF status in header. + ## 2026-10-18 — WAV via DAC2 only (touch-safe) - Drop I2S INTERNAL_DAC (it enables GPIO25 and kills touch). diff --git a/README.md b/README.md index d9df992..9b6c131 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ -# CYD TF + Speaker +# CYD Pomodoro -ESP32-2432S028R:LVGL 横屏显示 TF 卡信息,触控开关喇叭并播放音乐。 +ESP32-2432S028R:LVGL 横屏番茄钟,可选内置提示音,触控喇叭播放。 ## 功能 - SoftSPI 触摸(已标定,勿改 `src/touch.cpp` 映射) -- TF 卡信息(VSPI) -- 喇叭 GPIO26,默认音量 **20%**;按钮:Speaker ON/OFF、Play Music +- 番茄钟:预设 1 / 3 / 5 / 10 分钟;`Set` 自定义输入;`+/-` 微调;Start / Pause / Reset +- 结束音效:内置 Beep 列表自选(默认 Alarm),点名称可试听 +- 喇叭 GPIO26 + 音量滑条;TF 卡状态显示在右上角 +- 基线标签:`v1.0-beep-wav`(改番茄钟前的 Beep + WAV 界面) ## 构建 / 烧录 diff --git a/include/pomodoro.h b/include/pomodoro.h new file mode 100644 index 0000000..6f54804 --- /dev/null +++ b/include/pomodoro.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +enum PomoState : uint8_t { + POMO_IDLE = 0, + POMO_RUNNING = 1, + POMO_PAUSED = 2, + POMO_DONE = 3, +}; + +void pomo_init(); +void pomo_tick(); + +PomoState pomo_state(); +uint32_t pomo_remaining_sec(); +uint16_t pomo_duration_minutes(); // last configured length + +void pomo_set_minutes(uint16_t minutes); // 1..99; idle/paused/done only +void pomo_start_pause(); +void pomo_reset(); +void pomo_adjust_minutes(int8_t delta); + +const char *pomo_state_name(); +bool pomo_consume_alarm(); diff --git a/include/ui.h b/include/ui.h index 96be760..92493ae 100644 --- a/include/ui.h +++ b/include/ui.h @@ -5,9 +5,6 @@ void ui_create(); void ui_update(const SdCardInfo &info); +void ui_tick(); // refresh timer + fire alarm sound void ui_refresh_audio_buttons(); -void ui_refresh_lists(); void ui_set_touch_debug(int16_t lx, int16_t ly, bool down); - -// 0=Beep 1=WAV -uint8_t ui_active_tab(); diff --git a/platformio.ini b/platformio.ini index c8c504b..8935b40 100644 --- a/platformio.ini +++ b/platformio.ini @@ -43,13 +43,13 @@ build_flags = -DLV_FONT_MONTSERRAT_16=1 -DLV_FONT_MONTSERRAT_18=1 -DLV_FONT_MONTSERRAT_20=1 + -DLV_FONT_MONTSERRAT_28=1 -DLV_USE_TFT_ESPI=1 -DLV_USE_FLEX=1 -DLV_USE_BUTTON=1 -DLV_USE_LABEL=1 -DLV_USE_BAR=1 -DLV_USE_SLIDER=1 - -DLV_USE_TABVIEW=1 -I${PROJECT_DIR}/include extra_scripts = pre:copy_lv_conf.py diff --git a/src/main.cpp b/src/main.cpp index d154d45..e976294 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,8 +14,9 @@ static constexpr uint32_t DRAW_BUF_SIZE = static uint32_t draw_buf[DRAW_BUF_SIZE / 4]; static uint32_t last_refresh_ms = 0; -static uint32_t last_btn_ui_ms = 0; -static constexpr uint32_t REFRESH_MS = 4000; +static uint32_t last_ui_ms = 0; +static constexpr uint32_t REFRESH_MS = 8000; +static constexpr uint32_t UI_TICK_MS = 200; static void refresh_sd_ui() { SdCardInfo info = sd_read_info(); @@ -28,7 +29,6 @@ static void refresh_sd_ui() { format_bytes(info.card_bytes).c_str()); if (!audio_is_playing()) { wav_list_scan(); - ui_refresh_lists(); } } @@ -40,7 +40,7 @@ void setup() { Serial.begin(460800); delay(200); Serial.println(); - Serial.printf("LVGL %d.%d.%d | CYD Beep + WAV\n", lv_version_major(), + Serial.printf("LVGL %d.%d.%d | CYD Pomodoro\n", lv_version_major(), lv_version_minor(), lv_version_patch()); Serial.println("BOOT_OK"); @@ -69,7 +69,7 @@ void setup() { refresh_sd_ui(); last_refresh_ms = millis(); - last_btn_ui_ms = millis(); + last_ui_ms = millis(); } void loop() { @@ -84,20 +84,15 @@ void loop() { audio_tick(); const uint32_t now = millis(); + if (now - last_ui_ms >= UI_TICK_MS) { + last_ui_ms = now; + ui_tick(); + } + if (!playing && now - last_refresh_ms >= REFRESH_MS) { last_refresh_ms = now; refresh_sd_ui(); } - if (now - last_btn_ui_ms >= 200) { - last_btn_ui_ms = now; - static bool was_playing = false; - ui_refresh_audio_buttons(); - if (playing != was_playing) { - was_playing = playing; - ui_refresh_lists(); - } - } - delay(playing ? 2 : 5); } diff --git a/src/pomodoro.cpp b/src/pomodoro.cpp new file mode 100644 index 0000000..62857e1 --- /dev/null +++ b/src/pomodoro.cpp @@ -0,0 +1,143 @@ +#include "pomodoro.h" + +#include + +static constexpr uint16_t MIN_MINUTES = 1; +static constexpr uint16_t MAX_MINUTES = 99; +static constexpr uint16_t DEFAULT_MINUTES = 5; + +static PomoState g_state = POMO_IDLE; +static uint32_t g_duration_ms = DEFAULT_MINUTES * 60UL * 1000UL; +static uint32_t g_remain_ms = DEFAULT_MINUTES * 60UL * 1000UL; +static uint32_t g_last_ms = 0; +static bool g_alarm_pending = false; + +static uint32_t clamp_minutes_ms(uint16_t minutes) { + if (minutes < MIN_MINUTES) { + minutes = MIN_MINUTES; + } + if (minutes > MAX_MINUTES) { + minutes = MAX_MINUTES; + } + return (uint32_t)minutes * 60UL * 1000UL; +} + +void pomo_init() { + g_state = POMO_IDLE; + g_duration_ms = clamp_minutes_ms(DEFAULT_MINUTES); + g_remain_ms = g_duration_ms; + g_last_ms = 0; + g_alarm_pending = false; +} + +PomoState pomo_state() { + return g_state; +} + +uint32_t pomo_remaining_sec() { + return (g_remain_ms + 999UL) / 1000UL; +} + +uint16_t pomo_duration_minutes() { + return (uint16_t)((g_duration_ms + 59999UL) / 60000UL); +} + +const char *pomo_state_name() { + switch (g_state) { + case POMO_RUNNING: + return "RUN"; + case POMO_PAUSED: + return "PAUSE"; + case POMO_DONE: + return "DONE"; + case POMO_IDLE: + default: + return "READY"; + } +} + +void pomo_set_minutes(uint16_t minutes) { + if (g_state == POMO_RUNNING) { + return; + } + g_duration_ms = clamp_minutes_ms(minutes); + g_remain_ms = g_duration_ms; + g_state = POMO_IDLE; + g_alarm_pending = false; +} + +void pomo_start_pause() { + if (g_state == POMO_DONE) { + g_remain_ms = g_duration_ms; + g_state = POMO_IDLE; + } + if (g_state == POMO_RUNNING) { + g_state = POMO_PAUSED; + return; + } + if (g_remain_ms == 0) { + g_remain_ms = g_duration_ms; + } + g_state = POMO_RUNNING; + g_last_ms = millis(); + g_alarm_pending = false; +} + +void pomo_reset() { + g_remain_ms = g_duration_ms; + g_state = POMO_IDLE; + g_alarm_pending = false; +} + +void pomo_adjust_minutes(int8_t delta) { + if (g_state == POMO_RUNNING) { + return; + } + int32_t mins = (int32_t)pomo_duration_minutes() + (int32_t)delta; + if (mins < (int32_t)MIN_MINUTES) { + mins = MIN_MINUTES; + } + if (mins > (int32_t)MAX_MINUTES) { + mins = MAX_MINUTES; + } + // When paused mid-timer, adjust remaining by same delta minutes from current remain + if (g_state == POMO_PAUSED) { + int32_t sec = (int32_t)(g_remain_ms / 1000UL) + (int32_t)delta * 60; + if (sec < 60) { + sec = 60; + } + if (sec > (int32_t)MAX_MINUTES * 60) { + sec = (int32_t)MAX_MINUTES * 60; + } + g_remain_ms = (uint32_t)sec * 1000UL; + g_duration_ms = g_remain_ms; + g_alarm_pending = false; + return; + } + pomo_set_minutes((uint16_t)mins); +} + +void pomo_tick() { + if (g_state != POMO_RUNNING) { + return; + } + const uint32_t now = millis(); + const uint32_t elapsed = now - g_last_ms; + g_last_ms = now; + + if (elapsed >= g_remain_ms) { + g_remain_ms = 0; + g_state = POMO_DONE; + g_alarm_pending = true; + return; + } + g_remain_ms -= elapsed; +} + +bool pomo_consume_alarm() { + if (!g_alarm_pending) { + return false; + } + g_alarm_pending = false; + return true; +} diff --git a/src/ui.cpp b/src/ui.cpp index a4d4a03..085320a 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -1,10 +1,12 @@ #include "ui.h" #include "audio_test.h" #include "pins.h" -#include "wav_list.h" +#include "pomodoro.h" #include #include +#include +#include #include static constexpr uint32_t COL_BG = 0x0A1016; @@ -18,21 +20,35 @@ static constexpr uint32_t COL_BTN_OFF = 0x243140; static constexpr uint32_t COL_BTN_ON = 0x1B5E4A; static constexpr uint32_t COL_BTN_PLAY = 0x1A4A6E; static constexpr uint32_t COL_BTN_STOP = 0x8B2E2E; -static constexpr uint32_t COL_ROW = 0x15202C; -static constexpr uint32_t COL_ROW_SEL = 0x1E3A4A; +static constexpr uint32_t COL_MODE = 0x15202C; +static constexpr uint32_t COL_MODE_ON = 0x1E3A4A; +static constexpr uint32_t COL_TOMATO = 0xE85D4C; + +static constexpr uint16_t PRESETS[] = {1, 3, 5, 10}; +static constexpr int PRESET_N = 4; static lv_obj_t *lbl_chip; -static lv_obj_t *lbl_audio; +static lv_obj_t *lbl_timer; +static lv_obj_t *lbl_status; +static lv_obj_t *lbl_sound; static lv_obj_t *lbl_vol; +static lv_obj_t *lbl_audio; static lv_obj_t *slider_vol; -static lv_obj_t *tabview; -static lv_obj_t *list_beep; -static lv_obj_t *list_wav; static lv_obj_t *btn_speaker; -static lv_obj_t *btn_play; +static lv_obj_t *btn_start; +static lv_obj_t *btn_reset; static lv_obj_t *lbl_speaker; -static lv_obj_t *lbl_play; -static uint8_t g_tab = 0; +static lv_obj_t *lbl_start; +static lv_obj_t *btn_preset[PRESET_N]; +static lv_obj_t *btn_custom; +static lv_obj_t *panel_custom; +static lv_obj_t *lbl_custom_val; + +static char g_timer_text[8] = "05:00"; +static char g_status_text[40] = "5m · READY"; +static char g_sound_text[28] = "Alarm"; +static char g_custom_buf[4] = ""; +static char g_custom_disp[16] = "min: _"; static void style_btn(lv_obj_t *btn, uint32_t bg) { lv_obj_set_style_radius(btn, 10, 0); @@ -52,18 +68,94 @@ static void sync_vol_label() { lv_label_set_text(lbl_vol, buf); } -static void style_row(lv_obj_t *row, bool selected, bool playing) { - uint32_t bg = selected ? COL_ROW_SEL : COL_ROW; - if (playing) { - bg = COL_BTN_PLAY; +static void refresh_preset_highlight() { + const uint16_t mins = pomo_duration_minutes(); + bool matched = false; + for (int i = 0; i < PRESET_N; i++) { + if (!btn_preset[i]) { + continue; + } + const bool match = mins == PRESETS[i]; + if (match) { + matched = true; + } + style_btn(btn_preset[i], match ? COL_MODE_ON : COL_MODE); + } + if (btn_custom) { + style_btn(btn_custom, matched ? COL_MODE : COL_MODE_ON); + } +} + +static void refresh_timer_labels() { + const uint32_t sec = pomo_remaining_sec(); + const uint32_t mm = sec / 60U; + const uint32_t ss = sec % 60U; + snprintf(g_timer_text, sizeof(g_timer_text), "%02u:%02u", (unsigned)mm, + (unsigned)ss); + if (lbl_timer) { + lv_label_set_text(lbl_timer, g_timer_text); + const uint32_t color = (pomo_state() == POMO_DONE) ? COL_TOMATO : COL_ACCENT; + lv_obj_set_style_text_color(lbl_timer, lv_color_hex(color), 0); + } + + snprintf(g_status_text, sizeof(g_status_text), "%um · %s", + (unsigned)pomo_duration_minutes(), pomo_state_name()); + if (lbl_status) { + lv_label_set_text(lbl_status, g_status_text); + } + + if (lbl_start) { + if (pomo_state() == POMO_RUNNING) { + lv_label_set_text(lbl_start, "PAUSE"); + } else if (pomo_state() == POMO_DONE) { + lv_label_set_text(lbl_start, "AGAIN"); + } else { + lv_label_set_text(lbl_start, "START"); + } + } + if (btn_start) { + style_btn(btn_start, + pomo_state() == POMO_RUNNING ? COL_BTN_STOP : COL_BTN_PLAY); + } + refresh_preset_highlight(); +} + +static void refresh_sound_label() { + const int8_t sel = sound_selected(); + if (sel < 0) { + snprintf(g_sound_text, sizeof(g_sound_text), "-"); + } else { + snprintf(g_sound_text, sizeof(g_sound_text), "%s", sound_name((uint8_t)sel)); + } + if (lbl_sound) { + lv_label_set_text(lbl_sound, g_sound_text); + } +} + +static void sync_custom_label() { + if (g_custom_buf[0] == '\0') { + snprintf(g_custom_disp, sizeof(g_custom_disp), "min: _"); + } else { + snprintf(g_custom_disp, sizeof(g_custom_disp), "min: %s", g_custom_buf); + } + if (lbl_custom_val) { + lv_label_set_text(lbl_custom_val, g_custom_disp); + } +} + +static void custom_hide() { + if (panel_custom) { + lv_obj_add_flag(panel_custom, LV_OBJ_FLAG_HIDDEN); + } +} + +static void custom_show() { + g_custom_buf[0] = '\0'; + sync_custom_label(); + if (panel_custom) { + lv_obj_clear_flag(panel_custom, LV_OBJ_FLAG_HIDDEN); + lv_obj_move_foreground(panel_custom); } - lv_obj_set_style_bg_color(row, lv_color_hex(bg), 0); - lv_obj_set_style_bg_opa(row, LV_OPA_COVER, 0); - lv_obj_set_style_radius(row, 8, 0); - lv_obj_set_style_border_width(row, 0, 0); - lv_obj_set_style_shadow_width(row, 0, 0); - lv_obj_set_style_pad_left(row, 10, 0); - lv_obj_set_style_pad_right(row, 10, 0); } static void on_speaker_clicked(lv_event_t *e) { @@ -72,20 +164,130 @@ static void on_speaker_clicked(lv_event_t *e) { ui_refresh_audio_buttons(); } -static void on_play_clicked(lv_event_t *e) { +static void on_start_clicked(lv_event_t *e) { LV_UNUSED(e); - if (audio_is_playing()) { - audio_stop(); - } else if (g_tab == 1) { - const int8_t sel = wav_list_selected(); - if (sel >= 0) { - audio_play_wav_index((uint8_t)sel); - } - } else { - audio_play_selected(); - } + pomo_start_pause(); + refresh_timer_labels(); +} + +static void on_reset_clicked(lv_event_t *e) { + LV_UNUSED(e); + pomo_reset(); + audio_stop(); + refresh_timer_labels(); + ui_refresh_audio_buttons(); +} + +static void on_preset_clicked(lv_event_t *e) { + const int idx = (int)(intptr_t)lv_event_get_user_data(e); + if (idx < 0 || idx >= PRESET_N) { + return; + } + pomo_set_minutes(PRESETS[idx]); + refresh_timer_labels(); +} + +static void on_custom_open(lv_event_t *e) { + LV_UNUSED(e); + if (pomo_state() == POMO_RUNNING) { + return; + } + custom_show(); +} + +static void on_custom_cancel(lv_event_t *e) { + LV_UNUSED(e); + custom_hide(); +} + +static void on_custom_ok(lv_event_t *e) { + LV_UNUSED(e); + if (g_custom_buf[0] == '\0') { + custom_hide(); + return; + } + int mins = atoi(g_custom_buf); + if (mins < 1) { + mins = 1; + } + if (mins > 99) { + mins = 99; + } + pomo_set_minutes((uint16_t)mins); + custom_hide(); + refresh_timer_labels(); +} + +static void on_custom_digit(lv_event_t *e) { + const int d = (int)(intptr_t)lv_event_get_user_data(e); + const size_t len = strlen(g_custom_buf); + if (d == -1) { // clear + g_custom_buf[0] = '\0'; + sync_custom_label(); + return; + } + if (len >= 2) { + return; + } + if (len == 0 && d == 0) { + return; // no leading zero + } + g_custom_buf[len] = (char)('0' + d); + g_custom_buf[len + 1] = '\0'; + sync_custom_label(); +} + +static void on_minus_clicked(lv_event_t *e) { + LV_UNUSED(e); + pomo_adjust_minutes(-1); + refresh_timer_labels(); +} + +static void on_plus_clicked(lv_event_t *e) { + LV_UNUSED(e); + pomo_adjust_minutes(1); + refresh_timer_labels(); +} + +static void on_sound_prev(lv_event_t *e) { + LV_UNUSED(e); + const uint8_t n = sound_count(); + if (n == 0) { + return; + } + int8_t sel = sound_selected(); + if (sel <= 0) { + sel = (int8_t)(n - 1); + } else { + sel--; + } + sound_set_selected(sel); + refresh_sound_label(); +} + +static void on_sound_next(lv_event_t *e) { + LV_UNUSED(e); + const uint8_t n = sound_count(); + if (n == 0) { + return; + } + int8_t sel = sound_selected(); + sel++; + if (sel >= (int8_t)n) { + sel = 0; + } + sound_set_selected(sel); + refresh_sound_label(); +} + +static void on_sound_preview(lv_event_t *e) { + LV_UNUSED(e); + const int8_t sel = sound_selected(); + if (sel < 0) { + return; + } + audio_play_sound((uint8_t)sel); ui_refresh_audio_buttons(); - ui_refresh_lists(); } static void on_vol_changed(lv_event_t *e) { @@ -94,107 +296,83 @@ static void on_vol_changed(lv_event_t *e) { sync_vol_label(); } -static void on_beep_clicked(lv_event_t *e) { - const int idx = (int)(intptr_t)lv_event_get_user_data(e); - if (idx < 0 || idx >= (int)sound_count()) { - return; +static lv_obj_t *make_small_btn(lv_obj_t *parent, const char *txt, int x, int y, + int w, int h, lv_event_cb_t cb, void *ud) { + lv_obj_t *btn = lv_button_create(parent); + lv_obj_set_pos(btn, x, y); + lv_obj_set_size(btn, w, h); + lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE); + if (cb) { + lv_obj_add_event_cb(btn, cb, LV_EVENT_CLICKED, ud); } - audio_play_sound((uint8_t)idx); - ui_refresh_audio_buttons(); - ui_refresh_lists(); + style_btn(btn, COL_BTN_OFF); + + lv_obj_t *lbl = lv_label_create(btn); + lv_label_set_text(lbl, txt); + lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0); + lv_obj_center(lbl); + return btn; } -static void on_wav_clicked(lv_event_t *e) { - const int idx = (int)(intptr_t)lv_event_get_user_data(e); - if (idx < 0 || idx >= (int)wav_list_count()) { - return; - } - audio_play_wav_index((uint8_t)idx); - ui_refresh_audio_buttons(); - ui_refresh_lists(); -} +static void build_custom_panel(lv_obj_t *scr) { + panel_custom = lv_obj_create(scr); + lv_obj_set_size(panel_custom, UI_HOR_RES, UI_VER_RES); + lv_obj_set_pos(panel_custom, 0, 0); + lv_obj_set_style_bg_color(panel_custom, lv_color_hex(COL_BG), 0); + lv_obj_set_style_bg_opa(panel_custom, LV_OPA_COVER, 0); + lv_obj_set_style_border_width(panel_custom, 0, 0); + lv_obj_set_style_pad_all(panel_custom, 0, 0); + lv_obj_set_style_radius(panel_custom, 0, 0); + lv_obj_clear_flag(panel_custom, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(panel_custom, LV_OBJ_FLAG_HIDDEN); -static void on_tab_changed(lv_event_t *e) { - lv_obj_t *tv = (lv_obj_t *)lv_event_get_target(e); - g_tab = (uint8_t)lv_tabview_get_tab_active(tv); - ui_refresh_lists(); -} + lv_obj_t *title = lv_label_create(panel_custom); + lv_label_set_text(title, "Custom minutes"); + lv_obj_set_style_text_font(title, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(title, lv_color_hex(COL_ACCENT), 0); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 8); -static void fill_list_beep() { - if (!list_beep) { - return; - } - lv_obj_clean(list_beep); - const uint8_t n = sound_count(); - const int8_t sel = sound_selected(); - const bool playing = audio_is_playing() && g_tab == 0; + lbl_custom_val = lv_label_create(panel_custom); + lv_label_set_text(lbl_custom_val, "min: _"); + lv_obj_set_style_text_font(lbl_custom_val, &lv_font_montserrat_28, 0); + lv_obj_set_style_text_color(lbl_custom_val, lv_color_hex(COL_INK), 0); + lv_obj_align(lbl_custom_val, LV_ALIGN_TOP_MID, 0, 36); - for (uint8_t i = 0; i < n; i++) { - lv_obj_t *row = lv_button_create(list_beep); - lv_obj_set_width(row, lv_pct(100)); - lv_obj_set_height(row, 32); - lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(row, on_beep_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i); - style_row(row, sel == (int8_t)i, playing && sel == (int8_t)i); - - lv_obj_t *lbl = lv_label_create(row); - char line[36]; - snprintf(line, sizeof(line), "%u %s", (unsigned)(i + 1), sound_name(i)); - lv_label_set_text(lbl, line); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); - lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0); - lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0); - } -} - -static void fill_list_wav() { - if (!list_wav) { - return; - } - lv_obj_clean(list_wav); - const uint8_t n = wav_list_count(); - if (n == 0) { - lv_obj_t *empty = lv_label_create(list_wav); - lv_label_set_text(empty, "No .wav on TF\nPut in / or /wav"); - lv_obj_set_style_text_font(empty, &lv_font_montserrat_14, 0); - lv_obj_set_style_text_color(empty, lv_color_hex(COL_MUTED), 0); - return; - } - - const int8_t sel = wav_list_selected(); - const bool playing = audio_is_playing() && g_tab == 1; - - for (uint8_t i = 0; i < n; i++) { - const WavTrack *t = wav_list_get(i); - if (!t) { - continue; + // Numpad 3x4 + static const char *keys[12] = {"1", "2", "3", "4", "5", "6", + "7", "8", "9", "C", "0", "OK"}; + const int kw = 90; + const int kh = 32; + const int ox = 16; + const int oy = 72; + const int gap = 6; + for (int i = 0; i < 12; i++) { + const int r = i / 3; + const int c = i % 3; + const int x = ox + c * (kw + gap); + const int y = oy + r * (kh + gap); + lv_event_cb_t cb = on_custom_digit; + void *ud = nullptr; + if (i <= 8) { + ud = (void *)(intptr_t)(i + 1); // 1..9 + } else if (i == 9) { + ud = (void *)(intptr_t)-1; // clear + } else if (i == 10) { + ud = (void *)(intptr_t)0; // 0 + } else { + cb = on_custom_ok; + } + lv_obj_t *b = make_small_btn(panel_custom, keys[i], x, y, kw, kh, cb, ud); + if (i == 11) { + style_btn(b, COL_BTN_PLAY); + } else if (i == 9) { + style_btn(b, COL_BTN_STOP); } - lv_obj_t *row = lv_button_create(list_wav); - lv_obj_set_width(row, lv_pct(100)); - lv_obj_set_height(row, 32); - lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(row, on_wav_clicked, LV_EVENT_CLICKED, (void *)(intptr_t)i); - style_row(row, sel == (int8_t)i, playing && sel == (int8_t)i); - - lv_obj_t *lbl = lv_label_create(row); - char line[40]; - snprintf(line, sizeof(line), "%u %s", (unsigned)(i + 1), t->name); - lv_label_set_text(lbl, line); - lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0); - lv_obj_set_style_text_color(lbl, lv_color_hex(COL_INK), 0); - lv_label_set_long_mode(lbl, LV_LABEL_LONG_SCROLL_CIRCULAR); - lv_obj_set_width(lbl, lv_pct(100)); - lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0); } -} -void ui_refresh_lists() { - fill_list_beep(); - fill_list_wav(); -} - -uint8_t ui_active_tab() { - return g_tab; + make_small_btn(panel_custom, "Cancel", 16, UI_VER_RES - 34, UI_HOR_RES - 32, + 28, on_custom_cancel, nullptr); } void ui_refresh_audio_buttons() { @@ -204,43 +382,29 @@ void ui_refresh_audio_buttons() { if (btn_speaker) { style_btn(btn_speaker, audio_is_enabled() ? COL_BTN_ON : COL_BTN_OFF); } - if (lbl_play) { - lv_label_set_text(lbl_play, audio_is_playing() ? "STOP" : "PLAY"); - } - if (btn_play) { - style_btn(btn_play, audio_is_playing() ? COL_BTN_STOP : COL_BTN_PLAY); - } if (lbl_audio) { lv_label_set_text(lbl_audio, audio_last_status()); } - if (slider_vol && (int32_t)audio_get_volume() != lv_slider_get_value(slider_vol)) { + if (slider_vol && + (int32_t)audio_get_volume() != lv_slider_get_value(slider_vol)) { lv_slider_set_value(slider_vol, audio_get_volume(), LV_ANIM_OFF); } sync_vol_label(); } -static void style_scroll_list(lv_obj_t *list) { - lv_obj_set_size(list, lv_pct(100), lv_pct(100)); - lv_obj_set_style_bg_opa(list, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(list, 0, 0); - lv_obj_set_style_pad_all(list, 4, 0); - lv_obj_set_style_pad_row(list, 4, 0); - lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); - lv_obj_set_scroll_dir(list, LV_DIR_VER); - lv_obj_add_flag(list, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_clear_flag(list, LV_OBJ_FLAG_CLICKABLE); -} - void ui_create() { + pomo_init(); + sound_set_selected(11); // Alarm + lv_obj_t *scr = lv_screen_active(); lv_obj_set_style_bg_color(scr, lv_color_hex(COL_BG), 0); lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0); lv_obj_clear_flag(scr, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *lbl_title = lv_label_create(scr); - lv_label_set_text(lbl_title, "CYD"); + lv_label_set_text(lbl_title, "POMO"); lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_20, 0); - lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_ACCENT), 0); + lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_TOMATO), 0); lv_obj_set_pos(lbl_title, 12, 4); lbl_chip = lv_label_create(scr); @@ -249,15 +413,70 @@ void ui_create() { lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_MUTED), 0); lv_obj_align(lbl_chip, LV_ALIGN_TOP_RIGHT, -12, 8); + make_small_btn(scr, "-", 12, 32, 40, 44, on_minus_clicked, nullptr); + + lbl_timer = lv_label_create(scr); + lv_label_set_text(lbl_timer, "05:00"); + lv_obj_set_style_text_font(lbl_timer, &lv_font_montserrat_28, 0); + lv_obj_set_style_text_color(lbl_timer, lv_color_hex(COL_ACCENT), 0); + lv_obj_align(lbl_timer, LV_ALIGN_TOP_MID, 0, 34); + + make_small_btn(scr, "+", UI_HOR_RES - 52, 32, 40, 44, on_plus_clicked, + nullptr); + + lbl_status = lv_label_create(scr); + lv_label_set_text(lbl_status, "5m · READY"); + lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_MUTED), 0); + lv_obj_align(lbl_status, LV_ALIGN_TOP_MID, 0, 74); + + // Presets: 1 3 5 10 + Set + const int gap = 6; + const int pw = (UI_HOR_RES - 24 - gap * 4) / 5; + static const char *ptxt[PRESET_N] = {"1", "3", "5", "10"}; + for (int i = 0; i < PRESET_N; i++) { + btn_preset[i] = + make_small_btn(scr, ptxt[i], 12 + i * (pw + gap), 94, pw, 30, + on_preset_clicked, (void *)(intptr_t)i); + } + btn_custom = make_small_btn(scr, "Set", 12 + PRESET_N * (pw + gap), 94, pw, + 30, on_custom_open, nullptr); + + // Sound + lv_obj_t *snd_cap = lv_label_create(scr); + lv_label_set_text(snd_cap, "SOUND"); + lv_obj_set_style_text_font(snd_cap, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(snd_cap, lv_color_hex(COL_MUTED), 0); + lv_obj_set_pos(snd_cap, 12, 134); + + make_small_btn(scr, "<", 68, 128, 32, 28, on_sound_prev, nullptr); + + lv_obj_t *btn_snd = lv_button_create(scr); + lv_obj_set_pos(btn_snd, 104, 128); + lv_obj_set_size(btn_snd, UI_HOR_RES - 104 - 48, 28); + lv_obj_add_flag(btn_snd, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(btn_snd, on_sound_preview, LV_EVENT_CLICKED, nullptr); + style_btn(btn_snd, COL_MODE); + + lbl_sound = lv_label_create(btn_snd); + lv_label_set_text(lbl_sound, "Alarm"); + lv_obj_set_style_text_font(lbl_sound, &lv_font_montserrat_14, 0); + lv_obj_set_style_text_color(lbl_sound, lv_color_hex(COL_INK), 0); + lv_obj_center(lbl_sound); + + make_small_btn(scr, ">", UI_HOR_RES - 44, 128, 32, 28, on_sound_next, + nullptr); + + // Volume lv_obj_t *vol_cap = lv_label_create(scr); lv_label_set_text(vol_cap, "VOL"); lv_obj_set_style_text_font(vol_cap, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(vol_cap, lv_color_hex(COL_MUTED), 0); - lv_obj_set_pos(vol_cap, 12, 30); + lv_obj_set_pos(vol_cap, 12, 164); slider_vol = lv_slider_create(scr); lv_obj_set_size(slider_vol, UI_HOR_RES - 12 - 56 - 44, 14); - lv_obj_set_pos(slider_vol, 52, 30); + lv_obj_set_pos(slider_vol, 52, 164); lv_slider_set_range(slider_vol, 0, 100); lv_slider_set_value(slider_vol, audio_get_volume(), LV_ANIM_OFF); lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_LINE), LV_PART_MAIN); @@ -267,97 +486,101 @@ void ui_create() { lv_obj_set_style_bg_opa(slider_vol, LV_OPA_COVER, LV_PART_INDICATOR); lv_obj_set_style_bg_color(slider_vol, lv_color_hex(COL_INK), LV_PART_KNOB); lv_obj_set_style_pad_all(slider_vol, 4, LV_PART_KNOB); - lv_obj_add_event_cb(slider_vol, on_vol_changed, LV_EVENT_VALUE_CHANGED, nullptr); + lv_obj_add_event_cb(slider_vol, on_vol_changed, LV_EVENT_VALUE_CHANGED, + nullptr); lbl_vol = lv_label_create(scr); lv_label_set_text(lbl_vol, "40%"); lv_obj_set_style_text_font(lbl_vol, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_vol, lv_color_hex(COL_INK), 0); - lv_obj_align(lbl_vol, LV_ALIGN_TOP_RIGHT, -12, 30); + lv_obj_align(lbl_vol, LV_ALIGN_TOP_RIGHT, -12, 164); lbl_audio = lv_label_create(scr); lv_label_set_text(lbl_audio, "Speaker OFF"); lv_obj_set_style_text_font(lbl_audio, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_audio, lv_color_hex(COL_MUTED), 0); - lv_obj_set_pos(lbl_audio, 12, 48); + lv_obj_set_pos(lbl_audio, 12, 182); - // Tabs: Beep | WAV - tabview = lv_tabview_create(scr); - lv_obj_set_pos(tabview, 8, 66); - lv_obj_set_size(tabview, UI_HOR_RES - 16, 110); - lv_tabview_set_tab_bar_size(tabview, 28); - lv_obj_set_style_bg_color(tabview, lv_color_hex(COL_BG), 0); - lv_obj_add_event_cb(tabview, on_tab_changed, LV_EVENT_VALUE_CHANGED, nullptr); - - lv_obj_t *tab_beep = lv_tabview_add_tab(tabview, "Beep"); - lv_obj_t *tab_wav = lv_tabview_add_tab(tabview, "WAV"); - lv_obj_set_style_bg_color(tab_beep, lv_color_hex(COL_BG), 0); - lv_obj_set_style_bg_color(tab_wav, lv_color_hex(COL_BG), 0); - lv_obj_set_style_pad_all(tab_beep, 0, 0); - lv_obj_set_style_pad_all(tab_wav, 0, 0); - - list_beep = lv_obj_create(tab_beep); - style_scroll_list(list_beep); - - list_wav = lv_obj_create(tab_wav); - style_scroll_list(list_wav); - - const int btn_y = 184; - const int btn_h = 48; - const int gap = 10; - const int btn_w = (UI_HOR_RES - 24 - gap) / 2; + const int btn_y = 200; + const int btn_h = 36; + const int bgap = 8; + const int btn_w = (UI_HOR_RES - 24 - bgap * 2) / 3; btn_speaker = lv_button_create(scr); lv_obj_set_pos(btn_speaker, 12, btn_y); lv_obj_set_size(btn_speaker, btn_w, btn_h); lv_obj_add_flag(btn_speaker, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(btn_speaker, on_speaker_clicked, LV_EVENT_CLICKED, nullptr); + lv_obj_add_event_cb(btn_speaker, on_speaker_clicked, LV_EVENT_CLICKED, + nullptr); style_btn(btn_speaker, COL_BTN_OFF); lv_obj_t *spk_cap = lv_label_create(btn_speaker); - lv_label_set_text(spk_cap, "SPEAKER"); + lv_label_set_text(spk_cap, "SPK"); lv_obj_set_style_text_font(spk_cap, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(spk_cap, lv_color_hex(COL_MUTED), 0); - lv_obj_align(spk_cap, LV_ALIGN_TOP_MID, 0, 4); + lv_obj_align(spk_cap, LV_ALIGN_LEFT_MID, 8, 0); lbl_speaker = lv_label_create(btn_speaker); lv_label_set_text(lbl_speaker, "OFF"); - lv_obj_set_style_text_font(lbl_speaker, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_font(lbl_speaker, &lv_font_montserrat_14, 0); lv_obj_set_style_text_color(lbl_speaker, lv_color_hex(COL_INK), 0); - lv_obj_align(lbl_speaker, LV_ALIGN_BOTTOM_MID, 0, -6); + lv_obj_align(lbl_speaker, LV_ALIGN_RIGHT_MID, -8, 0); - btn_play = lv_button_create(scr); - lv_obj_set_pos(btn_play, 12 + btn_w + gap, btn_y); - lv_obj_set_size(btn_play, btn_w, btn_h); - lv_obj_add_flag(btn_play, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_event_cb(btn_play, on_play_clicked, LV_EVENT_CLICKED, nullptr); - style_btn(btn_play, COL_BTN_PLAY); + btn_start = lv_button_create(scr); + lv_obj_set_pos(btn_start, 12 + btn_w + bgap, btn_y); + lv_obj_set_size(btn_start, btn_w, btn_h); + lv_obj_add_flag(btn_start, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(btn_start, on_start_clicked, LV_EVENT_CLICKED, nullptr); + style_btn(btn_start, COL_BTN_PLAY); - lv_obj_t *play_cap = lv_label_create(btn_play); - lv_label_set_text(play_cap, "SOUND"); - lv_obj_set_style_text_font(play_cap, &lv_font_montserrat_14, 0); - lv_obj_set_style_text_color(play_cap, lv_color_hex(COL_MUTED), 0); - lv_obj_align(play_cap, LV_ALIGN_TOP_MID, 0, 4); + lbl_start = lv_label_create(btn_start); + lv_label_set_text(lbl_start, "START"); + lv_obj_set_style_text_font(lbl_start, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(lbl_start, lv_color_hex(COL_INK), 0); + lv_obj_center(lbl_start); - lbl_play = lv_label_create(btn_play); - lv_label_set_text(lbl_play, "PLAY"); - lv_obj_set_style_text_font(lbl_play, &lv_font_montserrat_16, 0); - lv_obj_set_style_text_color(lbl_play, lv_color_hex(COL_INK), 0); - lv_obj_align(lbl_play, LV_ALIGN_BOTTOM_MID, 0, -6); + btn_reset = lv_button_create(scr); + lv_obj_set_pos(btn_reset, 12 + (btn_w + bgap) * 2, btn_y); + lv_obj_set_size(btn_reset, btn_w, btn_h); + lv_obj_add_flag(btn_reset, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(btn_reset, on_reset_clicked, LV_EVENT_CLICKED, nullptr); + style_btn(btn_reset, COL_BTN_OFF); + lv_obj_t *lbl_rst = lv_label_create(btn_reset); + lv_label_set_text(lbl_rst, "RESET"); + lv_obj_set_style_text_font(lbl_rst, &lv_font_montserrat_16, 0); + lv_obj_set_style_text_color(lbl_rst, lv_color_hex(COL_INK), 0); + lv_obj_center(lbl_rst); + + build_custom_panel(scr); + + refresh_sound_label(); + refresh_timer_labels(); ui_refresh_audio_buttons(); - ui_refresh_lists(); } void ui_set_touch_debug(int16_t /*lx*/, int16_t /*ly*/, bool /*down*/) {} +void ui_tick() { + pomo_tick(); + if (pomo_consume_alarm()) { + const int8_t sel = sound_selected(); + if (sel >= 0) { + audio_play_sound((uint8_t)sel); + } + } + refresh_timer_labels(); + ui_refresh_audio_buttons(); +} + void ui_update(const SdCardInfo &info) { if (!info.mounted || !info.present) { - lv_label_set_text(lbl_chip, info.error.length() ? info.error.c_str() : "FAIL"); + lv_label_set_text(lbl_chip, + info.error.length() ? info.error.c_str() : "TF?"); lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_BAD), 0); } else { char chip[40]; - snprintf(chip, sizeof(chip), "OK %s W%u", info.type, (unsigned)wav_list_count()); + snprintf(chip, sizeof(chip), "OK %s", info.type); lv_label_set_text(lbl_chip, chip); lv_obj_set_style_text_color(lbl_chip, lv_color_hex(COL_OK), 0); }