#include "menu.h" #include "board.h" #include "display.h" #include "input.h" #include "leds.h" #include "net.h" #include "settings.h" #include "ui.h" #include "esp_log.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include #include static const char *TAG = "menu"; #define IDLE_CLOSE_MS 45000 typedef enum { VIEW_TOP, VIEW_FAV_PLAY, VIEW_FAV_SAVE, VIEW_FAV_FORGET } view_t; enum { ROW_FAV_TOGGLE, ROW_FAVS, ROW_SOURCE, ROW_INTERVAL, ROW_LISTING, ROW_LCD, ROW_RING, ROW_CAPTION, ROW_FORGET, ROW_WIFI, ROW_IP, ROW_CLOSE, TOP_ROWS }; static ui_menu_row_t rows[FAV_SLOTS + 1]; static char values[TOP_ROWS][20]; static char slot_labels[FAV_SLOTS][4]; static const char *listing_name(uint8_t l) { return l == LISTING_RATING ? "Top rated" : l == LISTING_NEW ? "Newest" : "Mixed"; } static int build_top(const cam_info_t *current, int fav_slot) { if (fav_slot >= 0) snprintf(values[ROW_FAV_TOGGLE], sizeof values[0], "slot %d", fav_slot + 1); else values[ROW_FAV_TOGGLE][0] = 0; snprintf(values[ROW_FAVS], sizeof values[0], "%d of %d", fav_count(), FAV_SLOTS); snprintf(values[ROW_INTERVAL], sizeof values[0], "%u s", settings.interval_s); snprintf(values[ROW_LCD], sizeof values[0], "%u%%", settings.lcd_brightness); snprintf(values[ROW_RING], sizeof values[0], "%u%%", (settings.led_brightness * 100 + 127) / 255); rows[ROW_FAV_TOGGLE] = (ui_menu_row_t){fav_slot >= 0 ? "Remove from favourites" : "Save as favourite", values[ROW_FAV_TOGGLE], current == NULL}; rows[ROW_FAVS] = (ui_menu_row_t){"Favourites", values[ROW_FAVS], false}; rows[ROW_SOURCE] = (ui_menu_row_t){"Source", settings.source == SOURCE_FAVORITES ? "Favourites" : "Random", false}; rows[ROW_INTERVAL] = (ui_menu_row_t){"Time per camera", values[ROW_INTERVAL], false}; rows[ROW_LISTING] = (ui_menu_row_t){"Listing", listing_name(settings.listing), false}; rows[ROW_LCD] = (ui_menu_row_t){"Screen brightness", values[ROW_LCD], false}; rows[ROW_RING] = (ui_menu_row_t){"Ring brightness", values[ROW_RING], false}; rows[ROW_CAPTION] = (ui_menu_row_t){"Caption", settings.caption ? "Shown" : "Hidden", false}; rows[ROW_FORGET] = (ui_menu_row_t){"Forget a favourite", NULL, fav_count() == 0}; rows[ROW_WIFI] = (ui_menu_row_t){"Wi-Fi setup", NULL, false}; rows[ROW_IP] = (ui_menu_row_t){board->name, net_ip_str(), true}; rows[ROW_CLOSE] = (ui_menu_row_t){"Close", NULL, false}; return TOP_ROWS; } // Slot list: "1 .. 12" with the place name, then "Back". static int build_slots(void) { for (int i = 0; i < FAV_SLOTS; i++) { const cam_info_t *c = fav_get(i); snprintf(slot_labels[i], sizeof slot_labels[i], "%d", i + 1); rows[i] = (ui_menu_row_t){slot_labels[i], c ? c->place : "empty", c == NULL}; } rows[FAV_SLOTS] = (ui_menu_row_t){"Back", NULL, false}; return FAV_SLOTS + 1; } static int clampi(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; } menu_result_t menu_run(const cam_info_t *current, int *fav_slot_out) { view_t view = VIEW_TOP; int sel = 0, top = 0, count = 0; bool editing = false; menu_result_t result = MENU_RESUME; leds_set_mode(LED_SOLID, 0x202028); while (1) { int fav_slot = current ? fav_find(current) : -1; const char *title = "insecam widget"; if (view == VIEW_TOP) count = build_top(current, fav_slot); else { count = build_slots(); title = view == VIEW_FAV_PLAY ? "Favourites" : view == VIEW_FAV_SAVE ? "Save to slot" : "Forget favourite"; } sel = clampi(sel, 0, count - 1); if (sel < top) top = sel; if (sel >= top + UI_MENU_ROWS) top = sel - UI_MENU_ROWS + 1; ui_menu_show(title, rows, count, top, sel, editing); input_event_t ev = input_wait(IDLE_CLOSE_MS); if (ev == INPUT_NONE) break; // idle: close // --- editing a number: knob changes it, click confirms if (editing) { int dir = ev == INPUT_CW ? 1 : ev == INPUT_CCW ? -1 : 0; if (dir) { if (sel == ROW_INTERVAL) settings.interval_s = clampi(settings.interval_s + dir * 5, 5, 300); else if (sel == ROW_LCD) { settings.lcd_brightness = clampi(settings.lcd_brightness + dir * 5, 5, 100); display_set_backlight(settings.lcd_brightness); } else if (sel == ROW_RING) { settings.led_brightness = clampi(settings.led_brightness + dir * 13, 0, 255); leds_set_brightness(settings.led_brightness); } } else if (ev == INPUT_CLICK || ev == INPUT_DOUBLE || ev == INPUT_LONG) { editing = false; settings_save(); } continue; } if (ev == INPUT_CW) { sel++; continue; } if (ev == INPUT_CCW) { sel--; continue; } if (ev == INPUT_LONG || ev == INPUT_KEY) { if (view == VIEW_TOP) break; view = VIEW_TOP; sel = top = 0; continue; } if (ev != INPUT_CLICK && ev != INPUT_DOUBLE) continue; // --- click if (view != VIEW_TOP) { if (sel == FAV_SLOTS) { view = VIEW_TOP; sel = top = 0; continue; } const cam_info_t *c = fav_get(sel); char msg[48]; if (view == VIEW_FAV_SAVE) { if (current) { fav_set(sel, current); snprintf(msg, sizeof msg, "Saved to slot %d", sel + 1); ui_toast(msg, 1500); } view = VIEW_TOP; sel = top = 0; } else if (view == VIEW_FAV_PLAY) { if (!c) { ui_toast("Empty slot", 1000); continue; } *fav_slot_out = sel; result = MENU_PLAY_FAV; goto out; } else { // VIEW_FAV_FORGET if (!c) { ui_toast("Empty slot", 1000); continue; } fav_clear(sel); snprintf(msg, sizeof msg, "Forgot slot %d", sel + 1); ui_toast(msg, 1500); } continue; } switch (sel) { case ROW_FAV_TOGGLE: if (!current) break; if (fav_slot >= 0) { fav_clear(fav_slot); ui_toast("Removed from favourites", 1500); } else { view = VIEW_FAV_SAVE; sel = top = 0; } break; case ROW_FAVS: view = VIEW_FAV_PLAY; sel = top = 0; break; case ROW_FORGET: view = VIEW_FAV_FORGET; sel = top = 0; break; case ROW_SOURCE: settings.source = settings.source == SOURCE_RANDOM ? SOURCE_FAVORITES : SOURCE_RANDOM; if (settings.source == SOURCE_FAVORITES && fav_count() == 0) ui_toast("No favourites saved yet", 1500); settings_save(); break; case ROW_LISTING: settings.listing = (settings.listing + 1) % 3; settings_save(); break; case ROW_CAPTION: settings.caption = !settings.caption; settings_save(); break; case ROW_INTERVAL: case ROW_LCD: case ROW_RING: editing = true; break; case ROW_WIFI: settings_request_portal(); ui_status("Wi-Fi setup", "restarting into the setup hotspot"); vTaskDelay(pdMS_TO_TICKS(800)); esp_restart(); break; case ROW_CLOSE: goto out; default: break; } } out: ui_menu_hide(); ui_set_overlay_visible(settings.caption); ESP_LOGI(TAG, "closed (%s)", result == MENU_PLAY_FAV ? "play favourite" : "resume"); return result; }