Phase 5: detail screens (scrollable lists + back button) + main controller

- weather_detail: scrollable hourly forecast (time, temp, description,
  rain probability), data logic ported from old weather_detail_screen.cpp
- calendar_detail: scrollable list of all 10 events (summary + formatted
  date/all_day), ported from old calendar_detail_screen.cpp
- mvg: scrollable departures list (line badge U/S colored, destination,
  time/delay, cancellation), 1-minute refresh timer ported from old
  mvg_screen.cpp
- all three use back_button widget (fixed bottom-left)
- main.cpp: Screen controller, navigates between screens via callbacks,
  5-minute inactivity timeout -> home, per-screen tick() for refresh
This commit is contained in:
2026-08-02 10:38:10 +02:00
parent 28e3faf056
commit 592a615a5d
7 changed files with 332 additions and 64 deletions
+77
View File
@@ -0,0 +1,77 @@
#include "calendar_detail_screen.h"
#include "ui/widgets/back_button.h"
#include "api/calendar_api.h"
#include <theme.h>
#include <text_utils.h>
#include <date_utils.h>
#include <lvgl.h>
namespace {
ScreenId (*g_navigate)(ScreenId) = nullptr;
void on_back(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::HOME); }
CalendarData lastCalendar;
bool ok = false;
lv_obj_t *list = nullptr;
void buildList() {
if (!list) return;
lv_obj_clean(list);
if (!ok) {
lv_obj_t *lbl = lv_label_create(list);
lv_label_set_text(lbl, LV_SYMBOL_WARNING " Keine Verbindung");
lv_obj_set_style_text_color(lbl, Theme::accentError(), 0);
return;
}
for (const auto &ev : lastCalendar.events) {
lv_obj_t *row = lv_obj_create(list);
lv_obj_set_size(row, 280, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 6, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t *sum = lv_label_create(row);
lv_label_set_text(sum, sanitizeGermanText(ev.summary.substring(0, 30)).c_str());
lv_obj_set_style_text_color(sum, Theme::text(), 0);
lv_obj_align(sum, LV_ALIGN_TOP_LEFT, 0, 0);
lv_obj_t *when = lv_label_create(row);
lv_label_set_text(when, DateUtils::formatShortDE(ev.startAt, ev.allDay).c_str());
lv_obj_set_style_text_color(when, Theme::accentCalendar(), 0);
lv_obj_align(when, LV_ALIGN_TOP_LEFT, 0, 20);
}
}
} // namespace
void calendarDetailScreen_setNavigator(ScreenId (*nav)(ScreenId)) { g_navigate = nav; }
void calendarDetailScreen_create(Screen &s) {
list = lv_list_create(s.root);
lv_obj_set_pos(list, 0, 0);
lv_obj_set_size(list, 320, 480 - 80);
lv_obj_set_style_bg_color(list, Theme::bg(), 0);
lv_obj_set_style_border_width(list, 0, 0);
back_button_create(s.root, on_back, nullptr);
}
void calendarDetailScreen_refresh(Screen &) {
CalendarData c = fetchCalendar();
ok = c.valid;
if (c.valid) lastCalendar = c;
buildList();
}
void calendarDetailScreen_tick(Screen &) {}
Screen calendarDetailScreen_make() {
Screen s;
s.create_fn = calendarDetailScreen_create;
s.refresh_fn = calendarDetailScreen_refresh;
s.tick_fn = calendarDetailScreen_tick;
return s;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "ui/screen_base.h"
// Calendar detail: scrollable list of all 10 events + back button.
Screen calendarDetailScreen_make();
void calendarDetailScreen_setNavigator(ScreenId (*nav)(ScreenId));
+105
View File
@@ -0,0 +1,105 @@
#include "mvg_screen.h"
#include "ui/widgets/back_button.h"
#include "api/departures_api.h"
#include <theme.h>
#include <text_utils.h>
#include <lvgl.h>
namespace {
ScreenId (*g_navigate)(ScreenId) = nullptr;
void on_back(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::HOME); }
DeparturesData lastData;
bool ok = false;
lv_obj_t *list = nullptr;
unsigned long lastFetchMs = 0;
const unsigned long REFRESH_INTERVAL_MS = 60UL * 1000UL;
void buildList() {
if (!list) return;
lv_obj_clean(list);
if (!ok) {
lv_obj_t *lbl = lv_label_create(list);
lv_label_set_text(lbl, LV_SYMBOL_WARNING " Keine Verbindung");
lv_obj_set_style_text_color(lbl, Theme::accentError(), 0);
return;
}
for (const auto &dep : lastData.departures) {
lv_obj_t *row = lv_obj_create(list);
lv_obj_set_size(row, 280, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 6, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
// Line badge (colored text: U red, S green).
String badge = String(dep.icon) + " " + dep.line;
lv_obj_t *ln = lv_label_create(row);
lv_label_set_text(ln, badge.c_str());
lv_obj_set_style_text_color(ln,
dep.type == "UBAHN" ? Theme::accentUBahn() : Theme::accentSBahn(), 0);
lv_obj_align(ln, LV_ALIGN_TOP_LEFT, 0, 0);
String dest = sanitizeGermanText(dep.destination.substring(0, 20));
lv_obj_t *d = lv_label_create(row);
lv_label_set_text(d, dest.c_str());
lv_obj_set_style_text_color(d, Theme::text(), 0);
lv_obj_align(d, LV_ALIGN_TOP_LEFT, 0, 18);
String right;
if (dep.cancelled) {
right = "Ausfall";
} else {
right = dep.timeStr;
if (dep.delayMin > 0) right += " +" + String(dep.delayMin);
}
lv_obj_t *t = lv_label_create(row);
lv_label_set_text(t, right.c_str());
lv_obj_set_style_text_color(t,
dep.cancelled ? Theme::accentError() : Theme::text(), 0);
lv_obj_align(t, LV_ALIGN_TOP_RIGHT, 0, 18);
}
}
void doFetch() {
DeparturesData d = fetchDepartures();
ok = d.valid;
if (d.valid) lastData = d;
lastFetchMs = millis();
}
} // namespace
void mvgScreen_setNavigator(ScreenId (*nav)(ScreenId)) { g_navigate = nav; }
void mvgScreen_create(Screen &s) {
list = lv_list_create(s.root);
lv_obj_set_pos(list, 0, 0);
lv_obj_set_size(list, 320, 480 - 80);
lv_obj_set_style_bg_color(list, Theme::bg(), 0);
lv_obj_set_style_border_width(list, 0, 0);
back_button_create(s.root, on_back, nullptr);
}
void mvgScreen_refresh(Screen &) {
doFetch();
buildList();
}
void mvgScreen_tick(Screen &) {
if (lastFetchMs == 0 || millis() - lastFetchMs >= REFRESH_INTERVAL_MS) {
doFetch();
buildList();
}
}
Screen mvgScreen_make() {
Screen s;
s.create_fn = mvgScreen_create;
s.refresh_fn = mvgScreen_refresh;
s.tick_fn = mvgScreen_tick;
return s;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "ui/screen_base.h"
// MVG departures: scrollable list of all departures + back button.
Screen mvgScreen_make();
void mvgScreen_setNavigator(ScreenId (*nav)(ScreenId));
+95
View File
@@ -0,0 +1,95 @@
#include "weather_detail_screen.h"
#include "ui/widgets/back_button.h"
#include "api/weather_api.h"
#include <theme.h>
#include <text_utils.h>
#include <lvgl.h>
namespace {
ScreenId (*g_navigate)(ScreenId) = nullptr;
void on_back(lv_event_t *) { if (g_navigate) g_navigate(ScreenId::HOME); }
WeatherData lastWeather;
bool ok = false;
lv_obj_t *list = nullptr;
void buildList() {
if (!list) return;
lv_obj_clean(list);
if (!ok) {
lv_obj_t *lbl = lv_label_create(list);
lv_label_set_text(lbl, LV_SYMBOL_WARNING " Keine Verbindung");
lv_obj_set_style_text_color(lbl, Theme::accentError(), 0);
return;
}
// Header row with current conditions.
String header = String(lastWeather.current.temperature) + " C " +
sanitizeGermanText(lastWeather.current.description);
lv_obj_t *h = lv_label_create(list);
lv_label_set_text(h, header.c_str());
lv_obj_set_style_text_font(h, &lv_font_montserrat_20, 0);
lv_obj_set_style_text_color(h, Theme::text(), 0);
for (const auto &fe : lastWeather.forecast) {
lv_obj_t *row = lv_obj_create(list);
lv_obj_set_size(row, 280, LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(row, 0, 0);
lv_obj_set_style_pad_all(row, 6, 0);
lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
// time hh:mm
int tIdx = fe.time.indexOf('T');
String hhmm = tIdx > 0 ? fe.time.substring(tIdx + 1, tIdx + 6) : fe.time;
String left = hhmm + " " + String(fe.temperature) + "C";
lv_obj_t *l = lv_label_create(row);
lv_label_set_text(l, left.c_str());
lv_obj_set_style_text_color(l, Theme::text(), 0);
lv_obj_align(l, LV_ALIGN_TOP_LEFT, 0, 0);
String desc = sanitizeGermanText(fe.description.substring(0, 16));
if (fe.precipitationType == "rain" && fe.precipitationProbability > 0.0f) {
char buf[8];
snprintf(buf, sizeof(buf), " %.0f%%", fe.precipitationProbability * 100);
desc += buf;
}
lv_obj_t *d = lv_label_create(row);
lv_label_set_text(d, desc.c_str());
lv_obj_set_style_text_color(d, Theme::textDim(), 0);
lv_obj_align(d, LV_ALIGN_TOP_LEFT, 0, 18);
}
}
} // namespace
void weatherDetailScreen_setNavigator(ScreenId (*nav)(ScreenId)) { g_navigate = nav; }
void weatherDetailScreen_create(Screen &s) {
list = lv_list_create(s.root);
lv_obj_set_pos(list, 0, 0);
lv_obj_set_size(list, 320, 480 - 80);
lv_obj_set_style_bg_color(list, Theme::bg(), 0);
lv_obj_set_style_border_width(list, 0, 0);
back_button_create(s.root, on_back, nullptr);
}
void weatherDetailScreen_refresh(Screen &) {
WeatherData w = fetchWeather();
ok = w.valid;
if (w.valid) lastWeather = w;
buildList();
}
void weatherDetailScreen_tick(Screen &) {}
Screen weatherDetailScreen_make() {
Screen s;
s.create_fn = weatherDetailScreen_create;
s.refresh_fn = weatherDetailScreen_refresh;
s.tick_fn = weatherDetailScreen_tick;
return s;
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include "ui/screen_base.h"
// Weather detail: scrollable hourly forecast list + back button.
Screen weatherDetailScreen_make();
void weatherDetailScreen_setNavigator(ScreenId (*nav)(ScreenId));