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)) {