915 lines
44 KiB
C++
915 lines
44 KiB
C++
#include "menu_screen.h"
|
|
#include "touch_input.h"
|
|
#include "calibration_screen.h"
|
|
#include "wifi_manage_screen.h"
|
|
#include "stats_screen.h"
|
|
#include "stats_history_screen.h"
|
|
#include "logbook_files_screen.h"
|
|
#include "webui_screen.h"
|
|
#include "ota_update.h"
|
|
#include "net_task.h"
|
|
#include "location_presets_screen.h"
|
|
#include "airline_filter_screen.h"
|
|
#include "aircraft_watchlist_screen.h"
|
|
#include "aircraft_list_screen.h"
|
|
#include "route_filter_screen.h"
|
|
#include "about_screen.h"
|
|
#include "brightness_screen.h"
|
|
#include "timeout_screen.h"
|
|
#include "language_screen.h"
|
|
#include "units_screen.h"
|
|
#include "settings_backup.h"
|
|
#include "settings_store.h"
|
|
#include "menu_stars.h"
|
|
#include "i18n.h"
|
|
#include "config.h"
|
|
#include <time.h>
|
|
|
|
namespace MenuScreen {
|
|
|
|
namespace {
|
|
struct Rect {
|
|
int16_t x, y, w, h;
|
|
bool contains(int16_t px, int16_t py) const {
|
|
return px >= x && px < x + w && py >= y && py < y + h;
|
|
}
|
|
};
|
|
|
|
// ROW_GAP/ROW_START_Y bleiben unveraendert (werden von flightRowRect()
|
|
// weiter unten mitbenutzt, siehe FLIGHT_ROW_H).
|
|
constexpr int16_t ROW_GAP = 1;
|
|
constexpr int16_t ROW_START_Y = 18;
|
|
|
|
// Region-Unterseite (Sprache/Einheiten/Zurueck, nur 3 Eintraege):
|
|
// Zeilenhoehe/-abstand werden aus der tatsaechlich verfuegbaren
|
|
// Bildschirmflaeche errechnet (gleiches Muster wie bei SYSTEM_ROW_H
|
|
// weiter unten), statt die kleine, fuer volle Seiten (z.B.
|
|
// Flugoptionen: 14 Eintraege) gedachte feste Hoehe (vorher 22px) zu
|
|
// benutzen - die liess bei nur 3 Eintraegen fast den ganzen Bildschirm
|
|
// leer und machte die Buttons winzig und schwer zu treffen.
|
|
constexpr uint8_t REGION_ROW_COUNT = 3;
|
|
constexpr int16_t REGION_ROW_GAP = 10;
|
|
constexpr int16_t REGION_END_Y = Config::SCREEN_HEIGHT - 10;
|
|
constexpr int16_t REGION_ROW_H =
|
|
(REGION_END_Y - ROW_START_Y - (REGION_ROW_COUNT - 1) * REGION_ROW_GAP) / REGION_ROW_COUNT;
|
|
|
|
Rect rowRect(uint8_t index) {
|
|
return {10, (int16_t)(ROW_START_Y + index * (REGION_ROW_H + REGION_ROW_GAP)),
|
|
(int16_t)(Config::SCREEN_WIDTH - 20), REGION_ROW_H};
|
|
}
|
|
|
|
constexpr int16_t CAT_ROW_H = 50;
|
|
constexpr int16_t CAT_ROW_GAP = 10;
|
|
constexpr int16_t CAT_START_Y = 30;
|
|
|
|
Rect catRowRect(uint8_t index) {
|
|
return {10, (int16_t)(CAT_START_Y + index * (CAT_ROW_H + CAT_ROW_GAP)),
|
|
(int16_t)(Config::SCREEN_WIDTH - 20), CAT_ROW_H};
|
|
}
|
|
|
|
// Eigene, etwas kompaktere Zeilenhoehe NUR fuer die Flugoptionen-Seite:
|
|
// die normale rowRect()-Hoehe (22px) liess bei 13 Eintraegen schon keinen
|
|
// Platz mehr fuer einen weiteren - andere Seiten (Region/System) bleiben
|
|
// bei der normalen Hoehe unveraendert, um dort nichts zu verschieben.
|
|
constexpr int16_t FLIGHT_ROW_H = 19;
|
|
Rect flightRowRect(uint8_t index) {
|
|
return {10, (int16_t)(ROW_START_Y + index * (FLIGHT_ROW_H + ROW_GAP)),
|
|
(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.
|
|
// War 10, bevor "Sicherung"/"Wiederherstellen" in die neue
|
|
// "Sicherung & Reset"-Unterseite ausgelagert wurden (siehe
|
|
// BACKUP_RESET_ROW_COUNT unten) und dort durch einen einzelnen
|
|
// Ordner-Button ersetzt wurden: -2 (Backup/Restore raus) +1 (neuer
|
|
// Ordner-Button) = 9. Danach +1 fuer den neuen "Nach Update
|
|
// suchen"-Punkt (OTA-Update, siehe ota_update.h) = 10. Der
|
|
// Ruhebildschirm-Umschalter, der zwischenzeitlich hier eine eigene
|
|
// Zeile hatte, ist in den neuen Bildschirm-Timeout-Screen umgezogen
|
|
// (siehe timeout_screen.cpp) - Zeilenzahl bleibt dadurch bei 10.
|
|
constexpr uint8_t SYSTEM_ROW_COUNT = 11; // +1 fuer Eco-Mode
|
|
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};
|
|
}
|
|
|
|
// Eigene Zeilenhoehe fuer die neue "Sicherung & Reset"-Unterseite (4
|
|
// Eintraege: Sichern/Wiederherstellen/Zuruecksetzen/Zurueck) - gleiches
|
|
// "aus verfuegbarem Platz errechnen"-Muster wie bei SYSTEM_ROW_H.
|
|
// ROW_START_Y wird mitbenutzt (siehe oben, gemeinsam mit rowRect()/
|
|
// flightRowRect()), nur GAP/COUNT/H sind eigene Werte.
|
|
constexpr uint8_t BACKUP_RESET_ROW_COUNT = 4;
|
|
constexpr int16_t BACKUP_RESET_ROW_GAP = 10;
|
|
constexpr int16_t BACKUP_RESET_END_Y = Config::SCREEN_HEIGHT - 10;
|
|
constexpr int16_t BACKUP_RESET_ROW_H =
|
|
(BACKUP_RESET_END_Y - ROW_START_Y - (BACKUP_RESET_ROW_COUNT - 1) * BACKUP_RESET_ROW_GAP) / BACKUP_RESET_ROW_COUNT;
|
|
Rect backupResetRowRect(uint8_t index) {
|
|
return {10, (int16_t)(ROW_START_Y + index * (BACKUP_RESET_ROW_H + BACKUP_RESET_ROW_GAP)),
|
|
(int16_t)(Config::SCREEN_WIDTH - 20), BACKUP_RESET_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;
|
|
uint16_t bg = active ? accent : TFT_BLACK;
|
|
uint16_t fg = active ? TFT_BLACK : accent;
|
|
tft.fillRoundRect(r.x, r.y, r.w, r.h, 4, bg);
|
|
tft.drawRoundRect(r.x, r.y, r.w, r.h, 4, accent);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(fg, bg);
|
|
tft.drawString(label, r.x + r.w / 2, r.y + r.h / 2);
|
|
tft.setTextDatum(TL_DATUM);
|
|
}
|
|
|
|
String onOff(bool on) { return I18n::t(on ? StringId::ON : StringId::OFF); }
|
|
|
|
// Fortschrittspunkte-Anzeige waehrend SettingsBackup::backup()/restore()
|
|
// laufen (siehe Aufrufe unten in Page::BackupReset) - diese sind
|
|
// synchrone, SD-lastige Vorgaenge, die spuerbar dauern koennen und den
|
|
// Button vorher wie eingefroren wirken liessen. SettingsBackup ruft den
|
|
// hier uebergebenen Funktionszeiger vor jedem der beiden Kopiervorgaenge
|
|
// (erst Einstellungen, dann WLAN) auf. Namespace-globale Zeiger/Variablen
|
|
// statt Lambda-Capture, da ein einfacher C-Funktionszeiger uebergeben
|
|
// werden muss (kein std::function im Projekt).
|
|
TFT_eSPI* progressTft = nullptr;
|
|
Rect progressBtnRect;
|
|
String progressLabel;
|
|
uint8_t progressDots = 0;
|
|
|
|
void drawProgressStep() {
|
|
if (!progressTft) return;
|
|
progressDots++;
|
|
String label = progressLabel;
|
|
for (uint8_t i = 0; i < progressDots; i++) label += ".";
|
|
drawButton(*progressTft, progressBtnRect, label);
|
|
}
|
|
|
|
String screenTimeoutLabel(uint8_t minutes) {
|
|
String prefix = I18n::t(StringId::MENU_SCREEN_TIMEOUT_PREFIX);
|
|
if (minutes == 0) return prefix + I18n::t(StringId::NEVER);
|
|
return prefix + String(minutes) + " min";
|
|
}
|
|
|
|
String brightnessLabel(uint8_t percent) {
|
|
return String(I18n::t(StringId::MENU_BRIGHTNESS_PREFIX)) + String(percent) + "%";
|
|
}
|
|
|
|
void showBriefMessage(TFT_eSPI& tft, const String& msg, uint16_t color) {
|
|
tft.fillRect(0, Config::SCREEN_HEIGHT - 18, Config::SCREEN_WIDTH, 18, TFT_BLACK);
|
|
tft.setTextColor(color, TFT_BLACK);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.drawString(msg, Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT - 9);
|
|
tft.setTextDatum(TL_DATUM);
|
|
delay(1200);
|
|
}
|
|
|
|
// Wie die einfache layoutWrapped()-Variante, aber mit optionalem
|
|
// Scroll-Offset und Sichtfenster (scrollY/viewTop/viewBottom) - Zeilen
|
|
// ausserhalb des Sichtfensters werden uebersprungen. Mit draw=false wird
|
|
// nur die Gesamthoehe berechnet, ohne etwas zu zeichnen (fuer die
|
|
// Scroll-Bedarfspruefung vorab).
|
|
int16_t layoutWrapped(TFT_eSPI& tft, int16_t x, int16_t startY, int16_t maxWidth,
|
|
int16_t lineHeight, const String& text, int16_t scrollY,
|
|
int16_t viewTop, int16_t viewBottom, bool draw) {
|
|
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 (tft.textWidth(line) > maxWidth) {
|
|
int32_t lastSpace = line.lastIndexOf(' ');
|
|
if (lastSpace <= 0) break;
|
|
line = line.substring(0, lastSpace);
|
|
}
|
|
|
|
if (draw) {
|
|
int16_t screenY = y - scrollY;
|
|
if (screenY >= viewTop && screenY <= viewBottom) {
|
|
tft.setCursor(x, screenY);
|
|
tft.print(line);
|
|
}
|
|
}
|
|
y += lineHeight;
|
|
start += line.length();
|
|
}
|
|
return y;
|
|
}
|
|
|
|
// Zerlegt einen Titel in bis zu maxLines Zeilen nach dem gleichen
|
|
// Wortumbruch-Prinzip wie layoutWrapped() - ein Titel wird aber nie
|
|
// gescrollt, sondern muss immer komplett sichtbar sein, daher eine
|
|
// eigene, einfachere Variante ohne Scroll-Unterstuetzung.
|
|
int wrapTitleLines(TFT_eSPI& tft, const String& text, int16_t maxWidth, String* outLines, int maxLines) {
|
|
int count = 0;
|
|
int32_t start = 0;
|
|
int32_t len = text.length();
|
|
while (start < len && count < maxLines) {
|
|
while (start < len && text[start] == ' ') start++;
|
|
if (start >= len) break;
|
|
|
|
String line = text.substring(start, len);
|
|
bool isLastAllowedLine = (count == maxLines - 1);
|
|
if (!isLastAllowedLine) {
|
|
while (tft.textWidth(line) > maxWidth) {
|
|
int32_t lastSpace = line.lastIndexOf(' ');
|
|
if (lastSpace <= 0) break;
|
|
line = line.substring(0, lastSpace);
|
|
}
|
|
}
|
|
outLines[count++] = line;
|
|
start += line.length();
|
|
}
|
|
if (count == 0) {
|
|
outLines[0] = text;
|
|
count = 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
// Warn-Ueberlage, die praktisch den kompletten Bildschirm einnimmt (nur
|
|
// ein paar Pixel Rand) - urspruenglich nur fuers Einschalten des
|
|
// Flugbuchs gebaut (erklaert, warum es sich nach 24h automatisch
|
|
// wieder abschaltet), jetzt generisch mit uebergebenen Titel-/Text-
|
|
// StringIds, damit sie auch fuer die "Einstellungen zuruecksetzen"-
|
|
// Bestaetigung (Werksreset) wiederverwendet werden kann - beides sind
|
|
// seltene, potenziell folgenreiche Aktionen, die dieselbe deutliche
|
|
// Warnung verdienen. "Achtung!!!" (titleId) steht ganz oben, mit einer
|
|
// Leerzeile Abstand zum Fliesstext (bodyId) darunter; der Text
|
|
// scrollt bei Bedarf (laengere Uebersetzungen) ueber eigene Pfeil-
|
|
// Buttons, OK/Zurueck bleiben dabei immer unten fix und
|
|
// kollisionsfrei sichtbar. Sternchen laufen im Hintergrund mit, wie auf
|
|
// allen anderen Menue-Screens (nur der Radar-Screen selbst spart sich
|
|
// das wegen der CPU-Last durch Abfragen/Zeichnen). Gibt true zurueck,
|
|
// wenn "OK" angetippt wurde, false bei "Zurueck".
|
|
//
|
|
// title/body stehen VOR dem Aufruf per I18n::t() fest (statt StringIds
|
|
// entgegenzunehmen), damit auch dynamisch zusammengesetzte Texte (z.B.
|
|
// mit eingefuegter Versionsnummer bei OTA) moeglich sind. accentColor
|
|
// (Default TFT_RED fuer die beiden bestehenden, wirklich destruktiven
|
|
// Aufrufer) faerbt Rahmen und Titel - der OTA-Aufrufer uebergibt
|
|
// TFT_GREEN, da ein Update zwar bestaetigt werden sollte, aber keine
|
|
// "gefaehrliche" Loesch-Aktion wie Werksreset/Flugbuch ist.
|
|
bool confirmWarningScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor = TFT_RED) {
|
|
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;
|
|
|
|
// Titel-Text vorab in so viele Zeilen umbrechen, wie bei Größe 2
|
|
// (oder bei zu langem Text Größe 1) nötig sind - siehe
|
|
// wrapTitleLines() oben. VIEW_TOP hängt dadurch von der
|
|
// tatsächlichen Zeilenzahl des Titels ab, ist also kein constexpr
|
|
// mehr wie vorher (wo immer nur eine Titelzeile angenommen wurde).
|
|
tft.setTextSize(2);
|
|
uint8_t titleTextSize = 2;
|
|
if (tft.textWidth(title) > TEXT_MAX_WIDTH) {
|
|
tft.setTextSize(1);
|
|
titleTextSize = 1;
|
|
}
|
|
constexpr int MAX_TITLE_LINES = 3;
|
|
String titleLines[MAX_TITLE_LINES];
|
|
int titleLineCount = wrapTitleLines(tft, title, TEXT_MAX_WIDTH, titleLines, MAX_TITLE_LINES);
|
|
// Fließtext wird immer bei Größe 1 vermessen/gezeichnet (siehe
|
|
// layoutWrapped()-Aufrufe unten) - Größe hier zurücksetzen, falls
|
|
// obiger Titel-Breitentest sie auf 2 stehen gelassen hat, sonst
|
|
// würde die gleich folgende totalH-Berechnung (vor dem ersten
|
|
// redraw()) mit falscher (zu breiter) Schriftgröße rechnen.
|
|
tft.setTextSize(1);
|
|
// Eine Leerzeile Abstand zwischen Titel und Fließtext.
|
|
int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12);
|
|
|
|
constexpr int16_t BTN_H = 36;
|
|
constexpr int16_t BTN_GAP = 8;
|
|
constexpr int16_t BOTTOM_MARGIN = 8;
|
|
constexpr int16_t CANCEL_Y = BOX_Y + BOX_H - BOTTOM_MARGIN - BTN_H;
|
|
constexpr int16_t OK_Y = CANCEL_Y - BTN_GAP - BTN_H;
|
|
constexpr int16_t SCROLL_ROW_H = 28;
|
|
constexpr int16_t SCROLL_ROW_GAP = 8;
|
|
|
|
// Ohne Scroll-Pfeile verfuegbare Texthoehe zuerst pruefen - nur wenn
|
|
// der Text da nicht reinpasst, wird zusaetzlich Platz fuer die
|
|
// Pfeile reserviert (mehr Text-Platz bei kurzen Uebersetzungen).
|
|
constexpr int16_t VIEW_BOTTOM_NO_SCROLL = OK_Y - 8;
|
|
constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP;
|
|
|
|
int16_t totalH = layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, 0, 0, 0, false);
|
|
|
|
bool scrollable = (totalH - VIEW_BOTTOM_NO_SCROLL) > 0;
|
|
int16_t viewBottom = scrollable ? VIEW_BOTTOM_SCROLL : VIEW_BOTTOM_NO_SCROLL;
|
|
int16_t maxScroll = totalH - viewBottom;
|
|
if (maxScroll < 0) maxScroll = 0;
|
|
int16_t scrollY = 0;
|
|
constexpr int16_t SCROLL_STEP = 48;
|
|
|
|
Rect okBtn = {(int16_t)(BOX_X + 10), OK_Y, (int16_t)(BOX_W - 20), BTN_H};
|
|
Rect cancelBtn = {(int16_t)(BOX_X + 10), CANCEL_Y, (int16_t)(BOX_W - 20), BTN_H};
|
|
int16_t scrollRowY = VIEW_BOTTOM_SCROLL + SCROLL_ROW_GAP;
|
|
Rect upBtn = {(int16_t)(BOX_X + BOX_W / 2 - 64), scrollRowY, 60, SCROLL_ROW_H};
|
|
Rect downBtn = {(int16_t)(BOX_X + BOX_W / 2 + 4), scrollRowY, 60, SCROLL_ROW_H};
|
|
|
|
MenuStars::reset();
|
|
|
|
auto redraw = [&]() {
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.drawRoundRect(BOX_X, BOX_Y, BOX_W, BOX_H, 6, accentColor);
|
|
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(accentColor, TFT_BLACK);
|
|
// Statische Titel ("Achtung!!!") sind kurz genug fuer Groesse 2,
|
|
// aber der OTA-Aufrufer baut den Titel dynamisch mit
|
|
// Versionsnummer zusammen (z.B. "Update verfuegbar: v3.5.0") -
|
|
// das passt bei Groesse 2 nicht mehr in die Box und lief vorher
|
|
// links/rechts ueber den Bildschirmrand hinaus. Deshalb hier
|
|
// die Breite bei Groesse 2 pruefen und bei Bedarf auf Groesse 1
|
|
// zurueckfallen, statt eine feste Groesse anzunehmen.
|
|
tft.setTextSize(titleTextSize);
|
|
for (int i = 0; i < titleLineCount; i++) {
|
|
tft.drawString(titleLines[i], BOX_X + BOX_W / 2, (int16_t)(TITLE_Y + i * LINE_H));
|
|
}
|
|
tft.setTextSize(1);
|
|
tft.setTextDatum(TL_DATUM);
|
|
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, scrollY, VIEW_TOP, viewBottom, true);
|
|
|
|
drawButton(tft, okBtn, I18n::t(StringId::OK));
|
|
drawButton(tft, cancelBtn, I18n::t(StringId::BACK));
|
|
if (scrollable) {
|
|
drawButton(tft, upBtn, "^");
|
|
drawButton(tft, downBtn, "v");
|
|
}
|
|
};
|
|
|
|
redraw();
|
|
|
|
while (true) {
|
|
TouchInput::Point tap;
|
|
if (TouchInput::wasTapped(tap)) {
|
|
if (okBtn.contains(tap.x, tap.y)) return true;
|
|
if (cancelBtn.contains(tap.x, tap.y)) return false;
|
|
if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) {
|
|
scrollY -= SCROLL_STEP;
|
|
if (scrollY < 0) scrollY = 0;
|
|
redraw();
|
|
} else if (scrollable && downBtn.contains(tap.x, tap.y) && scrollY < maxScroll) {
|
|
scrollY += SCROLL_STEP;
|
|
if (scrollY > maxScroll) scrollY = maxScroll;
|
|
redraw();
|
|
}
|
|
}
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
}
|
|
|
|
// Einfacher Info-Screen mit nur EINEM Button (kein Abbrechen) - fuer
|
|
// Endzustaende, bei denen es nichts mehr zu entscheiden gibt, nur zu
|
|
// bestaetigen (z.B. Ergebnis eines OTA-Updates). Anders als
|
|
// showBriefMessage() (kurze Meldung unten am Bildschirmrand,
|
|
// verschwindet nach 1,2s automatisch von selbst) bleibt dieser Screen
|
|
// stehen, bis aktiv bestaetigt wird - wichtig bei sicherheitsrelevanten
|
|
// Meldungen wie einem fehlgeschlagenen oder erfolgreichen Firmware-
|
|
// Update, die der Nutzer auf keinen Fall verpassen darf. Gleicher
|
|
// Kasten-/Scroll-Aufbau wie confirmWarningScreen(), nur mit einem
|
|
// einzigen, ueber die volle Breite gehenden Button statt OK/Zurueck.
|
|
void infoScreen(TFT_eSPI& tft, const String& title, const String& body, uint16_t accentColor, const String& buttonLabel) {
|
|
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;
|
|
|
|
// Titel-Text vorab in so viele Zeilen umbrechen, wie bei Größe 2
|
|
// (oder bei zu langem Text Größe 1) nötig sind - siehe
|
|
// wrapTitleLines() oben. VIEW_TOP hängt dadurch von der
|
|
// tatsächlichen Zeilenzahl des Titels ab, ist also kein constexpr
|
|
// mehr wie vorher (wo immer nur eine Titelzeile angenommen wurde).
|
|
tft.setTextSize(2);
|
|
uint8_t titleTextSize = 2;
|
|
if (tft.textWidth(title) > TEXT_MAX_WIDTH) {
|
|
tft.setTextSize(1);
|
|
titleTextSize = 1;
|
|
}
|
|
constexpr int MAX_TITLE_LINES = 3;
|
|
String titleLines[MAX_TITLE_LINES];
|
|
int titleLineCount = wrapTitleLines(tft, title, TEXT_MAX_WIDTH, titleLines, MAX_TITLE_LINES);
|
|
// Fließtext wird immer bei Größe 1 vermessen/gezeichnet (siehe
|
|
// layoutWrapped()-Aufrufe unten) - Größe hier zurücksetzen, falls
|
|
// obiger Titel-Breitentest sie auf 2 stehen gelassen hat, sonst
|
|
// würde die gleich folgende totalH-Berechnung (vor dem ersten
|
|
// redraw()) mit falscher (zu breiter) Schriftgröße rechnen.
|
|
tft.setTextSize(1);
|
|
// Eine Leerzeile Abstand zwischen Titel und Fließtext.
|
|
int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12);
|
|
|
|
constexpr int16_t BTN_H = 40;
|
|
constexpr int16_t BOTTOM_MARGIN = 10;
|
|
constexpr int16_t BTN_Y = BOX_Y + BOX_H - BOTTOM_MARGIN - BTN_H;
|
|
constexpr int16_t SCROLL_ROW_H = 28;
|
|
constexpr int16_t SCROLL_ROW_GAP = 8;
|
|
|
|
constexpr int16_t VIEW_BOTTOM_NO_SCROLL = BTN_Y - 8;
|
|
constexpr int16_t VIEW_BOTTOM_SCROLL = VIEW_BOTTOM_NO_SCROLL - SCROLL_ROW_H - SCROLL_ROW_GAP;
|
|
|
|
int16_t totalH = layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, 0, 0, 0, false);
|
|
|
|
bool scrollable = (totalH - VIEW_BOTTOM_NO_SCROLL) > 0;
|
|
int16_t viewBottom = scrollable ? VIEW_BOTTOM_SCROLL : VIEW_BOTTOM_NO_SCROLL;
|
|
int16_t maxScroll = totalH - viewBottom;
|
|
if (maxScroll < 0) maxScroll = 0;
|
|
int16_t scrollY = 0;
|
|
constexpr int16_t SCROLL_STEP = 48;
|
|
|
|
Rect okBtn = {(int16_t)(BOX_X + 10), BTN_Y, (int16_t)(BOX_W - 20), BTN_H};
|
|
int16_t scrollRowY = VIEW_BOTTOM_SCROLL + SCROLL_ROW_GAP;
|
|
Rect upBtn = {(int16_t)(BOX_X + BOX_W / 2 - 64), scrollRowY, 60, SCROLL_ROW_H};
|
|
Rect downBtn = {(int16_t)(BOX_X + BOX_W / 2 + 4), scrollRowY, 60, SCROLL_ROW_H};
|
|
|
|
MenuStars::reset();
|
|
|
|
auto redraw = [&]() {
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.drawRoundRect(BOX_X, BOX_Y, BOX_W, BOX_H, 6, accentColor);
|
|
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(accentColor, TFT_BLACK);
|
|
tft.setTextSize(titleTextSize);
|
|
for (int i = 0; i < titleLineCount; i++) {
|
|
tft.drawString(titleLines[i], BOX_X + BOX_W / 2, (int16_t)(TITLE_Y + i * LINE_H));
|
|
}
|
|
tft.setTextSize(1);
|
|
tft.setTextDatum(TL_DATUM);
|
|
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
layoutWrapped(tft, BOX_X + 10, VIEW_TOP, TEXT_MAX_WIDTH, LINE_H, body, scrollY, VIEW_TOP, viewBottom, true);
|
|
|
|
drawButton(tft, okBtn, buttonLabel);
|
|
if (scrollable) {
|
|
drawButton(tft, upBtn, "^");
|
|
drawButton(tft, downBtn, "v");
|
|
}
|
|
};
|
|
|
|
redraw();
|
|
|
|
while (true) {
|
|
TouchInput::Point tap;
|
|
if (TouchInput::wasTapped(tap)) {
|
|
if (okBtn.contains(tap.x, tap.y)) return;
|
|
if (scrollable && upBtn.contains(tap.x, tap.y) && scrollY > 0) {
|
|
scrollY -= SCROLL_STEP;
|
|
if (scrollY < 0) scrollY = 0;
|
|
redraw();
|
|
} else if (scrollable && downBtn.contains(tap.x, tap.y) && scrollY < maxScroll) {
|
|
scrollY += SCROLL_STEP;
|
|
if (scrollY > maxScroll) scrollY = maxScroll;
|
|
redraw();
|
|
}
|
|
}
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
}
|
|
|
|
// Fortschrittsanzeige waehrend OtaUpdate::performUpdate() laeuft -
|
|
// gleiches Namespace-globale-Zeiger-Prinzip wie progressTft oben (siehe
|
|
// Settings-Backup-Fortschrittspunkte), da OtaUpdate::performUpdate()
|
|
// ebenfalls einen einfachen C-Funktionszeiger erwartet, keine
|
|
// Lambda-Capture erlaubt.
|
|
TFT_eSPI* otaProgressTft = nullptr;
|
|
|
|
void drawOtaProgress(uint8_t percent) {
|
|
if (!otaProgressTft) return;
|
|
TFT_eSPI& t = *otaProgressTft;
|
|
// Zwei Zeilen statt einer langen: der Praefix-Text
|
|
// (OTA_INSTALLING_PREFIX) ist in manchen Sprachen zu lang, um
|
|
// zusammen mit der Prozentzahl auf einer Zeile bei lesbarer
|
|
// Schriftgroesse zu passen (lief vorher links/rechts ueber den
|
|
// Bildschirmrand hinaus). Jetzt: Beschriftung klein oben, Prozent
|
|
// gross darunter.
|
|
constexpr int16_t BAND_H = 60;
|
|
int16_t cy = Config::SCREEN_HEIGHT / 2;
|
|
t.fillRect(0, (int16_t)(cy - BAND_H / 2), Config::SCREEN_WIDTH, BAND_H, TFT_BLACK);
|
|
|
|
t.setTextDatum(MC_DATUM);
|
|
t.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
t.setTextSize(1);
|
|
t.drawString(I18n::t(StringId::OTA_INSTALLING_PREFIX), Config::SCREEN_WIDTH / 2, (int16_t)(cy - 14));
|
|
|
|
String percentLabel = String(percent) + "%";
|
|
t.setTextSize(3);
|
|
t.drawString(percentLabel, Config::SCREEN_WIDTH / 2, (int16_t)(cy + 12));
|
|
|
|
t.setTextSize(1);
|
|
t.setTextDatum(TL_DATUM);
|
|
}
|
|
|
|
// Kompletter Ablauf fuer "Nach Update suchen" (System-Menue) - Pruefung
|
|
// gegen GitHub-Releases, bei verfuegbarem Update explizite Bestaetigung
|
|
// (confirmWarningScreen() mit gruenem statt rotem Akzent - ein Update
|
|
// ist keine destruktive Aktion wie Werksreset, verdient aber trotzdem
|
|
// eine bewusste Bestaetigung, da WLAN/Strom waehrend des Vorgangs nicht
|
|
// unterbrochen werden sollten), danach Fortschrittsanzeige waehrend
|
|
// Download+Flash. WICHTIG: startet NICHT mehr automatisch neu und
|
|
// springt bei einem Fehler auch nicht einfach stillschweigend zurueck
|
|
// ins Menue - jedes Ergebnis (Erfolg wie Fehler) wird ueber infoScreen()
|
|
// als eigener, stehenbleibender Screen angezeigt, den der Nutzer aktiv
|
|
// bestaetigen muss. Bei Erfolg startet erst ein expliziter Tap auf
|
|
// "Jetzt neu starten" tatsaechlich neu.
|
|
void runOtaUpdateScreen(TFT_eSPI& tft) {
|
|
MenuStars::reset();
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.drawString(I18n::t(StringId::OTA_CHECKING), Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2);
|
|
tft.setTextDatum(TL_DATUM);
|
|
|
|
NetTask::pause();
|
|
OtaUpdate::CheckInfo info = OtaUpdate::checkForUpdate();
|
|
NetTask::resume();
|
|
|
|
if (info.result == OtaUpdate::CheckResult::Error) {
|
|
infoScreen(tft, I18n::t(StringId::OTA_CHECK_FAILED), "", TFT_RED, I18n::t(StringId::OK));
|
|
return;
|
|
}
|
|
if (info.result == OtaUpdate::CheckResult::UpToDate) {
|
|
String upToDateTitle = String(I18n::t(StringId::OTA_UP_TO_DATE_PREFIX)) + info.latestVersion;
|
|
infoScreen(tft, upToDateTitle, "", TFT_GREEN, I18n::t(StringId::OK));
|
|
return;
|
|
}
|
|
|
|
String title = String(I18n::t(StringId::OTA_UPDATE_AVAILABLE_PREFIX)) + info.latestVersion;
|
|
bool confirmed = confirmWarningScreen(tft, title, I18n::t(StringId::OTA_CONFIRM_BODY), TFT_GREEN);
|
|
if (!confirmed) return;
|
|
|
|
otaProgressTft = &tft;
|
|
tft.fillScreen(TFT_BLACK);
|
|
drawOtaProgress(0);
|
|
NetTask::pause();
|
|
bool ok = OtaUpdate::performUpdate(info.downloadUrl, drawOtaProgress);
|
|
NetTask::resume();
|
|
otaProgressTft = nullptr;
|
|
|
|
if (ok) {
|
|
// Bewusst KEIN automatischer Neustart mehr - der Nutzer
|
|
// bestaetigt aktiv per Button, damit er den Erfolg auch wirklich
|
|
// mitbekommt (vorher lief die Meldung nur 1,5s an, dann
|
|
// Neustart - leicht zu verpassen).
|
|
infoScreen(tft, I18n::t(StringId::OTA_UPDATE_SUCCESS), I18n::t(StringId::OTA_SUCCESS_BODY),
|
|
TFT_GREEN, I18n::t(StringId::OTA_RESTART_BUTTON));
|
|
ESP.restart();
|
|
} else {
|
|
// Bewusst ein stehenbleibender Info-Screen statt der alten
|
|
// showBriefMessage() (1,2s, dann automatisch zurueck ins Menue)
|
|
// - ein fehlgeschlagenes Firmware-Update ist keine
|
|
// Nebensaechlichkeit, die man verpassen darf.
|
|
infoScreen(tft, I18n::t(StringId::OTA_UPDATE_FAILED), I18n::t(StringId::OTA_FAILED_BODY),
|
|
TFT_RED, I18n::t(StringId::OK));
|
|
}
|
|
}
|
|
|
|
enum class Page { Main, Region, System, Flight, BackupReset };
|
|
}
|
|
|
|
void run(TFT_eSPI& tft) {
|
|
Page page = Page::Main;
|
|
bool done = false;
|
|
MenuStars::reset();
|
|
|
|
while (!done) {
|
|
tft.fillScreen(TFT_BLACK);
|
|
|
|
if (page == Page::Main) {
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.println(I18n::t(StringId::MENU_SETTINGS));
|
|
|
|
Rect regionBtn = catRowRect(0);
|
|
Rect wifiBtn = catRowRect(1);
|
|
Rect systemBtn = catRowRect(2);
|
|
Rect flightBtn = catRowRect(3);
|
|
Rect backBtn = catRowRect(4);
|
|
|
|
drawButton(tft, regionBtn, I18n::t(StringId::MENU_CATEGORY_REGION));
|
|
drawButton(tft, wifiBtn, I18n::t(StringId::MENU_CATEGORY_WIFI));
|
|
drawButton(tft, systemBtn, I18n::t(StringId::MENU_CATEGORY_SYSTEM));
|
|
drawButton(tft, flightBtn, I18n::t(StringId::MENU_CATEGORY_FLIGHT));
|
|
drawButton(tft, backBtn, I18n::t(StringId::BACK));
|
|
|
|
TouchInput::Point tap;
|
|
while (true) {
|
|
if (TouchInput::wasTapped(tap)) break;
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
|
|
if (regionBtn.contains(tap.x, tap.y)) {
|
|
page = Page::Region;
|
|
} else if (wifiBtn.contains(tap.x, tap.y)) {
|
|
WifiManageScreen::run(tft);
|
|
} else if (systemBtn.contains(tap.x, tap.y)) {
|
|
page = Page::System;
|
|
} else if (flightBtn.contains(tap.x, tap.y)) {
|
|
page = Page::Flight;
|
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
|
done = true;
|
|
}
|
|
|
|
} else if (page == Page::Region) {
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.println(I18n::t(StringId::MENU_CATEGORY_REGION));
|
|
|
|
Rect languageBtn = rowRect(0);
|
|
Rect unitsBtn = rowRect(1);
|
|
Rect backBtn = rowRect(2);
|
|
|
|
drawButton(tft, languageBtn, String(I18n::t(StringId::MENU_LANGUAGE)) + ": " + I18n::languageName(SettingsStore::language()));
|
|
drawButton(tft, unitsBtn, I18n::t(StringId::MENU_UNITS));
|
|
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
|
|
|
TouchInput::Point tap;
|
|
while (true) {
|
|
if (TouchInput::wasTapped(tap)) break;
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
|
|
if (languageBtn.contains(tap.x, tap.y)) {
|
|
LanguageScreen::run(tft);
|
|
} else if (unitsBtn.contains(tap.x, tap.y)) {
|
|
UnitsScreen::run(tft);
|
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
|
page = Page::Main;
|
|
}
|
|
|
|
} else if (page == Page::System) {
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.println(I18n::t(StringId::MENU_CATEGORY_SYSTEM));
|
|
|
|
Rect calibBtn = systemRowRect(0);
|
|
Rect invertBtn = systemRowRect(1);
|
|
Rect brightnessBtn = systemRowRect(2);
|
|
Rect timeoutBtn = systemRowRect(3);
|
|
Rect nightDimBtn = systemRowRect(4);
|
|
Rect webuiBtn = systemRowRect(6);
|
|
// "Sicherung & Reset" (Backup/Restore/Werksreset, siehe
|
|
// Page::BackupReset unten) und "Nach Update suchen" stehen
|
|
// bewusst direkt ueber "Info", das seinerseits als letzter
|
|
// Punkt direkt ueber dem Zurueck-Button steht - so bleiben
|
|
// beide Positionen stabil, egal wie viele weitere Punkte davor
|
|
// noch dazukommen.
|
|
Rect ecoModeBtn = systemRowRect(5);
|
|
Rect backupResetBtn = systemRowRect(7);
|
|
Rect checkUpdateBtn = systemRowRect(8);
|
|
Rect aboutBtn = systemRowRect(9);
|
|
Rect backBtn = systemRowRect(10);
|
|
|
|
drawButton(tft, calibBtn, I18n::t(StringId::MENU_CALIBRATE));
|
|
|
|
String invertLabel = SettingsStore::displayInverted()
|
|
? I18n::t(StringId::MENU_DISPLAY_INVERTED)
|
|
: I18n::t(StringId::MENU_DISPLAY_NORMAL);
|
|
drawButton(tft, invertBtn, invertLabel);
|
|
|
|
drawButton(tft, brightnessBtn, brightnessLabel(SettingsStore::brightnessPercent()));
|
|
drawButton(tft, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes()));
|
|
drawButton(tft, nightDimBtn, I18n::t(StringId::MENU_NIGHT_DIMMING) + onOff(SettingsStore::nightDimmingEnabled()));
|
|
drawButton(tft, webuiBtn, I18n::t(StringId::MENU_LOGBOOK_WEBUI));
|
|
drawButton(tft, backupResetBtn, I18n::t(StringId::MENU_BACKUP_RESET));
|
|
drawButton(tft, checkUpdateBtn, I18n::t(StringId::MENU_CHECK_UPDATE));
|
|
drawButton(tft, aboutBtn, I18n::t(StringId::MENU_ABOUT));
|
|
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
|
|
|
TouchInput::Point tap;
|
|
while (true) {
|
|
if (TouchInput::wasTapped(tap)) break;
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
|
|
if (calibBtn.contains(tap.x, tap.y)) {
|
|
CalibrationScreen::run(tft);
|
|
} else if (invertBtn.contains(tap.x, tap.y)) {
|
|
bool newState = !SettingsStore::displayInverted();
|
|
SettingsStore::setDisplayInverted(newState);
|
|
tft.invertDisplay(newState);
|
|
} else if (brightnessBtn.contains(tap.x, tap.y)) {
|
|
BrightnessScreen::run(tft);
|
|
} else if (timeoutBtn.contains(tap.x, tap.y)) {
|
|
// Vorher: Durchklicken per wiederholtem Antippen (0-10, ein
|
|
// Tipp pro Minute - bei z.B. 10 Minuten also zehn Tipps).
|
|
// Jetzt: eigener Screen mit Schieberegler, siehe
|
|
// timeout_screen.cpp - dort lebt jetzt auch der
|
|
// Ruhebildschirm-Umschalter (inhaltlich eng verwandt, und
|
|
// dort ist Platz fuer eine kurze Erklaerung).
|
|
TimeoutScreen::run(tft);
|
|
} else if (nightDimBtn.contains(tap.x, tap.y)) {
|
|
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
|
|
} else if (ecoModeBtn.contains(tap.x, tap.y)) {
|
|
SettingsStore::setEcoModeEnabled(!SettingsStore::ecoModeEnabled());
|
|
} else if (webuiBtn.contains(tap.x, tap.y)) {
|
|
WebUiScreen::run(tft);
|
|
} else if (backupResetBtn.contains(tap.x, tap.y)) {
|
|
page = Page::BackupReset;
|
|
} else if (checkUpdateBtn.contains(tap.x, tap.y)) {
|
|
runOtaUpdateScreen(tft);
|
|
} else if (aboutBtn.contains(tap.x, tap.y)) {
|
|
AboutScreen::run(tft);
|
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
|
page = Page::Main;
|
|
}
|
|
|
|
} else if (page == Page::BackupReset) {
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.println(I18n::t(StringId::MENU_BACKUP_RESET));
|
|
|
|
Rect backupBtn = backupResetRowRect(0);
|
|
Rect restoreBtn = backupResetRowRect(1);
|
|
Rect resetBtn = backupResetRowRect(2);
|
|
Rect backBtn = backupResetRowRect(3);
|
|
|
|
drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP));
|
|
drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE));
|
|
// Danger-Akzent (rot) - deutlich von Sichern/Wiederherstellen
|
|
// abgesetzt, da diese Aktion (nach Bestaetigung) ALLE Daten
|
|
// unwiderruflich loescht, siehe confirmWarningScreen() unten.
|
|
drawButton(tft, resetBtn, I18n::t(StringId::MENU_FACTORY_RESET), false, true);
|
|
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
|
|
|
|
TouchInput::Point tap;
|
|
while (true) {
|
|
if (TouchInput::wasTapped(tap)) break;
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
|
|
if (backupBtn.contains(tap.x, tap.y)) {
|
|
progressTft = &tft;
|
|
progressBtnRect = backupBtn;
|
|
progressLabel = I18n::t(StringId::MENU_BACKUP);
|
|
progressDots = 0;
|
|
bool ok = SettingsBackup::backup(drawProgressStep);
|
|
progressTft = nullptr;
|
|
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_BACKUP_SAVED : StringId::MENU_BACKUP_FAILED),
|
|
ok ? TFT_GREEN : TFT_RED);
|
|
} else if (restoreBtn.contains(tap.x, tap.y)) {
|
|
if (SettingsBackup::hasBackup()) {
|
|
progressTft = &tft;
|
|
progressBtnRect = restoreBtn;
|
|
progressLabel = I18n::t(StringId::MENU_RESTORE);
|
|
progressDots = 0;
|
|
bool ok = SettingsBackup::restore(drawProgressStep);
|
|
progressTft = nullptr;
|
|
showBriefMessage(tft, I18n::t(ok ? StringId::MENU_RESTORED : StringId::MENU_RESTORE_FAILED),
|
|
ok ? TFT_GREEN : TFT_RED);
|
|
}
|
|
} else if (resetBtn.contains(tap.x, tap.y)) {
|
|
if (confirmWarningScreen(tft, I18n::t(StringId::MENU_LOGBOOK_WARNING_TITLE),
|
|
I18n::t(StringId::MENU_FACTORY_RESET_WARNING_BODY))) {
|
|
tft.fillScreen(TFT_BLACK);
|
|
tft.setTextDatum(MC_DATUM);
|
|
tft.setTextColor(TFT_RED, TFT_BLACK);
|
|
tft.drawString(I18n::t(StringId::MENU_FACTORY_RESET_DELETING),
|
|
Config::SCREEN_WIDTH / 2, Config::SCREEN_HEIGHT / 2);
|
|
tft.setTextDatum(TL_DATUM);
|
|
// Erfolgsfall: factoryReset() startet das Geraet neu und
|
|
// kehrt nie zurueck - dieser Code danach laeuft nur im
|
|
// (seltenen) Fehlerfall (SD nicht eingehaengt) ueberhaupt
|
|
// weiter.
|
|
bool ok = SettingsBackup::factoryReset();
|
|
if (!ok) {
|
|
showBriefMessage(tft, I18n::t(StringId::MENU_FACTORY_RESET_FAILED), TFT_RED);
|
|
}
|
|
}
|
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
|
page = Page::System;
|
|
}
|
|
|
|
} else { // Page::Flight
|
|
tft.setTextColor(TFT_GREEN, TFT_BLACK);
|
|
tft.setCursor(10, 14);
|
|
tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT));
|
|
|
|
// Reine An/Aus-Schalter (Heartbeat, Notfall-Alarm, Naeherungs-LED,
|
|
// Flugbuch, Beobachtungsalarm, Bodenfahrzeuge-Filter) stehen
|
|
// bewusst gemeinsam am Ende der Liste, direkt ueber dem
|
|
// Zurueck-Button - zuerst die LED-gesteuerten Alarme (Heartbeat,
|
|
// Notfall, Naeherung; siehe LedAlert::Mode) zusammenhaengend,
|
|
// danach die beiden nicht-LED-basierten Schalter Flugbuch und
|
|
// Beobachtungsalarm sowie zuletzt der Bodenfahrzeuge-Filter.
|
|
Rect aircraftListBtn = flightRowRect(0);
|
|
Rect statsBtn = flightRowRect(1);
|
|
Rect statsHistoryBtn = flightRowRect(2);
|
|
Rect logFilesBtn = flightRowRect(3);
|
|
Rect locationBtn = flightRowRect(4);
|
|
Rect airlineBtn = flightRowRect(5);
|
|
Rect watchlistBtn = flightRowRect(6);
|
|
Rect routeFilterBtn = flightRowRect(7);
|
|
Rect heartbeatBtn = flightRowRect(8);
|
|
Rect emergencyBtn = flightRowRect(9);
|
|
Rect proximityBtn = flightRowRect(10);
|
|
Rect logbookBtn = flightRowRect(11);
|
|
Rect watchlistAlertBtn = flightRowRect(12);
|
|
Rect groundBtn = flightRowRect(13);
|
|
Rect backBtn = flightRowRect(14);
|
|
|
|
drawButton(tft, aircraftListBtn, I18n::t(StringId::MENU_AIRCRAFT_LIST));
|
|
drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS));
|
|
drawButton(tft, statsHistoryBtn, I18n::t(StringId::MENU_STATS_HISTORY));
|
|
drawButton(tft, logFilesBtn, I18n::t(StringId::MENU_LOGBOOK_FILES));
|
|
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, routeFilterBtn, I18n::t(StringId::MENU_ROUTE_FILTER));
|
|
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, logbookBtn, I18n::t(StringId::MENU_FLIGHT_LOGBOOK) + onOff(SettingsStore::flightLogbookEnabled()));
|
|
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));
|
|
|
|
TouchInput::Point tap;
|
|
while (true) {
|
|
if (TouchInput::wasTapped(tap)) break;
|
|
MenuStars::update(tft);
|
|
delay(20);
|
|
}
|
|
|
|
if (aircraftListBtn.contains(tap.x, tap.y)) {
|
|
if (AircraftListScreen::run(tft)) {
|
|
// Ein Flugzeug wurde in der Liste ausgewaehlt - direkt bis
|
|
// zum Radar zurueckspringen (mit offenem Detail-Panel),
|
|
// statt in der Flugoptionen-Seite stehen zu bleiben.
|
|
done = true;
|
|
}
|
|
} else if (statsBtn.contains(tap.x, tap.y)) {
|
|
StatsScreen::run(tft);
|
|
} else if (statsHistoryBtn.contains(tap.x, tap.y)) {
|
|
StatsHistoryScreen::run(tft);
|
|
} else if (logFilesBtn.contains(tap.x, tap.y)) {
|
|
LogbookFilesScreen::run(tft);
|
|
} 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 (routeFilterBtn.contains(tap.x, tap.y)) {
|
|
RouteFilterScreen::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 (logbookBtn.contains(tap.x, tap.y)) {
|
|
if (SettingsStore::flightLogbookEnabled()) {
|
|
// Ausschalten ist immer unbedenklich - keine Bestaetigung noetig.
|
|
SettingsStore::setFlightLogbookEnabled(false);
|
|
SettingsStore::setFlightLogbookEnabledAtEpoch(0);
|
|
SettingsStore::setFlightLogbookSessionFile("");
|
|
} else if (confirmWarningScreen(tft, I18n::t(StringId::MENU_LOGBOOK_WARNING_TITLE),
|
|
I18n::t(StringId::MENU_LOGBOOK_WARNING_BODY))) {
|
|
SettingsStore::setFlightLogbookEnabled(true);
|
|
SettingsStore::setFlightLogbookEnabledAtEpoch((uint32_t)time(nullptr));
|
|
// Leerer Eintrag erzwingt eine frische Sitzungsdatei beim
|
|
// naechsten FlightLogbook::update() statt eine evtl. noch
|
|
// vorhandene alte Datei weiterzuschreiben.
|
|
SettingsStore::setFlightLogbookSessionFile("");
|
|
}
|
|
} else if (watchlistAlertBtn.contains(tap.x, tap.y)) {
|
|
SettingsStore::setWatchlistAlertEnabled(!SettingsStore::watchlistAlertEnabled());
|
|
} else if (groundBtn.contains(tap.x, tap.y)) {
|
|
SettingsStore::setHideGroundVehicles(!SettingsStore::hideGroundVehicles());
|
|
} else if (backBtn.contains(tap.x, tap.y)) {
|
|
page = Page::Main;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|