From 52f692e70cd3a3b9f448c53f738e0befa7307b34 Mon Sep 17 00:00:00 2001 From: Eiswolf-BG <37225519+Eiswolf-BG@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:58:53 +0200 Subject: [PATCH] Weather info popup, larger System menu buttons, group Flight toggles Tapping the weather icon in the header now shows a small info popup explaining that the shown weather always follows the currently active location (including an active location preset). The System menu's buttons now fill the available screen height instead of leaving unused space below, computed from a dedicated systemRowRect() helper. "Info" (About) is pinned as the second-to-last entry, directly above Back, so its position stays stable regardless of how many other entries come before it. In the Flight options menu, all plain ON/OFF toggles (LED heartbeat, emergency alert, proximity LED, watchlist alert, hide ground vehicles) are now grouped together at the end of the list, right above Back, instead of being interleaved with the screen/list entries above them. Co-Authored-By: Claude Sonnet 5 --- src/i18n.h | 7 ++++ src/i18n_de.h | 3 ++ src/i18n_en.h | 3 ++ src/i18n_es.h | 3 ++ src/i18n_fr.h | 3 ++ src/i18n_it.h | 3 ++ src/i18n_tr.h | 3 ++ src/main.cpp | 99 +++++++++++++++++++++++++++++++++++++++++++++ src/menu_screen.cpp | 80 ++++++++++++++++++++++++------------ 9 files changed, 178 insertions(+), 26 deletions(-) diff --git a/src/i18n.h b/src/i18n.h index 14bd1cd..f04f7ea 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -214,6 +214,13 @@ enum class StringId : uint8_t { WEBUI_INFO_PARA1, WEBUI_INFO_PARA2, + // Kleines Info-Fenster, das beim Antippen des Wetter-Icons im Header + // erscheint (siehe main.cpp/showWeatherInfo()) - erklaert, dass das + // Wetter zum aktuell aktiven Standort (bzw. aktivem Standort-Preset) + // gehoert. + WEATHER_INFO_TITLE, + WEATHER_INFO_BODY, + COUNT }; diff --git a/src/i18n_de.h b/src/i18n_de.h index 501fdb4..c65fda4 100644 --- a/src/i18n_de.h +++ b/src/i18n_de.h @@ -200,6 +200,9 @@ static const char* const I18N_DE[] = { "Logbuch / WebUI", "Über diese Webseite kannst du das Flugbuch direkt im Browser ansehen, herunterladen und einzeln löschen. Im selben WLAN einfach im Browser aufrufen:", "Keine WLAN-Verbindung - die Seite ist nur erreichbar, solange eine Netzwerkverbindung besteht.", + + "Wetter", + "Das Wetter-Icon zeigt die aktuelle Wetterlage für deinen aktiven Standort. Ist ein Standort-Preset ausgewählt, wird stattdessen dessen Wetter angezeigt.", }; static_assert(sizeof(I18N_DE) / sizeof(I18N_DE[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_en.h b/src/i18n_en.h index a68693a..f4b71f9 100644 --- a/src/i18n_en.h +++ b/src/i18n_en.h @@ -201,6 +201,9 @@ static const char* const I18N_EN[] = { "Logbook / WebUI", "This web page lets you view, download and individually delete the flight logbook right from a browser. While connected to the same WiFi network, open a browser and go to:", "No WiFi connection - the page is only reachable while connected to a network.", + + "Weather", + "The weather icon shows the current weather for your active location. If a location preset is selected, its weather is shown instead.", }; static_assert(sizeof(I18N_EN) / sizeof(I18N_EN[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_es.h b/src/i18n_es.h index 890c57a..12b4070 100644 --- a/src/i18n_es.h +++ b/src/i18n_es.h @@ -200,6 +200,9 @@ static const char* const I18N_ES[] = { "Registro / WebUI", "Esta página web permite ver, descargar y eliminar individualmente el registro de vuelo directamente desde un navegador. Conectado a la misma red WiFi, simplemente abre un navegador y ve a:", "Sin conexión WiFi - la página solo es accesible mientras haya una conexión de red.", + + "Clima", + "El icono del clima muestra el clima actual de tu ubicación activa. Si hay una ubicación guardada seleccionada, se muestra el clima de esa ubicación en su lugar.", }; static_assert(sizeof(I18N_ES) / sizeof(I18N_ES[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_fr.h b/src/i18n_fr.h index be113f9..7a1189d 100644 --- a/src/i18n_fr.h +++ b/src/i18n_fr.h @@ -200,6 +200,9 @@ static const char* const I18N_FR[] = { "Journal / WebUI", "Cette page web permet de consulter, télécharger et supprimer individuellement le journal de vol directement depuis un navigateur. Connecté au même réseau WiFi, ouvrez simplement un navigateur et allez à :", "Pas de connexion WiFi - la page n'est accessible que lorsqu'une connexion réseau est établie.", + + "Météo", + "L'icône météo indique la situation actuelle pour votre position active. Si une position enregistrée est sélectionnée, sa météo est affichée à la place.", }; static_assert(sizeof(I18N_FR) / sizeof(I18N_FR[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_it.h b/src/i18n_it.h index 5972a1e..f0fd2a2 100644 --- a/src/i18n_it.h +++ b/src/i18n_it.h @@ -200,6 +200,9 @@ static const char* const I18N_IT[] = { "Registro / WebUI", "Questa pagina web permette di visualizzare, scaricare ed eliminare singolarmente il registro di volo direttamente dal browser. Connesso alla stessa rete WiFi, apri un browser e vai a:", "Nessuna connessione WiFi - la pagina è raggiungibile solo mentre è attiva una connessione di rete.", + + "Meteo", + "L'icona meteo mostra le condizioni meteo attuali per la tua posizione attiva. Se è selezionata una posizione salvata, viene mostrato invece il meteo di quella posizione.", }; static_assert(sizeof(I18N_IT) / sizeof(I18N_IT[0]) == (size_t)StringId::COUNT, diff --git a/src/i18n_tr.h b/src/i18n_tr.h index 8d6d322..abf6a40 100644 --- a/src/i18n_tr.h +++ b/src/i18n_tr.h @@ -200,6 +200,9 @@ static const char* const I18N_TR[] = { "Kayıt Defteri / WebUI", "Bu web sayfası, uçuş kayıt defterini doğrudan tarayıcıdan görüntülemenizi, indirmenizi ve tek tek silmenizi sağlar. Aynı WiFi ağındayken tarayıcıyı açıp şu adrese gidin:", "WiFi bağlantısı yok - sayfaya yalnızca bir ağ bağlantısı varken erişilebilir.", + + "Hava Durumu", + "Hava durumu simgesi, aktif konumunuz için güncel hava durumunu gösterir. Bir konum ön ayarı seçiliyse, bunun yerine o ön ayarın hava durumu gösterilir.", }; static_assert(sizeof(I18N_TR) / sizeof(I18N_TR[0]) == (size_t)StringId::COUNT, diff --git a/src/main.cpp b/src/main.cpp index 124e756..98aca78 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -164,6 +164,100 @@ void drawWeatherIcon() { } } +// Einfacher Zeilenumbruch fuer Fliesstext, analog zu layoutWrapped() in +// menu_screen.cpp/webui_screen.cpp (siehe "keine geteilten Module"-Konvention +// - jede Screen-Datei hat ihre eigene kleine Kopie). Ohne Scroll-Unterstuetzung, +// da der Infotext des Wetter-Popups kurz genug ist, um immer komplett in die +// verfuegbare Boxhoehe zu passen. +int16_t weatherInfoLayoutWrapped(TFT_eSPI& tftRef, int16_t x, int16_t startY, + int16_t maxWidth, int16_t lineHeight, const String& text) { + int16_t y = startY; + int32_t start = 0; + int32_t len = text.length(); + while (start < len) { + while (start < len && text[start] == ' ') start++; + if (start >= len) break; + + String line = text.substring(start, len); + while (tftRef.textWidth(line) > maxWidth) { + int32_t lastSpace = line.lastIndexOf(' '); + if (lastSpace <= 0) break; + line = line.substring(0, lastSpace); + } + + tftRef.setCursor(x, y); + tftRef.print(line); + y += lineHeight; + start += line.length(); + } + return y; +} + +void weatherInfoDrawButton(TFT_eSPI& tftRef, const Rect& r, const String& label) { + tftRef.fillRoundRect(r.x, r.y, r.w, r.h, 4, TFT_BLACK); + tftRef.drawRoundRect(r.x, r.y, r.w, r.h, 4, TFT_GREEN); + tftRef.setTextDatum(MC_DATUM); + tftRef.setTextColor(TFT_GREEN, TFT_BLACK); + tftRef.drawString(label, r.x + r.w / 2, r.y + r.h / 2); + tftRef.setTextDatum(TL_DATUM); +} + +// Kleines Info-Fenster beim Antippen des Wetter-Icons im Header - erklaert, +// dass das angezeigte Wetter immer zum aktuell aktiven Standort (bzw. +// aktivem Standort-Preset, siehe LocationManager::getHomeLocation()) +// gehoert. Gleicher geboxter Overlay-Stil wie confirmLogbookEnable() in +// menu_screen.cpp, hier aber ohne Warnfarbe und nur mit einem +// Zurueck-Button, da rein informativ (keine Bestaetigung noetig). +void showWeatherInfo(TFT_eSPI& tftRef) { + constexpr int16_t BOX_X = 4; + constexpr int16_t BOX_Y = 4; + constexpr int16_t BOX_W = Config::SCREEN_WIDTH - 2 * BOX_X; + constexpr int16_t BOX_H = Config::SCREEN_HEIGHT - 2 * BOX_Y; + constexpr int16_t TEXT_MAX_WIDTH = BOX_W - 20; + constexpr int16_t LINE_H = 16; + constexpr int16_t TITLE_Y = BOX_Y + 16; + // Eine Leerzeile Abstand zwischen Titel und Fliesstext. + constexpr int16_t VIEW_TOP = TITLE_Y + 12 + LINE_H; + + constexpr int16_t BTN_H = 36; + constexpr int16_t BOTTOM_MARGIN = 8; + constexpr int16_t BACK_Y = BOX_Y + BOX_H - BOTTOM_MARGIN - BTN_H; + + String body = I18n::t(StringId::WEATHER_INFO_BODY); + + Rect backBtn = {(int16_t)(BOX_X + 10), BACK_Y, (int16_t)(BOX_W - 20), BTN_H}; + + MenuStars::reset(); + + auto redraw = [&]() { + tftRef.fillScreen(TFT_BLACK); + tftRef.drawRoundRect(BOX_X, BOX_Y, BOX_W, BOX_H, 6, TFT_GREEN); + + tftRef.setTextDatum(MC_DATUM); + tftRef.setTextColor(TFT_GREEN, TFT_BLACK); + tftRef.setTextSize(2); + tftRef.drawString(I18n::t(StringId::WEATHER_INFO_TITLE), BOX_X + BOX_W / 2, TITLE_Y); + tftRef.setTextSize(1); + tftRef.setTextDatum(TL_DATUM); + + tftRef.setTextColor(TFT_GREEN, TFT_BLACK); + weatherInfoLayoutWrapped(tftRef, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body); + + weatherInfoDrawButton(tftRef, backBtn, I18n::t(StringId::BACK)); + }; + + redraw(); + + while (true) { + TouchInput::Point tap; + if (TouchInput::wasTapped(tap)) { + if (backBtn.contains(tap.x, tap.y)) return; + } + MenuStars::update(tftRef); + delay(20); + } +} + void drawWifiIcon(int16_t rightX, int16_t rowTop, int16_t rowH, int8_t rssi) { uint8_t level; if (rssi >= -55) level = 4; @@ -467,6 +561,11 @@ void loop() { drawHeader(); updateStatusLine(); forceRedraw = true; + } else if (weatherIconRect.contains(tap.x, tap.y)) { + showWeatherInfo(tft); + drawHeader(); + updateStatusLine(); + forceRedraw = true; } else if (tap.y >= CONTENT_TOP) { if (RadarScreen::handleTap(tap.x, tap.y, CONTENT_TOP)) { forceRedraw = true; diff --git a/src/menu_screen.cpp b/src/menu_screen.cpp index 0328be8..1153df6 100644 --- a/src/menu_screen.cpp +++ b/src/menu_screen.cpp @@ -59,6 +59,24 @@ namespace { (int16_t)(Config::SCREEN_WIDTH - 20), FLIGHT_ROW_H}; } + // Eigene, groesser berechnete Zeilenhoehe NUR fuer die System-Seite: mit + // nur 10 Eintraegen (vs. z.B. 14 auf der Flugoptionen-Seite) liess die + // normale rowRect()-Hoehe (22px) unten viel ungenutzten Platz frei. Die + // Hoehe wird hier stattdessen aus der tatsaechlich verfuegbaren + // Bildschirmflaeche errechnet, sodass die Buttons den Platz bis knapp + // ueber den Bildschirmrand ausfuellen - andere Seiten bleiben bei ihren + // eigenen, unveraenderten Zeilenhoehen. + constexpr uint8_t SYSTEM_ROW_COUNT = 10; + constexpr int16_t SYSTEM_ROW_GAP = 4; + constexpr int16_t SYSTEM_START_Y = 18; + constexpr int16_t SYSTEM_END_Y = Config::SCREEN_HEIGHT - 10; + constexpr int16_t SYSTEM_ROW_H = + (SYSTEM_END_Y - SYSTEM_START_Y - (SYSTEM_ROW_COUNT - 1) * SYSTEM_ROW_GAP) / SYSTEM_ROW_COUNT; + Rect systemRowRect(uint8_t index) { + return {10, (int16_t)(SYSTEM_START_Y + index * (SYSTEM_ROW_H + SYSTEM_ROW_GAP)), + (int16_t)(Config::SCREEN_WIDTH - 20), SYSTEM_ROW_H}; + } + void drawButton(TFT_eSPI& tft, const Rect& r, const String& label, bool active = false, bool danger = false) { uint16_t accent = danger ? TFT_RED : TFT_GREEN; @@ -305,16 +323,19 @@ void run(TFT_eSPI& tft) { tft.setCursor(10, 14); tft.println(I18n::t(StringId::MENU_CATEGORY_SYSTEM)); - Rect calibBtn = rowRect(0); - Rect invertBtn = rowRect(1); - Rect brightnessBtn = rowRect(2); - Rect timeoutBtn = rowRect(3); - Rect nightDimBtn = rowRect(4); - Rect backupBtn = rowRect(5); - Rect restoreBtn = rowRect(6); - Rect aboutBtn = rowRect(7); - Rect webuiBtn = rowRect(8); - Rect backBtn = rowRect(9); + Rect calibBtn = systemRowRect(0); + Rect invertBtn = systemRowRect(1); + Rect brightnessBtn = systemRowRect(2); + Rect timeoutBtn = systemRowRect(3); + Rect nightDimBtn = systemRowRect(4); + Rect backupBtn = systemRowRect(5); + Rect restoreBtn = systemRowRect(6); + Rect webuiBtn = systemRowRect(7); + // "Info" (About) steht bewusst als letzter Punkt direkt ueber dem + // Zurueck-Button - so bleibt seine Position stabil, egal wie + // viele weitere Punkte davor noch dazukommen. + Rect aboutBtn = systemRowRect(8); + Rect backBtn = systemRowRect(9); drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE)); @@ -328,8 +349,8 @@ void run(TFT_eSPI& tft) { drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled())); drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP)); drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE)); - drawButton(tft, aboutBtn, I18n::t(StringId::MENU_ABOUT)); drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI)); + drawButton(tft, aboutBtn, I18n::t(StringId::MENU_ABOUT)); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); TouchInput::Point tap; @@ -376,17 +397,24 @@ void run(TFT_eSPI& tft) { tft.setCursor(10, 14); tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT)); + // Reine An/Aus-Schalter (Heartbeat, Notfall-Alarm, Naeherungs-LED, + // Beobachtungsalarm, Bodenfahrzeuge-Filter) stehen bewusst + // gemeinsam am Ende der Liste, direkt ueber dem Zurueck-Button - + // und darunter wiederum die LED-gesteuerten Alarme (Heartbeat, + // Notfall, Naeherung, Beobachtung; siehe LedAlert::Mode) + // zusammenhaengend vor dem nicht-LED-basierten + // Bodenfahrzeuge-Filter. Rect aircraftListBtn = flightRowRect(0); Rect statsBtn = flightRowRect(1); Rect statsHistoryBtn = flightRowRect(2); Rect logFilesBtn = flightRowRect(3); Rect logbookBtn = flightRowRect(4); - Rect heartbeatBtn = flightRowRect(5); - Rect proximityBtn = flightRowRect(6); - Rect emergencyBtn = flightRowRect(7); - Rect locationBtn = flightRowRect(8); - Rect airlineBtn = flightRowRect(9); - Rect watchlistBtn = flightRowRect(10); + Rect locationBtn = flightRowRect(5); + Rect airlineBtn = flightRowRect(6); + Rect watchlistBtn = flightRowRect(7); + Rect heartbeatBtn = flightRowRect(8); + Rect emergencyBtn = flightRowRect(9); + Rect proximityBtn = flightRowRect(10); Rect watchlistAlertBtn = flightRowRect(11); Rect groundBtn = flightRowRect(12); Rect backBtn = flightRowRect(13); @@ -396,12 +424,12 @@ void run(TFT_eSPI& tft) { drawButton(tft, statsHistoryBtn, I18n::t(StringId::MENU_STATS_HISTORY)); drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES)); drawButton(tft, logbookBtn, I18n::t(StringId::MENU_FLIGHT_LOGBOOK) + onOff(SettingsStore::flightLogbookEnabled())); - drawButton(tft, heartbeatBtn, I18n::t(StringId::MENU_LED_HEARTBEAT) + onOff(SettingsStore::ledHeartbeatEnabled())); - drawButton(tft, proximityBtn, I18n::t(StringId::MENU_PROXIMITY_LED) + onOff(SettingsStore::proximityAlertEnabled())); - drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled())); drawButton(tft, locationBtn, I18n::t(StringId::MENU_LOCATION_PRESETS)); drawButton(tft, airlineBtn, I18n::t(StringId::MENU_AIRLINE_FILTER)); drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST)); + drawButton(tft, heartbeatBtn, I18n::t(StringId::MENU_LED_HEARTBEAT) + onOff(SettingsStore::ledHeartbeatEnabled())); + drawButton(tft, emergencyBtn, I18n::t(StringId::MENU_EMERGENCY_ALERT) + onOff(SettingsStore::emergencyAlertEnabled())); + drawButton(tft, proximityBtn, I18n::t(StringId::MENU_PROXIMITY_LED) + onOff(SettingsStore::proximityAlertEnabled())); drawButton(tft, watchlistAlertBtn, I18n::t(StringId::MENU_WATCHLIST_ALERT) + onOff(SettingsStore::watchlistAlertEnabled())); drawButton(tft, groundBtn, I18n::t(StringId::MENU_HIDE_GROUND) + onOff(SettingsStore::hideGroundVehicles())); drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW)); @@ -440,18 +468,18 @@ void run(TFT_eSPI& tft) { // vorhandene alte Datei weiterzuschreiben. SettingsStore::setFlightLogbookSessionFile(""); } - } else if (heartbeatBtn.contains(tap.x, tap.y)) { - SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled()); - } else if (proximityBtn.contains(tap.x, tap.y)) { - SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled()); - } else if (emergencyBtn.contains(tap.x, tap.y)) { - SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled()); } else if (locationBtn.contains(tap.x, tap.y)) { LocationPresetsScreen::run(tft); } else if (airlineBtn.contains(tap.x, tap.y)) { AirlineFilterScreen::run(tft); } else if (watchlistBtn.contains(tap.x, tap.y)) { AircraftWatchlistScreen::run(tft); + } else if (heartbeatBtn.contains(tap.x, tap.y)) { + SettingsStore::setLedHeartbeatEnabled(!SettingsStore::ledHeartbeatEnabled()); + } else if (emergencyBtn.contains(tap.x, tap.y)) { + SettingsStore::setEmergencyAlertEnabled(!SettingsStore::emergencyAlertEnabled()); + } else if (proximityBtn.contains(tap.x, tap.y)) { + SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled()); } else if (watchlistAlertBtn.contains(tap.x, tap.y)) { SettingsStore::setWatchlistAlertEnabled(!SettingsStore::watchlistAlertEnabled()); } else if (groundBtn.contains(tap.x, tap.y)) {