#include "menu_screen.h" #include "touch_input.h" #include "calibration_screen.h" #include "wifi_manage_screen.h" #include "stats_screen.h" #include "logbook_files_screen.h" #include "location_presets_screen.h" #include "airline_filter_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" 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; } }; constexpr int16_t ROW_H = 24; constexpr int16_t ROW_GAP = 2; constexpr int16_t ROW_START_Y = 18; Rect rowRect(uint8_t index) { return {10, (int16_t)(ROW_START_Y + index * (ROW_H + ROW_GAP)), (int16_t)(Config::SCREEN_WIDTH - 20), 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}; } 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); } 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"; } 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); } enum class Page { Main, Region, System, Flight }; } 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 = rowRect(0); Rect invertBtn = rowRect(1); Rect timeoutBtn = rowRect(2); Rect backupBtn = rowRect(3); Rect restoreBtn = rowRect(4); Rect backBtn = rowRect(5); 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, timeoutBtn, screenTimeoutLabel(SettingsStore::screenTimeoutMinutes())); drawButton(tft, backupBtn, I18n::t(StringId::MENU_BACKUP)); drawButton(tft, restoreBtn, I18n::t(StringId::MENU_RESTORE)); 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 (timeoutBtn.contains(tap.x, tap.y)) { uint8_t current = SettingsStore::screenTimeoutMinutes(); uint8_t next = (current >= 10) ? 0 : (current + 1); SettingsStore::setScreenTimeoutMinutes(next); } else if (backupBtn.contains(tap.x, tap.y)) { bool ok = SettingsBackup::backup(); 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()) { bool ok = SettingsBackup::restore(); showBriefMessage(tft, I18n::t(ok ? StringId::MENU_RESTORED : StringId::MENU_RESTORE_FAILED), ok ? TFT_GREEN : TFT_RED); } } else if (backBtn.contains(tap.x, tap.y)) { page = Page::Main; } } else { // Page::Flight tft.setTextColor(TFT_GREEN, TFT_BLACK); tft.setCursor(10, 14); tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT)); Rect statsBtn = rowRect(0); Rect logFilesBtn = rowRect(1); Rect logbookBtn = rowRect(2); Rect heartbeatBtn = rowRect(3); Rect proximityBtn = rowRect(4); Rect emergencyBtn = rowRect(5); Rect locationBtn = rowRect(6); Rect airlineBtn = rowRect(7); Rect groundBtn = rowRect(8); Rect backBtn = rowRect(9); drawButton(tft, statsBtn, I18n::t(StringId::MENU_STATISTICS)); 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, 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 (statsBtn.contains(tap.x, tap.y)) { StatsScreen::run(tft); } else if (logFilesBtn.contains(tap.x, tap.y)) { LogbookFilesScreen::run(tft); } else if (logbookBtn.contains(tap.x, tap.y)) { SettingsStore::setFlightLogbookEnabled(!SettingsStore::flightLogbookEnabled()); } 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 (groundBtn.contains(tap.x, tap.y)) { SettingsStore::setHideGroundVehicles(!SettingsStore::hideGroundVehicles()); } else if (backBtn.contains(tap.x, tap.y)) { page = Page::Main; } } } } }