localization

This commit is contained in:
2026-08-16 14:33:27 +02:00
parent 9c634b819e
commit fb09b2b7a1
65 changed files with 1621 additions and 1679 deletions

View File

@@ -35,18 +35,17 @@ namespace {
}
};
// ROW_GAP/ROW_START_Y bleiben unveraendert (werden von flightRowRect()
// weiter unten mitbenutzt, siehe FLIGHT_ROW_H).
// ROW_GAP/ROW_START_Y remain unchanged (also used by flightRowRect()
// below, see 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.
// Region sub-page (Language/Units/Back, only 3 entries): row
// height/spacing calculated from actual available screen area (same
// pattern as SYSTEM_ROW_H below), instead of the small fixed height
// (formerly 22px) intended for full pages (e.g. Flight Options: 14
// entries) - that left nearly the whole screen empty with only 3
// entries and made buttons tiny and hard to hit.
constexpr uint8_t REGION_ROW_COUNT = 3;
constexpr int16_t REGION_ROW_GAP = 10;
constexpr int16_t REGION_END_Y = Config::SCREEN_HEIGHT - 10;
@@ -67,33 +66,31 @@ namespace {
(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.
// Custom, slightly more compact row height ONLY for the Flight Options
// page: the normal rowRect() height (22px) already left no room for
// another entry with 13 entries - other pages (Region/System) keep
// their normal height unchanged to avoid shifting anything there.
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
// Custom, larger calculated row height ONLY for the System page: with
// only 10 entries (vs. e.g. 14 on the Flight Options page), the
// normal rowRect() height (22px) left a lot of unused space at the
// bottom. Height is instead calculated from actual available screen
// area so buttons fill the space down to just above the screen
// edge - other pages keep their own unchanged row heights.
// Was 10 before "Backup"/"Restore" were moved to the new
// "Backup & Reset" sub-page (see BACKUP_RESET_ROW_COUNT below) and
// replaced there by a single folder button: -2 (Backup/Restore
// removed) +1 (new folder button) = 9. Then +1 for the new
// "Check for update" item (OTA update, see ota_update.h) = 10. The
// screensaver toggle, which temporarily had its own row here, moved
// to the new screen timeout screen (see timeout_screen.cpp) - row
// count therefore stays at 10.
constexpr uint8_t SYSTEM_ROW_COUNT = 11; // +1 for 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;
@@ -104,11 +101,11 @@ namespace {
(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.
// Custom row height for the new "Backup & Reset" sub-page (4
// entries: Backup/Restore/Factory Reset/Back) - same "calculate from
// available space" pattern as SYSTEM_ROW_H.
// ROW_START_Y is reused (see above, shared with rowRect()/
// flightRowRect()), only GAP/COUNT/H are custom values.
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;
@@ -134,14 +131,15 @@ namespace {
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).
// Progress dots display while SettingsBackup::backup()/restore()
// run (see calls below in Page::BackupReset) - these are
// synchronous, SD-intensive operations that can take noticeable
// time and previously made the button appear frozen.
// SettingsBackup calls the function pointer passed here before each
// of the two copy operations (first settings, then WiFi).
// Namespace-global pointers/variables instead of lambda capture,
// because a plain C function pointer must be passed (no
// std::function in the project).
TFT_eSPI* progressTft = nullptr;
Rect progressBtnRect;
String progressLabel;
@@ -174,11 +172,11 @@ namespace {
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).
// Like the simple layoutWrapped() variant, but with optional scroll
// offset and viewport (scrollY/viewTop/viewBottom) - lines outside
// the viewport are skipped. With draw=false, only the total height
// is calculated without drawing anything (for pre-checking scroll
// need).
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) {
@@ -209,10 +207,10 @@ namespace {
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.
// Splits a title into up to maxLines lines using the same word-wrapping
// principle as layoutWrapped() - but a title is never scrolled and must
// always be fully visible, hence a separate, simpler variant without
// scroll support.
int wrapTitleLines(TFT_eSPI& tft, const String& text, int16_t maxWidth, String* outLines, int maxLines) {
int count = 0;
int32_t start = 0;
@@ -240,29 +238,27 @@ namespace {
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".
// Warning overlay that covers nearly the entire screen (just a few
// pixels margin) - originally built only for enabling the flight
// logbook (explains why it auto-disables after 24h), now generic
// with passed-in title/text to also be reusable for the "Reset
// settings" confirmation (factory reset) - both are rare, potentially
// consequential actions that deserve the same clear warning.
// "Attention!!!" (titleId) appears at the top, with a blank line
// before the body text (bodyId) below; the text scrolls if needed
// (longer translations) via dedicated arrow buttons, OK/Back always
// stay fixed at the bottom and collision-free visible. Stars run in
// the background, as on all other menu screens (only the radar
// screen skips them due to CPU load from polling/drawing). Returns
// true if "OK" was tapped, false for "Back".
//
// 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.
// title/body are fixed BEFORE the call via I18n::t() (instead of
// accepting StringIds), so dynamically composed texts (e.g. with
// inserted version number for OTA) are possible. accentColor
// (default TFT_RED for the two existing, truly destructive callers)
// colors frame and title - the OTA caller passes TFT_GREEN, since an
// update should be confirmed but is not a "dangerous" deletion
// action like factory reset/flight logbook.
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;
@@ -272,11 +268,11 @@ namespace {
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).
// Wrap title text into as many lines as needed at size 2 (or size 1
// if text is too long) - see wrapTitleLines() above. VIEW_TOP
// therefore depends on the actual number of title lines, so it is
// no longer constexpr as before (where only one title line was
// assumed).
tft.setTextSize(2);
uint8_t titleTextSize = 2;
if (tft.textWidth(title) > TEXT_MAX_WIDTH) {
@@ -286,13 +282,13 @@ namespace {
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.
// Body text is always measured/drawn at size 1 (see
// layoutWrapped() calls below) - reset size here in case the
// title width check above left it at 2, otherwise the
// immediately following totalH calculation (before the first
// redraw()) would use the wrong (too wide) font size.
tft.setTextSize(1);
// Eine Leerzeile Abstand zwischen Titel und Fließtext.
// One blank line between title and body text.
int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12);
constexpr int16_t BTN_H = 36;
@@ -303,9 +299,9 @@ namespace {
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).
// First check available text height without scroll arrows - only if
// the text does not fit, additional space for arrows is reserved
// (more text space for short translations).
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;
@@ -332,13 +328,12 @@ namespace {
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.
// Static titles ("Attention!!!") are short enough for size 2, but
// the OTA caller builds the title dynamically with a version
// number (e.g. "Update available: v3.5.0") - this no longer
// fits in the box at size 2 and previously overflowed left/right
// beyond the screen edge. Hence check width at size 2 and fall
// back to size 1 if needed, instead of assuming a fixed size.
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));
@@ -379,16 +374,15 @@ namespace {
}
}
// 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.
// Simple info screen with only ONE button (no cancel) - for
// terminal states where there is nothing left to decide, only to
// acknowledge (e.g. result of an OTA update). Unlike
// showBriefMessage() (short message at the bottom of the screen,
// auto-disappears after 1.2s), this screen stays until actively
// acknowledged - important for safety-relevant messages like a
// failed or successful firmware update that the user must not miss.
// Same box/scroll layout as confirmWarningScreen(), but with a
// single full-width button instead of OK/Back.
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;
@@ -398,11 +392,11 @@ namespace {
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).
// Wrap title text into as many lines as needed at size 2 (or size 1
// if text is too long) - see wrapTitleLines() above. VIEW_TOP
// therefore depends on the actual number of title lines, so it is
// no longer constexpr as before (where only one title line was
// assumed).
tft.setTextSize(2);
uint8_t titleTextSize = 2;
if (tft.textWidth(title) > TEXT_MAX_WIDTH) {
@@ -412,13 +406,13 @@ namespace {
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.
// Body text is always measured/drawn at size 1 (see
// layoutWrapped() calls below) - reset size here in case the
// title width check above left it at 2, otherwise the
// immediately following totalH calculation (before the first
// redraw()) would use the wrong (too wide) font size.
tft.setTextSize(1);
// Eine Leerzeile Abstand zwischen Titel und Fließtext.
// One blank line between title and body text.
int16_t VIEW_TOP = (int16_t)(TITLE_Y + titleLineCount * LINE_H + 12);
constexpr int16_t BTN_H = 40;
@@ -490,22 +484,21 @@ namespace {
}
}
// 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.
// Progress display while OtaUpdate::performUpdate() runs - same
// namespace-global pointer principle as progressTft above (see
// Settings-Backup progress dots), since OtaUpdate::performUpdate()
// also expects a plain C function pointer, no lambda capture
// allowed.
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.
// Two lines instead of one long one: the prefix text
// (OTA_INSTALLING_PREFIX) is too long in some languages to fit
// together with the percentage on one line at a readable font size
// (previously overflowed left/right beyond the screen edge).
// Now: label small at top, percentage large below.
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);
@@ -523,18 +516,17 @@ namespace {
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.
// Complete flow for "Check for update" (System menu) - checks
// against GitHub releases, explicit confirmation when update is
// available (confirmWarningScreen() with green instead of red
// accent - an update is not a destructive action like factory reset,
// but still deserves conscious confirmation since WiFi/power should
// not be interrupted during the process), then progress display
// during download+flash. IMPORTANT: no longer auto-restarts and no
// longer silently jumps back to the menu on error - every result
// (success as well as failure) is shown via infoScreen() as its own
// persistent screen that the user must actively acknowledge. On
// success, only an explicit tap on "Restart now" actually restarts.
void runOtaUpdateScreen(TFT_eSPI& tft) {
MenuStars::reset();
tft.fillScreen(TFT_BLACK);
@@ -570,18 +562,18 @@ namespace {
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).
// Deliberately NO automatic restart anymore - the user
// actively confirms via button so they actually notice the
// success (previously the message showed for only 1.5s,
// then restart - easy to miss).
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.
// Deliberately a persistent info screen instead of the old
// showBriefMessage() (1.2s, then automatically back to menu)
// - a failed firmware update is not a minor detail that one
// can afford to miss.
infoScreen(tft, I18n::t(StringId::OTA_UPDATE_FAILED), I18n::t(StringId::OTA_FAILED_BODY),
TFT_RED, I18n::t(StringId::OK));
}
@@ -673,12 +665,12 @@ void run(TFT_eSPI& tft) {
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.
// "Backup & Reset" (Backup/Restore/Factory Reset, see
// Page::BackupReset below) and "Check for update" are
// deliberately placed directly above "About", which in turn
// is the last item directly above the Back button - this
// keeps both positions stable regardless of how many further
// items come before them.
Rect ecoModeBtn = systemRowRect(5);
Rect backupResetBtn = systemRowRect(7);
Rect checkUpdateBtn = systemRowRect(8);
@@ -717,12 +709,12 @@ void run(TFT_eSPI& tft) {
} 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).
// Previously: cycling through values by repeated tapping
// (0-10, one tap per minute - e.g. 10 taps for 10 minutes).
// Now: dedicated screen with a slider, see
// timeout_screen.cpp - the screensaver toggle now also
// lives there (closely related in content, and there is
// room for a brief explanation).
TimeoutScreen::run(tft);
} else if (nightDimBtn.contains(tap.x, tap.y)) {
SettingsStore::setNightDimmingEnabled(!SettingsStore::nightDimmingEnabled());
@@ -752,9 +744,9 @@ void run(TFT_eSPI& tft) {
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.
// Danger accent (red) - clearly distinguished from Backup/Restore
// since this action (after confirmation) irreversibly DELETES
// ALL data, see confirmWarningScreen() below.
drawButton(tft, resetBtn, I18n::t(StringId::MENU_FACTORY_RESET), false, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK_ARROW));
@@ -794,10 +786,9 @@ void run(TFT_eSPI& tft) {
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.
// On success: factoryReset() restarts the device and never
// returns - this code after it only continues in the
// (rare) error case (SD not mounted).
bool ok = SettingsBackup::factoryReset();
if (!ok) {
showBriefMessage(tft, I18n::t(StringId::MENU_FACTORY_RESET_FAILED), TFT_RED);
@@ -812,13 +803,13 @@ 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,
// 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.
// Pure on/off toggles (Heartbeat, Emergency Alert, Proximity LED,
// Flight Logbook, Watchlist Alert, Ground Vehicles Filter) are
// deliberately grouped together at the end of the list,
// directly above the Back button - first the LED-driven alerts
// (Heartbeat, Emergency, Proximity; see LedAlert::Mode)
// together, then the two non-LED-based toggles Flight Logbook
// and Watchlist Alert, and finally the Ground Vehicles Filter.
Rect aircraftListBtn = flightRowRect(0);
Rect statsBtn = flightRowRect(1);
Rect statsHistoryBtn = flightRowRect(2);
@@ -860,9 +851,9 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
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.
// An aircraft was selected in the list - jump directly back
// to the radar (with detail panel open), instead of
// staying on the Flight Options page.
done = true;
}
} else if (statsBtn.contains(tap.x, tap.y)) {
@@ -887,7 +878,7 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
SettingsStore::setProximityAlertEnabled(!SettingsStore::proximityAlertEnabled());
} else if (logbookBtn.contains(tap.x, tap.y)) {
if (SettingsStore::flightLogbookEnabled()) {
// Ausschalten ist immer unbedenklich - keine Bestaetigung noetig.
// Turning off is always harmless - no confirmation needed.
SettingsStore::setFlightLogbookEnabled(false);
SettingsStore::setFlightLogbookEnabledAtEpoch(0);
SettingsStore::setFlightLogbookSessionFile("");
@@ -895,9 +886,9 @@ drawButton(tft, watchlistBtn, I18n::t(StringId::MENU_WATCHLIST));
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.
// Empty entry forces a fresh session file on the next
// FlightLogbook::update() instead of continuing to
// write to a possibly existing old file.
SettingsStore::setFlightLogbookSessionFile("");
}
} else if (watchlistAlertBtn.contains(tap.x, tap.y)) {