|
|
|
@@ -104,6 +104,12 @@ static lv_obj_t* bulbTempW = nullptr; static lv_obj_t* bulbTempD = nullptr;
|
|
|
|
|
static lv_obj_t* g_designBtn[TD_DESIGN_COUNT] = {}; // Umschalter auf der Info-Seite
|
|
|
|
|
static lv_obj_t* g_readyPill = nullptr; // "BEREIT"-Pille im Header (Kessel im Zielband)
|
|
|
|
|
static bool g_readyState = false; // mit Hysterese, gegen Flackern an der Bandgrenze
|
|
|
|
|
// Profil-Chip im Header (zuletzt geladenes Profil; "*" = danach geaenderte Einstellungen).
|
|
|
|
|
// Antippen oeffnet die Profil-Schnellwahl (Overlay mit allen Profilen zum Direkt-Laden).
|
|
|
|
|
static lv_obj_t* g_profChip = nullptr;
|
|
|
|
|
static lv_obj_t* g_profPicker = nullptr; // Schnellwahl-Overlay (Vollbild-Abdunkelung)
|
|
|
|
|
static lv_obj_t* g_profPickerList = nullptr; // Button-Liste im Overlay
|
|
|
|
|
static lv_obj_t* g_profPickerInfo = nullptr; // "Lade Profile..."-Hinweis im Overlay
|
|
|
|
|
static lv_obj_t* cellShot; static lv_obj_t* lblShotTimer; static lv_obj_t* g_bbtBar = nullptr;
|
|
|
|
|
static lv_obj_t* cellSteam; static lv_obj_t* lblSteamTimer; static lv_obj_t* g_sbtBar = nullptr;
|
|
|
|
|
static lv_obj_t* cellBezug; static lv_obj_t* lblBezug; static lv_obj_t* g_bbwBar = nullptr;
|
|
|
|
@@ -2375,6 +2381,112 @@ static void prof_save_cb(lv_event_t*) {
|
|
|
|
|
}
|
|
|
|
|
static void prof_refresh_cb(lv_event_t*) { if (g_client) g_client->sendListProfiles(); }
|
|
|
|
|
|
|
|
|
|
// --- Profil-Schnellwahl: Overlay mit allen Profilen zum Direkt-Laden ------------------
|
|
|
|
|
// Geoeffnet ueber den Profil-Chip im Header; ein Tap laedt das Profil sofort (die
|
|
|
|
|
// Bestaetigung uebernimmt der Toast + der aktualisierte Chip aus dem naechsten State).
|
|
|
|
|
static void prof_picker_close() {
|
|
|
|
|
if (!g_profPicker) return;
|
|
|
|
|
lv_obj_delete(g_profPicker);
|
|
|
|
|
g_profPicker = nullptr;
|
|
|
|
|
g_profPickerList = nullptr;
|
|
|
|
|
g_profPickerInfo = nullptr;
|
|
|
|
|
}
|
|
|
|
|
static void prof_picker_bg_cb(lv_event_t* e) {
|
|
|
|
|
// nur ein Tap auf die Abdunkelung selbst schliesst (Karte bubbelt nicht)
|
|
|
|
|
if (lv_event_get_target(e) == lv_event_get_current_target(e)) prof_picker_close();
|
|
|
|
|
}
|
|
|
|
|
static void prof_picker_close_cb(lv_event_t*) { prof_picker_close(); }
|
|
|
|
|
static void prof_picker_load_cb(lv_event_t* e) {
|
|
|
|
|
lv_obj_t* btn = (lv_obj_t*)lv_event_get_current_target(e);
|
|
|
|
|
lv_obj_t* lbl = lv_obj_get_child(btn, 0); // Kind 0 = Name (pur), Kind 1 = optionales Haekchen
|
|
|
|
|
if (!lbl || !g_client) return;
|
|
|
|
|
String name = lv_label_get_text(lbl);
|
|
|
|
|
prof_picker_close();
|
|
|
|
|
if (name == g_state.activeProfile && !g_state.profileDirty) {
|
|
|
|
|
showToast(("Profil ist bereits aktiv: " + name).c_str(), false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
g_client->sendLoadProfile(name);
|
|
|
|
|
showToast(("Lade Profil: " + name).c_str(), false);
|
|
|
|
|
}
|
|
|
|
|
static void prof_picker_fill(const String profiles[], int count) {
|
|
|
|
|
if (!g_profPickerList) return;
|
|
|
|
|
lv_obj_clean(g_profPickerList);
|
|
|
|
|
if (g_profPickerInfo) {
|
|
|
|
|
if (count == 0) {
|
|
|
|
|
lv_label_set_text(g_profPickerInfo, "Keine Profile gespeichert.");
|
|
|
|
|
lv_obj_remove_flag(g_profPickerInfo, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
} else {
|
|
|
|
|
lv_obj_add_flag(g_profPickerInfo, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
|
bool active = (profiles[i] == g_state.activeProfile);
|
|
|
|
|
lv_obj_t* b = lv_button_create(g_profPickerList);
|
|
|
|
|
lv_obj_set_width(b, LV_PCT(100));
|
|
|
|
|
lv_obj_set_style_bg_color(b, active ? COL_ACCENT : COL_CARD2, 0);
|
|
|
|
|
lv_obj_set_style_radius(b, 10, 0);
|
|
|
|
|
lv_obj_set_style_pad_ver(b, 12, 0);
|
|
|
|
|
lv_obj_set_style_pad_hor(b, 14, 0);
|
|
|
|
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
|
|
|
|
lv_obj_t* l = lv_label_create(b); // Kind 0: purer Profilname (prof_picker_load_cb liest ihn)
|
|
|
|
|
lv_label_set_text(l, profiles[i].c_str());
|
|
|
|
|
lv_obj_set_style_text_color(l, active ? lv_color_hex(0x000000) : COL_TEXT, 0);
|
|
|
|
|
lv_obj_align(l, LV_ALIGN_LEFT_MID, 0, 0);
|
|
|
|
|
if (active) {
|
|
|
|
|
lv_obj_t* chk = lv_label_create(b);
|
|
|
|
|
lv_label_set_text(chk, LV_SYMBOL_OK);
|
|
|
|
|
lv_obj_set_style_text_color(chk, lv_color_hex(0x000000), 0);
|
|
|
|
|
lv_obj_align(chk, LV_ALIGN_RIGHT_MID, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
lv_obj_add_event_cb(b, prof_picker_load_cb, LV_EVENT_CLICKED, nullptr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
static void prof_picker_open() {
|
|
|
|
|
if (g_profPicker) return;
|
|
|
|
|
g_profPicker = lv_obj_create(lv_screen_active());
|
|
|
|
|
lv_obj_set_size(g_profPicker, LV_PCT(100), LV_PCT(100));
|
|
|
|
|
lv_obj_set_pos(g_profPicker, 0, 0);
|
|
|
|
|
lv_obj_set_style_bg_color(g_profPicker, lv_color_hex(0x000000), 0);
|
|
|
|
|
lv_obj_set_style_bg_opa(g_profPicker, LV_OPA_60, 0);
|
|
|
|
|
lv_obj_set_style_border_width(g_profPicker, 0, 0);
|
|
|
|
|
lv_obj_set_style_radius(g_profPicker, 0, 0);
|
|
|
|
|
lv_obj_set_style_pad_all(g_profPicker, 0, 0);
|
|
|
|
|
lv_obj_clear_flag(g_profPicker, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
|
lv_obj_add_event_cb(g_profPicker, prof_picker_bg_cb, LV_EVENT_CLICKED, nullptr);
|
|
|
|
|
|
|
|
|
|
lv_obj_t* card = th_card(g_profPicker);
|
|
|
|
|
lv_obj_set_size(card, 460, 400);
|
|
|
|
|
lv_obj_center(card);
|
|
|
|
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
|
|
|
|
lv_obj_set_style_pad_row(card, 8, 0);
|
|
|
|
|
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
|
|
|
|
|
|
lv_obj_t* hdr = row(card, nullptr);
|
|
|
|
|
lv_obj_t* t = lv_label_create(hdr);
|
|
|
|
|
lv_label_set_text(t, "Profil laden");
|
|
|
|
|
lv_obj_set_style_text_color(t, COL_ACCENT, 0);
|
|
|
|
|
lv_obj_set_style_text_font(t, &lv_font_maven_pro_18, 0);
|
|
|
|
|
lv_obj_t* x = th_button(hdr, LV_SYMBOL_CLOSE, COL_CARD2, COL_TEXT);
|
|
|
|
|
lv_obj_add_event_cb(x, prof_picker_close_cb, LV_EVENT_CLICKED, nullptr);
|
|
|
|
|
|
|
|
|
|
g_profPickerInfo = lv_label_create(card);
|
|
|
|
|
lv_label_set_text(g_profPickerInfo, "Lade Profile...");
|
|
|
|
|
lv_obj_set_style_text_color(g_profPickerInfo, COL_TEXT_DIM, 0);
|
|
|
|
|
|
|
|
|
|
g_profPickerList = lv_obj_create(card);
|
|
|
|
|
lv_obj_set_width(g_profPickerList, LV_PCT(100));
|
|
|
|
|
lv_obj_set_flex_grow(g_profPickerList, 1);
|
|
|
|
|
lv_obj_set_style_bg_opa(g_profPickerList, LV_OPA_TRANSP, 0);
|
|
|
|
|
lv_obj_set_style_border_width(g_profPickerList, 0, 0);
|
|
|
|
|
lv_obj_set_style_pad_all(g_profPickerList, 0, 0);
|
|
|
|
|
lv_obj_set_style_pad_row(g_profPickerList, 6, 0);
|
|
|
|
|
lv_obj_set_flex_flow(g_profPickerList, LV_FLEX_FLOW_COLUMN);
|
|
|
|
|
|
|
|
|
|
if (g_client) g_client->sendListProfiles(); // Antwort fuellt via ui_set_profiles/prof_picker_fill
|
|
|
|
|
}
|
|
|
|
|
static void prof_chip_cb(lv_event_t*) { prof_picker_open(); }
|
|
|
|
|
|
|
|
|
|
static void build_profiles(lv_obj_t* p) {
|
|
|
|
|
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
|
|
|
|
|
|
|
|
@@ -2859,6 +2971,22 @@ void ui_init(ProtocolClient* client) {
|
|
|
|
|
lv_obj_set_flex_grow(g_hdrTitle, 1);
|
|
|
|
|
lv_obj_set_style_text_font(g_hdrTitle, &lv_font_maven_pro_18, 0);
|
|
|
|
|
|
|
|
|
|
// Profil-Chip: zuletzt geladenes Profil ("*" = seitdem geaenderte Einstellungen).
|
|
|
|
|
// Antippen oeffnet die Profil-Schnellwahl. Unsichtbar, solange die S3 kein Profil meldet.
|
|
|
|
|
g_profChip = lv_label_create(header);
|
|
|
|
|
lv_label_set_text(g_profChip, "");
|
|
|
|
|
lv_obj_set_style_bg_color(g_profChip, COL_CARD2, 0);
|
|
|
|
|
lv_obj_set_style_bg_opa(g_profChip, LV_OPA_COVER, 0);
|
|
|
|
|
lv_obj_set_style_text_color(g_profChip, COL_TEXT, 0);
|
|
|
|
|
lv_obj_set_style_radius(g_profChip, 12, 0);
|
|
|
|
|
lv_obj_set_style_pad_hor(g_profChip, 10, 0);
|
|
|
|
|
lv_obj_set_style_pad_ver(g_profChip, 3, 0);
|
|
|
|
|
lv_obj_set_style_margin_right(g_profChip, 10, 0);
|
|
|
|
|
lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_CLICKABLE);
|
|
|
|
|
lv_obj_set_ext_click_area(g_profChip, 8);
|
|
|
|
|
lv_obj_add_event_cb(g_profChip, prof_chip_cb, LV_EVENT_CLICKED, nullptr);
|
|
|
|
|
lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
|
|
|
|
|
g_readyPill = lv_label_create(header); // "BEREIT": Kessel im Zielband (auf allen Seiten sichtbar)
|
|
|
|
|
lv_label_set_text(g_readyPill, "BEREIT");
|
|
|
|
|
lv_obj_set_style_bg_color(g_readyPill, COL_OK, 0);
|
|
|
|
@@ -3437,6 +3565,24 @@ void ui_update(const MachineState& st) {
|
|
|
|
|
else ui_fade_out_hide(g_readyPill, UI_FADE_MS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Profil-Chip im Header: Name des zuletzt geladenen Profils, "*" wenn seitdem
|
|
|
|
|
// profil-relevante Einstellungen geaendert wurden. Lange Namen werden gekuerzt,
|
|
|
|
|
// damit der Header nicht ueberlaeuft. Leer (alte S3-FW / kein Profil) -> versteckt.
|
|
|
|
|
if (g_profChip) {
|
|
|
|
|
if (st.activeProfile.length() > 0) {
|
|
|
|
|
String t = st.activeProfile;
|
|
|
|
|
if (t.length() > 18) {
|
|
|
|
|
int cut = 17; // nicht mitten in einem UTF-8-Mehrbyte-Zeichen (Umlaut) schneiden
|
|
|
|
|
while (cut > 0 && ((uint8_t)t[cut] & 0xC0) == 0x80) cut--;
|
|
|
|
|
t = t.substring(0, cut) + "...";
|
|
|
|
|
}
|
|
|
|
|
if (st.profileDirty) t += " *";
|
|
|
|
|
label_set_if_changed(g_profChip, t.c_str());
|
|
|
|
|
lv_obj_remove_flag(g_profChip, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
} else {
|
|
|
|
|
lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Timer-Synchronisation (die laufende Anzeige macht der 100-ms-Timer dash_timers_cb)
|
|
|
|
|
// Tarier-Phase vor Brew-by-Weight-Bezug -> dash_timers_cb zeigt "Tariere..." im Shot-Timer
|
|
|
|
|
g_scaleTaring = st.scaleTaring;
|
|
|
|
@@ -3629,12 +3775,16 @@ void ui_update(const MachineState& st) {
|
|
|
|
|
void ui_toast(const char* txt, bool error) { showToast(txt, error); }
|
|
|
|
|
|
|
|
|
|
void ui_set_profiles(const String profiles[], int count) {
|
|
|
|
|
// Schnellwahl-Overlay (falls offen) mit derselben Antwort befuellen
|
|
|
|
|
prof_picker_fill(profiles, count);
|
|
|
|
|
if (!profList) return;
|
|
|
|
|
lv_obj_clean(profList);
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
|
lv_obj_t* b = lv_list_add_button(profList, LV_SYMBOL_FILE, profiles[i].c_str());
|
|
|
|
|
// aktives Profil markieren (Haekchen + Akzentfarbe statt Datei-Symbol)
|
|
|
|
|
bool active = (profiles[i] == g_state.activeProfile);
|
|
|
|
|
lv_obj_t* b = lv_list_add_button(profList, active ? LV_SYMBOL_OK : LV_SYMBOL_FILE, profiles[i].c_str());
|
|
|
|
|
lv_obj_set_style_bg_color(b, COL_CARD2, 0);
|
|
|
|
|
lv_obj_set_style_text_color(b, COL_TEXT, 0);
|
|
|
|
|
lv_obj_set_style_text_color(b, active ? COL_ACCENT : COL_TEXT, 0);
|
|
|
|
|
lv_obj_add_event_cb(b, prof_select_cb, LV_EVENT_CLICKED, nullptr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|