v2.3.1: Info button for location presets, fix menu titles clipped at top edge, scrollable info screen

This commit is contained in:
Eiswolf-BG
2026-08-05 15:27:29 +02:00
parent 59aa5af1b2
commit e37dd469af
17 changed files with 168 additions and 25 deletions

View File

@@ -63,7 +63,7 @@ namespace {
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::AIRLINE_ADD_TITLE));
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_BLACK);
@@ -131,7 +131,7 @@ void run(TFT_eSPI& tft) {
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 8);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::AIRLINE_FILTER_TITLE));
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 26);

View File

@@ -92,7 +92,7 @@ void run(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::CALIB_SAVED));
delay(800);
}

View File

@@ -87,6 +87,10 @@ enum class StringId : uint8_t {
LOCATION_ADD,
LOCATION_LAT_PROMPT,
LOCATION_LON_PROMPT,
LOCATION_INFO_TITLE,
LOCATION_INFO_PARA1,
LOCATION_INFO_PARA2,
LOCATION_INFO_PARA3,
STATS_TITLE,
STATS_TODAY,

View File

@@ -87,6 +87,10 @@ static const char* const I18N_DE[] = {
"Preset hinzufügen",
"Breitengrad (z.B. 48.6833)",
"Längengrad (z.B. 9.8333)",
"Wie Presets funktionieren",
"Nur EIN Standort ist zur selben Zeit aktiv - entweder Auto oder genau ein Preset. Tippe einen Eintrag an, um ihn zu aktivieren.",
"Du kannst auch die Koordinaten eines beliebigen anderen Ortes eintragen - z.B. Mailand, New York oder Tokio - und siehst DORT den aktuellen Flugverkehr an, egal wo dein Gerät tatsächlich steht!",
"Praktisch auch für mehr Genauigkeit als die automatische IP-Standortbestimmung, z.B. wenn das Gerät dauerhaft an einem festen Ort steht.",
"Statistik",
"Heute geloggte Flugzeuge",

View File

@@ -88,6 +88,11 @@ static const char* const I18N_EN[] = {
"Latitude (e.g. 48.6833)",
"Longitude (e.g. 9.8333)",
"How presets work",
"Only ONE location is active at a time - either Auto or exactly one preset. Tap an entry to activate it.",
"You can also enter the coordinates of any other place - e.g. Milan, New York or Tokyo - and see the live air traffic THERE, no matter where your device actually is!",
"Also handy for more precision than automatic IP-based location, e.g. when the device stays in one fixed spot.",
"Statistics",
"Aircraft logged today",
"Aircraft logged all-time",

View File

@@ -87,6 +87,10 @@ static const char* const I18N_ES[] = {
"Añadir ubicación",
"Latitud (ej. 48.6833)",
"Longitud (ej. 9.8333)",
"Como funcionan los presets",
"Solo UNA ubicacion esta activa a la vez - Auto o exactamente un preset. Toque una entrada para activarla.",
"Tambien puede introducir las coordenadas de cualquier otro lugar - ej. Milan, Nueva York o Tokio - y ver el trafico aereo en tiempo real ALLI, sin importar donde este su dispositivo!",
"Tambien es util para mas precision que la geolocalizacion automatica por IP, por ejemplo si el dispositivo permanece siempre en un lugar fijo.",
"Estadísticas",
"Aviones registrados hoy",

View File

@@ -87,6 +87,10 @@ static const char* const I18N_FR[] = {
"Ajouter une position",
"Latitude (ex. 48.6833)",
"Longitude (ex. 9.8333)",
"Comment marchent les presets",
"Un SEUL lieu est actif a la fois - soit Auto, soit un preset precis. Touchez une entree pour l'activer.",
"Vous pouvez aussi entrer les coordonnees de n'importe quel autre lieu - ex. Milan, New York ou Tokyo - et voir le trafic aerien LA-BAS, peu importe ou se trouve votre appareil !",
"Pratique aussi pour plus de precision que la geolocalisation IP automatique, par exemple si l'appareil reste toujours au meme endroit.",
"Statistiques",
"Avions enregistrés aujourd'hui",

View File

@@ -87,6 +87,10 @@ static const char* const I18N_IT[] = {
"Aggiungi posizione",
"Latitudine (es. 48.6833)",
"Longitudine (es. 9.8333)",
"Come funzionano i preset",
"Solo UNA posizione e attiva alla volta - Auto oppure esattamente un preset. Tocca una voce per attivarla.",
"Puoi anche inserire le coordinate di qualsiasi altro luogo - es. Milano, New York o Tokyo - e vedere il traffico aereo in tempo reale LI, indipendentemente da dove si trovi il dispositivo!",
"Utile anche per maggiore precisione rispetto alla geolocalizzazione IP automatica, ad esempio se il dispositivo resta sempre in un punto fisso.",
"Statistiche",
"Aerei registrati oggi",

View File

@@ -87,6 +87,10 @@ static const char* const I18N_TR[] = {
"Ön ayar ekle",
"Enlem (örn. 48.6833)",
"Boylam (örn. 9.8333)",
"On ayarlar nasil calisir",
"Aynı anda SADECE bir konum aktiftir - Otomatik veya tam olarak bir on ayar. Aktif etmek icin bir girise dokunun.",
"Ayrica baska herhangi bir yerin koordinatlarini da girebilirsiniz - orn. Milano, New York veya Tokyo - ve cihazinizin gercek konumundan bagimsiz olarak ORADAKI canli hava trafigini gorebilirsiniz!",
"Cihaz surekli sabit bir yerde kaliyorsa, otomatik IP tabanli konumdan daha hassas olmasi icin de kullanislidir.",
"İstatistikler",
"Bugün kaydedilen uçaklar",

View File

@@ -42,7 +42,7 @@ void run(TFT_eSPI& tft) {
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 6);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LANGUAGE_TITLE));
uint8_t current = SettingsStore::language();

View File

@@ -60,7 +60,7 @@ namespace {
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(title);
tft.fillRect(8, 40, Config::SCREEN_WIDTH - 16, 34, TFT_BLACK);
@@ -125,6 +125,113 @@ namespace {
return LocationPresets::addPreset(lat, lon);
}
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;
}
String truncateForWidth(TFT_eSPI& tft, const String& text, int16_t maxWidth) {
String s = text;
if (tft.textWidth(s) <= maxWidth) return s;
while (s.length() > 1 && tft.textWidth(s + "...") > maxWidth) {
s.remove(s.length() - 1);
}
return s + "...";
}
void runInfoScreen(TFT_eSPI& tft) {
MenuStars::reset();
constexpr int16_t textMaxWidth = Config::SCREEN_WIDTH - 20;
constexpr int16_t LINE_H = 16;
constexpr int16_t VIEW_TOP = 40;
Rect backBtn = {10, (int16_t)(Config::SCREEN_HEIGHT - 50), (int16_t)(Config::SCREEN_WIDTH - 20), 40};
constexpr int16_t VIEW_BOTTOM = Config::SCREEN_HEIGHT - 60;
int16_t totalH = VIEW_TOP;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA1), 0, 0, 0, false);
totalH += 8;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), 0, 0, 0, false);
totalH += 8;
totalH = layoutWrapped(tft, 10, totalH, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), 0, 0, 0, false);
int16_t maxScroll = totalH - VIEW_BOTTOM;
if (maxScroll < 0) maxScroll = 0;
bool scrollable = maxScroll > 0;
int16_t scrollY = 0;
Rect upBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 4, 30, 24};
Rect downBtn = {(int16_t)(Config::SCREEN_WIDTH - 34), 30, 30, 24};
constexpr int16_t SCROLL_STEP = 48;
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOCATION_INFO_TITLE));
if (scrollable) {
drawButton(tft, upBtn, "^");
drawButton(tft, downBtn, "v");
}
tft.setTextColor(TFT_GREEN, TFT_BLACK);
int16_t y = VIEW_TOP;
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA1), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
y += 8;
y = layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA2), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
y += 8;
layoutWrapped(tft, 10, y, textMaxWidth, LINE_H, I18n::t(StringId::LOCATION_INFO_PARA3), scrollY, VIEW_TOP, VIEW_BOTTOM, true);
drawButton(tft, backBtn, I18n::t(StringId::BACK));
};
redraw();
while (true) {
TouchInput::Point tap;
if (TouchInput::wasTapped(tap)) {
if (backBtn.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);
}
}
}
void run(TFT_eSPI& tft) {
@@ -137,9 +244,12 @@ void run(TFT_eSPI& tft) {
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 8);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOCATION_TITLE));
Rect infoBtn = {(int16_t)(Config::SCREEN_WIDTH - 40), 2, 30, 24};
drawButton(tft, infoBtn, "?");
int8_t active = LocationPresets::activeIndex();
uint8_t count = LocationPresets::count();
int16_t y = 36;
@@ -163,8 +273,9 @@ void run(TFT_eSPI& tft) {
double lat = 0, lon = 0;
LocationPresets::getLatLon(i, lat, lon);
char coords[24];
snprintf(coords, sizeof(coords), "%d: %.3f, %.3f", i + 1, lat, lon);
snprintf(coords, sizeof(coords), "%d: %.2f, %.2f", i + 1, lat, lon);
String label = (active == (int8_t)i ? "> " : "") + presetWord + " " + coords;
label = truncateForWidth(tft, label, (int16_t)(rowRect.w - 10));
drawButton(tft, rowRect, label, active == (int8_t)i);
drawButton(tft, removeRect, "X", false, true);
} else {
@@ -196,7 +307,10 @@ void run(TFT_eSPI& tft) {
}
bool handled = false;
if (autoRect.contains(tap.x, tap.y)) {
if (infoBtn.contains(tap.x, tap.y)) {
runInfoScreen(tft);
handled = true;
} else if (autoRect.contains(tap.x, tap.y)) {
LocationPresets::setActiveIndex(-1);
handled = true;
}

View File

@@ -31,7 +31,7 @@ namespace {
void run(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 8);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOGFILES_TITLE));
tft.setTextColor(TFT_DARKGREEN, TFT_BLACK);
tft.setCursor(10, 30);
@@ -44,7 +44,7 @@ void run(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 8);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::LOGFILES_TITLE));
if (count == 0) {

View File

@@ -85,7 +85,7 @@ void run(TFT_eSPI& tft) {
if (page == Page::Main) {
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 6);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_SETTINGS));
Rect regionBtn = catRowRect(0);
@@ -121,7 +121,7 @@ void run(TFT_eSPI& tft) {
} else if (page == Page::Region) {
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 2);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_CATEGORY_REGION));
Rect languageBtn = rowRect(0);
@@ -149,7 +149,7 @@ void run(TFT_eSPI& tft) {
} else if (page == Page::System) {
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 2);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_CATEGORY_SYSTEM));
Rect calibBtn = rowRect(0);
@@ -204,7 +204,7 @@ void run(TFT_eSPI& tft) {
} else { // Page::Flight
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 2);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::MENU_CATEGORY_FLIGHT));
Rect statsBtn = rowRect(0);

View File

@@ -46,7 +46,7 @@ namespace {
void run(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::STATS_TITLE));
tft.setTextColor(TFT_DARKGREEN, TFT_BLACK);
tft.setCursor(10, ROW1_Y);
@@ -67,7 +67,7 @@ void run(TFT_eSPI& tft) {
auto redraw = [&]() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::STATS_TITLE));
drawStatRow(tft, ROW1_Y, I18n::t(StringId::STATS_TODAY), String(today));

View File

@@ -45,7 +45,7 @@ void run(TFT_eSPI& tft) {
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::UNITS_TITLE));
uint8_t current = SettingsStore::unitsMode();

View File

@@ -37,7 +37,7 @@ void run(TFT_eSPI& tft) {
while (!done) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WIFI_NETWORKS_TITLE));
uint8_t count = WifiMgr::networkCount();

View File

@@ -203,7 +203,7 @@ bool run(TFT_eSPI& tft) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextSize(1);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.println(I18n::t(StringId::WIFI_SCANNING));
drawCancelButton(tft);
@@ -223,7 +223,7 @@ bool run(TFT_eSPI& tft) {
needsRedraw = true;
tft.fillScreen(TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.println(ssidCount == 0 ? I18n::t(StringId::WIFI_NO_NETWORKS) : I18n::t(StringId::WIFI_SELECT));
drawCancelButton(tft);
@@ -236,7 +236,7 @@ bool run(TFT_eSPI& tft) {
WifiMgr::addNetwork(ssidList[selectedIndex].c_str(), passwordBuf);
stage = Stage::Done;
tft.fillScreen(TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.println(I18n::t(StringId::WIFI_CONNECTED_BANG));
delay(900);
@@ -244,7 +244,7 @@ bool run(TFT_eSPI& tft) {
} else if (WifiMgr::getState() == WifiMgr::State::Failed) {
stage = Stage::Done;
tft.fillScreen(TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.println(I18n::t(StringId::WIFI_CONNECTION_FAILED));
tft.setTextColor(TFT_GREEN, TFT_BLACK);
@@ -309,7 +309,7 @@ bool run(TFT_eSPI& tft) {
} else if (connectBtn.contains(tap.x, tap.y)) {
if (WifiMgr::networkCount() >= Config::MAX_WIFI_NETWORKS) {
tft.fillScreen(TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.println(I18n::t(StringId::WIFI_ALREADY_3));
tft.setTextColor(TFT_GREEN, TFT_BLACK);
@@ -322,7 +322,7 @@ bool run(TFT_eSPI& tft) {
WifiMgr::connectTo(ssidList[selectedIndex].c_str(), passwordBuf);
stage = Stage::Connecting;
tft.fillScreen(TFT_BLACK);
tft.setCursor(10, 10);
tft.setCursor(10, 14);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.println(I18n::t(StringId::WIFI_CONNECTING));
} else if (backBtn.contains(tap.x, tap.y)) {
@@ -358,7 +358,7 @@ bool run(TFT_eSPI& tft) {
} else if (stage == Stage::EnterPassword) {
tft.fillRect(0, 0, Config::SCREEN_WIDTH, KB_TOP - 4, TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setCursor(10, 6);
tft.setCursor(10, 14);
tft.printf("%s%s", I18n::t(StringId::WIFI_LABEL_PREFIX), ssidList[selectedIndex].c_str());
tft.setCursor(10, 22);
tft.setTextColor(TFT_GREEN, TFT_BLACK);