Telemetry request: map and autorefresh

This commit is contained in:
HDDen
2026-05-19 20:37:34 +03:00
parent c4b3971bdd
commit 425229fce8
39 changed files with 1131 additions and 18 deletions
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Изтрий",
"common_enable": "Активирай",
"common_disable": "Деактивирай",
"common_autoRefresh": "Автоматично обновяване",
"common_interval": "Интервал",
"common_reboot": "Рестартирай",
"common_loading": "Зареждане...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Брой заявки",
"telemetry_error": "Неуспешно получаване на данни",
"telemetry_noData": "Няма налични данни за телеметрията.",
"telemetry_channelTitle": "Канал {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Löschen",
"common_enable": "Aktivieren",
"common_disable": "Deaktivieren",
"common_autoRefresh": "Automatische Aktualisierung",
"common_interval": "Intervall",
"common_reboot": "Neustart",
"common_loading": "Laden...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Anzahl der Anfragen",
"telemetry_error": "Daten konnten nicht abgerufen werden",
"telemetry_noData": "Keine Telemetriedaten verfügbar.",
"telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -47,6 +47,8 @@
}
}
},
"common_autoRefresh": "Autorefresh",
"common_interval": "Interval",
"scanner_title": "MeshCore Open",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
@@ -1661,6 +1663,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Requests quantity",
"telemetry_error": "Unable to retrieve data",
"neighbors_receivedData": "Received Neighbors Data",
"neighbors_requestTimedOut": "Neighbors request timed out.",
"neighbors_errorLoading": "Error loading neighbors: {error}",
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Eliminar",
"common_enable": "Activar",
"common_disable": "Desactivar",
"common_autoRefresh": "Actualización automática",
"common_interval": "Intervalo",
"common_reboot": "Reiniciar",
"common_loading": "Cargando...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Número de solicitudes",
"telemetry_error": "No se pudieron obtener los datos",
"telemetry_noData": "No hay datos de telemetría disponibles.",
"telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Supprimer",
"common_enable": "Activer",
"common_disable": "Désactiver",
"common_autoRefresh": "Actualisation automatique",
"common_interval": "Intervalle",
"common_reboot": "Redémarrer",
"common_loading": "Chargement...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Nombre de requêtes",
"telemetry_error": "Impossible de récupérer les données",
"telemetry_noData": "Aucune donnée de télémétrie disponible.",
"telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -27,6 +27,8 @@
"common_remove": "Eltávolít",
"common_enable": "Engedélyezés",
"common_disable": "Leteteszt",
"common_autoRefresh": "Automatikus frissítés",
"common_interval": "Intervallum",
"common_reboot": "Újraindítás",
"common_loading": "Betöltés...",
"common_notAvailable": "—",
@@ -1466,6 +1468,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Kérések száma",
"telemetry_error": "Nem sikerült lekérni az adatokat",
"telemetry_noData": "Nincsenek elérhető telemetriadatok.",
"telemetry_channelTitle": "{channel} csatorna",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Elimina",
"common_enable": "Abilita",
"common_disable": "Disattivare",
"common_autoRefresh": "Aggiornamento automatico",
"common_interval": "Intervallo",
"common_reboot": "Riavvia",
"common_loading": "Caricamento...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Numero di richieste",
"telemetry_error": "Impossibile recuperare i dati",
"telemetry_noData": "Nessun dato di telemetria disponibile.",
"telemetry_channelTitle": "Canale {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -27,6 +27,8 @@
"common_remove": "削除",
"common_enable": "有効化する",
"common_disable": "無効化する",
"common_autoRefresh": "自動更新",
"common_interval": "間隔",
"common_reboot": "再起動",
"common_loading": "読み込み中...",
"common_notAvailable": "—",
@@ -1466,6 +1468,8 @@
}
}
},
"telemetry_autoFetchQuantity": "リクエスト数",
"telemetry_error": "データを取得できません",
"telemetry_noData": "テレメトリデータは利用できません。",
"telemetry_channelTitle": "チャンネル {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -27,6 +27,8 @@
"common_remove": "제거",
"common_enable": "활성화",
"common_disable": "비활성화",
"common_autoRefresh": "자동 새로고침",
"common_interval": "간격",
"common_reboot": "재부팅",
"common_loading": "로딩 중...",
"common_notAvailable": "—",
@@ -1466,6 +1468,8 @@
}
}
},
"telemetry_autoFetchQuantity": "요청 수",
"telemetry_error": "데이터를 가져올 수 없습니다",
"telemetry_noData": "텔레메트리 데이터는 제공되지 않습니다.",
"telemetry_channelTitle": "채널 {channel}",
"@telemetry_channelTitle": {
+24
View File
@@ -328,6 +328,18 @@ abstract class AppLocalizations {
/// **'{percent}%'**
String common_percentValue(int percent);
/// No description provided for @common_autoRefresh.
///
/// In en, this message translates to:
/// **'Autorefresh'**
String get common_autoRefresh;
/// No description provided for @common_interval.
///
/// In en, this message translates to:
/// **'Interval'**
String get common_interval;
/// No description provided for @scanner_title.
///
/// In en, this message translates to:
@@ -5690,6 +5702,18 @@ abstract class AppLocalizations {
/// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @telemetry_autoFetchQuantity.
///
/// In en, this message translates to:
/// **'Requests quantity'**
String get telemetry_autoFetchQuantity;
/// No description provided for @telemetry_error.
///
/// In en, this message translates to:
/// **'Unable to retrieve data'**
String get telemetry_error;
/// No description provided for @neighbors_receivedData.
///
/// In en, this message translates to:
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsBg extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Автоматично обновяване';
@override
String get common_interval => 'Интервал';
@override
String get scanner_title => 'MeshCore – Отворена версия';
@@ -3276,6 +3282,12 @@ class AppLocalizationsBg extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Брой заявки';
@override
String get telemetry_error => 'Неуспешно получаване на данни';
@override
String get neighbors_receivedData => 'Получени данни за съседи';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsDe extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatische Aktualisierung';
@override
String get common_interval => 'Intervall';
@override
String get scanner_title => 'MeshCore Open-Version';
@@ -3282,6 +3288,12 @@ class AppLocalizationsDe extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Anzahl der Anfragen';
@override
String get telemetry_error => 'Daten konnten nicht abgerufen werden';
@override
String get neighbors_receivedData => 'Empfangene Nachbarsdaten';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsEn extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Autorefresh';
@override
String get common_interval => 'Interval';
@override
String get scanner_title => 'MeshCore Open';
@@ -3213,6 +3219,12 @@ class AppLocalizationsEn extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Requests quantity';
@override
String get telemetry_error => 'Unable to retrieve data';
@override
String get neighbors_receivedData => 'Received Neighbors Data';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsEs extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Actualización automática';
@override
String get common_interval => 'Intervalo';
@override
String get scanner_title => 'MeshCore: Versión abierta';
@@ -3270,6 +3276,12 @@ class AppLocalizationsEs extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Número de solicitudes';
@override
String get telemetry_error => 'No se pudieron obtener los datos';
@override
String get neighbors_receivedData => 'Recibidas Datos de Vecinos';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsFr extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Actualisation automatique';
@override
String get common_interval => 'Intervalle';
@override
String get scanner_title => 'MeshCore Open';
@@ -3293,6 +3299,12 @@ class AppLocalizationsFr extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Nombre de requêtes';
@override
String get telemetry_error => 'Impossible de récupérer les données';
@override
String get neighbors_receivedData => 'Données des voisins reçues';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsHu extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatikus frissítés';
@override
String get common_interval => 'Intervallum';
@override
String get scanner_title => 'MeshCore nyitott';
@@ -3286,6 +3292,12 @@ class AppLocalizationsHu extends AppLocalizations {
return '$celsius °C / $fahrenheit °F';
}
@override
String get telemetry_autoFetchQuantity => 'Kérések száma';
@override
String get telemetry_error => 'Nem sikerült lekérni az adatokat';
@override
String get neighbors_receivedData => 'Kapott szomszédok adatait';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsIt extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Aggiornamento automatico';
@override
String get common_interval => 'Intervallo';
@override
String get scanner_title => 'MeshCore Open';
@@ -3276,6 +3282,12 @@ class AppLocalizationsIt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Numero di richieste';
@override
String get telemetry_error => 'Impossibile recuperare i dati';
@override
String get neighbors_receivedData => 'Ricevute dati vicini';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsJa extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => '自動更新';
@override
String get common_interval => '間隔';
@override
String get scanner_title => 'MeshCore オープン';
@@ -3094,6 +3100,12 @@ class AppLocalizationsJa extends AppLocalizations {
return '$celsius℃ / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'リクエスト数';
@override
String get telemetry_error => 'データを取得できません';
@override
String get neighbors_receivedData => '近隣住民のデータを受信';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsKo extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => '자동 새로고침';
@override
String get common_interval => '간격';
@override
String get scanner_title => 'MeshCore 공개';
@@ -3096,6 +3102,12 @@ class AppLocalizationsKo extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => '요청 수';
@override
String get telemetry_error => '데이터를 가져올 수 없습니다';
@override
String get neighbors_receivedData => '이웃 정보 수집';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsNl extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatisch vernieuwen';
@override
String get common_interval => 'Tijdsinterval';
@override
String get scanner_title => 'MeshCore Open';
@@ -3256,6 +3262,12 @@ class AppLocalizationsNl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Aantal aanvragen';
@override
String get telemetry_error => 'Kan gegevens niet ophalen';
@override
String get neighbors_receivedData => 'Ontvangen Buurdata';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsPl extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatyczne odświeżanie';
@override
String get common_interval => 'Interwał';
@override
String get scanner_title => 'MeshCore wersja open source';
@@ -3288,6 +3294,12 @@ class AppLocalizationsPl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Liczba żądań';
@override
String get telemetry_error => 'Nie udało się pobrać danych';
@override
String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsPt extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Atualização automática';
@override
String get common_interval => 'Intervalo';
@override
String get scanner_title => 'MeshCore: Versão aberta';
@@ -3269,6 +3275,12 @@ class AppLocalizationsPt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Número de solicitações';
@override
String get telemetry_error => 'Não foi possível obter os dados';
@override
String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsRu extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Автообновление';
@override
String get common_interval => 'Интервал';
@override
String get scanner_title => 'MeshCore Open';
@@ -3277,6 +3283,12 @@ class AppLocalizationsRu extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Количество запросов';
@override
String get telemetry_error => 'Не удалось получить данные';
@override
String get neighbors_receivedData => 'Полученные данные о соседях';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsSk extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatické obnovenie';
@override
String get common_interval => 'Časový interval';
@override
String get scanner_title => 'MeshCore Verzia pre verejnosť';
@@ -3255,6 +3261,12 @@ class AppLocalizationsSk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Počet požiadaviek';
@override
String get telemetry_error => 'Nepodarilo sa získať údaje';
@override
String get neighbors_receivedData => 'Obdielo dáta suseda';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsSl extends AppLocalizations {
return '$percent %';
}
@override
String get common_autoRefresh => 'Samodejno osveževanje';
@override
String get common_interval => 'Časovni interval';
@override
String get scanner_title => 'MeshCore Odprto';
@@ -3250,6 +3256,12 @@ class AppLocalizationsSl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Število zahtev';
@override
String get telemetry_error => 'Podatkov ni bilo mogoče pridobiti';
@override
String get neighbors_receivedData => 'Prejeto podatke o sosedih';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsSv extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Automatisk uppdatering';
@override
String get common_interval => 'Intervall';
@override
String get scanner_title => 'MeshCore Öppen version';
@@ -3231,6 +3237,12 @@ class AppLocalizationsSv extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Antal förfrågningar';
@override
String get telemetry_error => 'Det gick inte att hämta data';
@override
String get neighbors_receivedData => 'Mottagna grannars data';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsUk extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => 'Автооновлення';
@override
String get common_interval => 'Інтервал';
@override
String get scanner_title => 'MeshCore: Відкритий доступ';
@@ -3274,6 +3280,12 @@ class AppLocalizationsUk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => 'Кількість запитів';
@override
String get telemetry_error => 'Не вдалося отримати дані';
@override
String get neighbors_receivedData => 'Дані сусідів отримано';
+12
View File
@@ -111,6 +111,12 @@ class AppLocalizationsZh extends AppLocalizations {
return '$percent%';
}
@override
String get common_autoRefresh => '自动刷新';
@override
String get common_interval => '间隔';
@override
String get scanner_title => '连接设备';
@@ -2999,6 +3005,12 @@ class AppLocalizationsZh extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get telemetry_autoFetchQuantity => '请求次数';
@override
String get telemetry_error => '无法获取数据';
@override
String get neighbors_receivedData => '已接收邻居信息';
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Verwijderen",
"common_enable": "Activeren",
"common_disable": "Uitschakelen",
"common_autoRefresh": "Automatisch vernieuwen",
"common_interval": "Tijdsinterval",
"common_reboot": "Herstarten",
"common_loading": "Laden...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Aantal aanvragen",
"telemetry_error": "Kan gegevens niet ophalen",
"telemetry_noData": "Geen telemetriedata beschikbaar.",
"telemetry_channelTitle": "Kanaal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Usuń",
"common_enable": "Włącz",
"common_disable": "Wyłącz",
"common_autoRefresh": "Automatyczne odświeżanie",
"common_interval": "Interwał",
"common_reboot": "Uruchom ponownie",
"common_loading": "Ładowanie...",
"common_notAvailable": "—",
@@ -1290,6 +1292,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Liczba żądań",
"telemetry_error": "Nie udało się pobrać danych",
"telemetry_noData": "Brak dostępnych danych telemetrycznych.",
"telemetry_channelTitle": "Kanał {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Remover",
"common_enable": "Ativar",
"common_disable": "Desativar",
"common_autoRefresh": "Atualização automática",
"common_interval": "Intervalo",
"common_reboot": "Reiniciar",
"common_loading": "Carregando...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Número de solicitações",
"telemetry_error": "Não foi possível obter os dados",
"telemetry_noData": "Não estão disponíveis dados de telemetria.",
"telemetry_channelTitle": "Canal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -39,6 +39,8 @@
"common_notAvailable": "—",
"common_voltageValue": "{volts} В",
"common_percentValue": "{percent}%",
"common_autoRefresh": "Автообновление",
"common_interval": "Интервал",
"scanner_title": "MeshCore Open",
"scanner_scanning": "Поиск устройств...",
"scanner_connecting": "Подключение...",
@@ -686,6 +688,8 @@
"telemetry_voltageValue": "{volts}В",
"telemetry_currentValue": "{amps}А",
"telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
"telemetry_autoFetchQuantity": "Количество запросов",
"telemetry_error": "Не удалось получить данные",
"neighbors_receivedData": "Полученные данные о соседях",
"neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.",
"neighbors_errorLoading": "Ошибка загрузки соседей: {error}",
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Odstrániť",
"common_enable": "Povolit",
"common_disable": "Zakázať",
"common_autoRefresh": "Automatické obnovenie",
"common_interval": "Časový interval",
"common_reboot": "Restartovať",
"common_loading": "Načítavanie...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Počet požiadaviek",
"telemetry_error": "Nepodarilo sa získať údaje",
"telemetry_noData": "Nejsú dostupné žiadne údaje z telemetrie.",
"telemetry_channelTitle": "Kanál {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Izbrisati",
"common_enable": "Omogoči",
"common_disable": "Izklopiti",
"common_autoRefresh": "Samodejno osveževanje",
"common_interval": "Časovni interval",
"common_reboot": "Ponoviti",
"common_loading": "Naložanje...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Število zahtev",
"telemetry_error": "Podatkov ni bilo mogoče pridobiti",
"telemetry_noData": "Niso na voljo podatki o telemetriji.",
"telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -33,6 +33,8 @@
"common_remove": "Ta bort",
"common_enable": "Aktivera",
"common_disable": "Inaktivera",
"common_autoRefresh": "Automatisk uppdatering",
"common_interval": "Intervall",
"common_reboot": "Start om",
"common_loading": "Laddar...",
"common_notAvailable": "—",
@@ -1280,6 +1282,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Antal förfrågningar",
"telemetry_error": "Det gick inte att hämta data",
"telemetry_noData": "Inga telemetridata tillgängliga.",
"telemetry_channelTitle": "Kanal {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -34,6 +34,8 @@
"common_remove": "Прибрати",
"common_enable": "Увімкнути",
"common_disable": "Вимкнути",
"common_autoRefresh": "Автооновлення",
"common_interval": "Інтервал",
"common_reboot": "Перезавантажити",
"common_loading": "Завантаження...",
"common_notAvailable": "—",
@@ -1291,6 +1293,8 @@
}
}
},
"telemetry_autoFetchQuantity": "Кількість запитів",
"telemetry_error": "Не вдалося отримати дані",
"telemetry_noData": "Дані телеметрії недоступні.",
"telemetry_channelTitle": "Канал {channel}",
"@telemetry_channelTitle": {
+4
View File
@@ -34,6 +34,8 @@
"common_remove": "移除",
"common_enable": "启用",
"common_disable": "禁用",
"common_autoRefresh": "自动刷新",
"common_interval": "间隔",
"common_reboot": "重启",
"common_loading": "正在加载...",
"common_notAvailable": "—",
@@ -1310,6 +1312,8 @@
}
}
},
"telemetry_autoFetchQuantity": "请求次数",
"telemetry_error": "无法获取数据",
"telemetry_noData": "暂无遥测数据",
"telemetry_channelTitle": "频道 {channel}",
"@telemetry_channelTitle": {
+382 -3
View File
@@ -1,11 +1,13 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../models/app_settings.dart';
import '../storage/prefs_manager.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/app_settings_service.dart';
@@ -15,6 +17,7 @@ import '../widgets/path_management_dialog.dart';
import '../helpers/cayenne_lpp.dart';
import '../utils/battery_utils.dart';
import '../helpers/snack_bar_builder.dart';
import '../widgets/telemetry_location_map.dart';
class TelemetryScreen extends StatefulWidget {
final Contact contact;
@@ -26,6 +29,13 @@ class TelemetryScreen extends StatefulWidget {
}
class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _autoRefreshDefaultIntervalSeconds = 20;
static const int _autoRefreshDefaultQuantity = 10;
static const int _autoRefreshMinIntervalSeconds = 10;
static const int _autoRefreshMaxIntervalSeconds = 300;
static const int _autoRefreshMinQuantity = 1;
static const int _autoRefreshMaxQuantity = 10;
int _tagData = 0;
bool _isLoading = false;
@@ -36,6 +46,17 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedTelemetry;
final TextEditingController _autoRefreshIntervalController =
TextEditingController(text: '$_autoRefreshDefaultIntervalSeconds');
final TextEditingController _autoRefreshQuantityController =
TextEditingController(text: '$_autoRefreshDefaultQuantity');
Timer? _autoRefreshTimer;
bool _isAutoRefreshEnabled = false;
bool _activeTelemetryRequestIsAutoRefresh = false;
bool _autoRefreshLastAttemptFailed = false;
int _autoRefreshCurrentAttempt = 0;
int _autoRefreshTotalAttempts = 0;
int _autoRefreshIntervalSeconds = _autoRefreshDefaultIntervalSeconds;
int _tripTime = 0;
@@ -62,6 +83,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_loadAutoRefreshSettings();
_setupMessageListener();
_loadTelemetry();
_hasData = false;
@@ -81,17 +103,26 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_tagData = reader.readUInt32LE();
_tripTime = reader.readUInt32LE();
_statusTimeout?.cancel();
final isAutoRefreshRequest = _activeTelemetryRequestIsAutoRefresh;
_statusTimeout = Timer(Duration(milliseconds: _tripTime), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
if (isAutoRefreshRequest && _isAutoRefreshEnabled) {
_autoRefreshLastAttemptFailed = true;
}
});
if (!isAutoRefreshRequest) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_requestTimeout),
backgroundColor: Colors.red,
);
}
if (isAutoRefreshRequest && _isAutoRefreshEnabled) {
_scheduleNextAutoRefreshAttempt();
}
_recordTelemetryResult(false);
});
}
@@ -133,15 +164,22 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
);
}
if (!mounted) return;
final isAutoRefreshRequest = _activeTelemetryRequestIsAutoRefresh;
setState(() {
_parsedTelemetry = parsedTelemetry;
if (isAutoRefreshRequest) {
_autoRefreshLastAttemptFailed = false;
}
_activeTelemetryRequestIsAutoRefresh = false;
});
if (!isAutoRefreshRequest) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_receivedData),
backgroundColor: Colors.green,
);
}
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
@@ -149,14 +187,18 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
_isLoaded = true;
_hasData = true;
});
if (isAutoRefreshRequest) {
_scheduleNextAutoRefreshAttempt();
}
}
Future<void> _loadTelemetry() async {
Future<void> _loadTelemetry({bool isAutoRefresh = false}) async {
if (_commandService == null) return;
setState(() {
_isLoading = true;
_isLoaded = false;
_activeTelemetryRequestIsAutoRefresh = isAutoRefresh;
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
@@ -179,8 +221,16 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
setState(() {
_isLoading = false;
_isLoaded = false;
if (isAutoRefresh) {
_autoRefreshLastAttemptFailed = true;
}
_activeTelemetryRequestIsAutoRefresh = false;
});
if (isAutoRefresh) {
_scheduleNextAutoRefreshAttempt();
}
if (!isAutoRefresh) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
@@ -189,6 +239,57 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
}
}
}
}
void _loadAutoRefreshSettings() {
final prefs = PrefsManager.instance;
final contactKey = widget.contact.publicKeyHex;
final interval =
(prefs.getInt(_autoRefreshIntervalKey(contactKey)) ??
_autoRefreshDefaultIntervalSeconds)
.clamp(
_autoRefreshMinIntervalSeconds,
_autoRefreshMaxIntervalSeconds,
)
.toInt();
final quantity =
(prefs.getInt(_autoRefreshQuantityKey(contactKey)) ??
_autoRefreshDefaultQuantity)
.clamp(_autoRefreshMinQuantity, _autoRefreshMaxQuantity)
.toInt();
_autoRefreshIntervalSeconds = interval;
_autoRefreshIntervalController.text = interval.toString();
_autoRefreshQuantityController.text = quantity.toString();
}
Future<void> _saveAutoRefreshSettings() async {
final contactKey = widget.contact.publicKeyHex;
final interval = _clampControllerValue(
controller: _autoRefreshIntervalController,
min: _autoRefreshMinIntervalSeconds,
max: _autoRefreshMaxIntervalSeconds,
fallback: _autoRefreshIntervalSeconds,
);
final quantity = _clampControllerValue(
controller: _autoRefreshQuantityController,
min: _autoRefreshMinQuantity,
max: _autoRefreshMaxQuantity,
fallback: _autoRefreshDefaultQuantity,
);
final prefs = PrefsManager.instance;
await prefs.setInt(_autoRefreshIntervalKey(contactKey), interval);
await prefs.setInt(_autoRefreshQuantityKey(contactKey), quantity);
}
String _autoRefreshIntervalKey(String contactKey) {
return 'telemetry_auto_refresh_interval_$contactKey';
}
String _autoRefreshQuantityKey(String contactKey) {
return 'telemetry_auto_refresh_quantity_$contactKey';
}
void _recordTelemetryResult(bool success) {
final selection = _pendingStatusSelection;
@@ -205,9 +306,13 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
@override
void dispose() {
unawaited(_saveAutoRefreshSettings());
_frameSubscription?.cancel();
_commandService?.dispose();
_statusTimeout?.cancel();
_autoRefreshTimer?.cancel();
_autoRefreshIntervalController.dispose();
_autoRefreshQuantityController.dispose();
super.dispose();
}
@@ -313,7 +418,9 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadTelemetry,
onPressed: (_isLoading || _isAutoRefreshEnabled)
? null
: () => _loadTelemetry(),
tooltip: l10n.repeater_refresh,
),
],
@@ -321,7 +428,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadTelemetry,
onRefresh: () =>
_isAutoRefreshEnabled ? Future.value() : _loadTelemetry(),
child: ListView(
padding: const EdgeInsets.all(16),
children: [
@@ -344,6 +452,7 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
entry['channel'],
isImperialUnits,
),
_buildAutoRefreshCard(),
],
),
),
@@ -407,6 +516,8 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
l10n.telemetry_currentLabel,
l10n.telemetry_currentValue(entry.value.toString()),
)
else if (entry.key == 'gps')
_buildGpsInfo(entry.value)
else
_buildInfoRow(entry.key, entry.value.toString()),
],
@@ -415,6 +526,274 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
);
}
Widget _buildAutoRefreshCard() {
final l10n = context.l10n;
final counterText = _autoRefreshCounterText();
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Icon(
Icons.autorenew,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
l10n.common_autoRefresh,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
_buildAutoRefreshNumberField(
controller: _autoRefreshIntervalController,
label: l10n.common_interval,
min: _autoRefreshMinIntervalSeconds,
max: _autoRefreshMaxIntervalSeconds,
fallback: _autoRefreshIntervalSeconds,
),
const SizedBox(height: 12),
_buildAutoRefreshNumberField(
controller: _autoRefreshQuantityController,
label: l10n.telemetry_autoFetchQuantity,
min: _autoRefreshMinQuantity,
max: _autoRefreshMaxQuantity,
fallback: _autoRefreshDefaultQuantity,
),
if (counterText != null) ...[
const SizedBox(height: 12),
Text(
counterText,
textAlign: TextAlign.center,
style: TextStyle(
color: _autoRefreshLastAttemptFailed
? Theme.of(context).colorScheme.error
: null,
fontWeight: FontWeight.w600,
),
),
],
const SizedBox(height: 12),
FilledButton(
onPressed: _isLoading && !_isAutoRefreshEnabled
? null
: _toggleAutoRefresh,
child: _isAutoRefreshEnabled
? SizedBox(
width: double.infinity,
height: 20,
child: Stack(
alignment: Alignment.center,
children: [
Center(child: Text(l10n.common_disable)),
const Positioned(
right: 0,
child: SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
),
],
),
)
: Text(l10n.common_enable),
),
],
),
),
);
}
Widget _buildAutoRefreshNumberField({
required TextEditingController controller,
required String label,
required int min,
required int max,
required int fallback,
}) {
return TextField(
controller: controller,
enabled: !_isAutoRefreshEnabled,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
isDense: true,
),
onEditingComplete: () {
_clampControllerValue(
controller: controller,
min: min,
max: max,
fallback: fallback,
);
unawaited(_saveAutoRefreshSettings());
FocusScope.of(context).unfocus();
},
onSubmitted: (_) => unawaited(_saveAutoRefreshSettings()),
onTapOutside: (_) {
unawaited(_saveAutoRefreshSettings());
FocusScope.of(context).unfocus();
},
);
}
String? _autoRefreshCounterText() {
if (!_isAutoRefreshEnabled && _autoRefreshCurrentAttempt == 0) return null;
final counter = '$_autoRefreshCurrentAttempt/$_autoRefreshTotalAttempts';
if (_autoRefreshLastAttemptFailed) {
return '${context.l10n.telemetry_error}: $counter';
}
return counter;
}
void _toggleAutoRefresh() {
if (_isAutoRefreshEnabled) {
_stopAutoRefresh();
return;
}
_startAutoRefresh();
}
void _startAutoRefresh() {
final interval = _clampControllerValue(
controller: _autoRefreshIntervalController,
min: _autoRefreshMinIntervalSeconds,
max: _autoRefreshMaxIntervalSeconds,
fallback: _autoRefreshIntervalSeconds,
);
final quantity = _clampControllerValue(
controller: _autoRefreshQuantityController,
min: _autoRefreshMinQuantity,
max: _autoRefreshMaxQuantity,
fallback: _autoRefreshDefaultQuantity,
);
unawaited(_saveAutoRefreshSettings());
setState(() {
_isAutoRefreshEnabled = true;
_autoRefreshIntervalSeconds = interval;
_autoRefreshTotalAttempts = quantity;
_autoRefreshCurrentAttempt = 0;
_autoRefreshLastAttemptFailed = false;
});
_runAutoRefreshAttempt();
}
void _stopAutoRefresh() {
_autoRefreshTimer?.cancel();
_autoRefreshTimer = null;
if (!mounted) return;
setState(() {
_isAutoRefreshEnabled = false;
});
}
Future<void> _runAutoRefreshAttempt() async {
if (!_isAutoRefreshEnabled || !mounted) return;
if (_autoRefreshCurrentAttempt >= _autoRefreshTotalAttempts) {
_stopAutoRefresh();
return;
}
setState(() {
_autoRefreshCurrentAttempt += 1;
});
await _loadTelemetry(isAutoRefresh: true);
}
void _scheduleNextAutoRefreshAttempt() {
if (!_isAutoRefreshEnabled || !mounted) return;
_autoRefreshTimer?.cancel();
if (_autoRefreshCurrentAttempt >= _autoRefreshTotalAttempts) {
_stopAutoRefresh();
return;
}
// Start the interval only after the current request has finished: after a
// telemetry response, timeout, or send error. This keeps slow replies from
// shortening the intended pause between requests.
_autoRefreshTimer = Timer(
Duration(seconds: _autoRefreshIntervalSeconds),
_runAutoRefreshAttempt,
);
}
int _clampControllerValue({
required TextEditingController controller,
required int min,
required int max,
required int fallback,
}) {
final parsed = int.tryParse(controller.text);
final value = (parsed ?? fallback).clamp(min, max).toInt();
controller.text = value.toString();
controller.selection = TextSelection.collapsed(
offset: controller.text.length,
);
return value;
}
Widget _buildGpsInfo(dynamic value) {
final latitude = _readGpsValue(value, 'latitude');
final longitude = _readGpsValue(value, 'longitude');
final altitude = _readGpsValue(value, 'altitude');
final isValidPosition = _isValidGpsPosition(latitude, longitude);
final gpsText = isValidPosition
? [
latitude!.toStringAsFixed(5),
longitude!.toStringAsFixed(5),
if (altitude != null) '${altitude.toStringAsFixed(1)} m',
].join(', ')
: value.toString();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('gps', gpsText),
if (isValidPosition)
TelemetryLocationMap(
// The map renders only after bounds validation, keeping malformed
// Cayenne payloads from creating an invalid FlutterMap center.
latitude: latitude!,
longitude: longitude!,
label: widget.contact.name,
contactType: widget.contact.type,
contactPublicKeyHex: widget.contact.publicKeyHex,
),
],
);
}
double? _readGpsValue(dynamic value, String key) {
if (value is! Map) return null;
final rawValue = value[key];
if (rawValue is num) return rawValue.toDouble();
return null;
}
bool _isValidGpsPosition(double? latitude, double? longitude) {
if (latitude == null || longitude == null) return false;
const double epsilon = 1e-6;
return (latitude.abs() > epsilon || longitude.abs() > epsilon) &&
latitude >= -90.0 &&
latitude <= 90.0 &&
longitude >= -180.0 &&
longitude <= 180.0;
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
+422
View File
@@ -0,0 +1,422 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../models/contact.dart';
import '../services/app_settings_service.dart';
import '../services/map_tile_cache_service.dart';
class TelemetryLocationMap extends StatefulWidget {
final double latitude;
final double longitude;
final String label;
final int contactType;
final String contactPublicKeyHex;
const TelemetryLocationMap({
super.key,
required this.latitude,
required this.longitude,
required this.label,
required this.contactType,
required this.contactPublicKeyHex,
});
@override
State<TelemetryLocationMap> createState() => _TelemetryLocationMapState();
}
class _TelemetryLocationMapState extends State<TelemetryLocationMap> {
static const double _initialZoom = 14.0;
static const double _minZoom = 2.0;
static const double _maxZoom = 18.0;
final MapController _mapController = MapController();
LatLng get _position => LatLng(widget.latitude, widget.longitude);
@override
void didUpdateWidget(covariant TelemetryLocationMap oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.latitude == widget.latitude &&
oldWidget.longitude == widget.longitude) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(_position, _initialZoom);
}
});
}
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
final settingsService = context.watch<AppSettingsService>();
final settings = settingsService.settings;
final tileCache = context.read<MapTileCacheService>();
final contacts = _filteredContacts(connector, settings);
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: LayoutBuilder(
builder: (context, constraints) {
final maxHeight = MediaQuery.sizeOf(context).height * 0.75;
final squareHeight = constraints.maxWidth.isFinite
? constraints.maxWidth
: maxHeight;
// Prefer square sizing by width, but cap only the height so the map
// remains usable on wide screens without growing past the viewport.
final mapHeight = squareHeight > maxHeight
? maxHeight
: squareHeight;
return SizedBox(
width: double.infinity,
height: mapHeight,
child: Stack(
children: [
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _position,
initialZoom: _initialZoom,
minZoom: _minZoom,
maxZoom: _maxZoom,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate,
scrollWheelVelocity: isDesktop ? 0.012 : 0.005,
cursorKeyboardRotationOptions:
CursorKeyboardRotationOptions.disabled(),
keyboardOptions: isDesktop
? const KeyboardOptions(
enableArrowKeysPanning: true,
enableWASDPanning: true,
enableRFZooming: true,
)
: const KeyboardOptions.disabled(),
),
),
children: [
TileLayer(
urlTemplate: kMapTileUrlTemplate,
tileProvider: tileCache.tileProvider,
userAgentPackageName:
MapTileCacheService.userAgentPackageName,
maxZoom: 19,
),
MarkerLayer(
markers: [
...contacts.map(_buildContactMarker),
_buildTelemetryMarker(),
_buildLabelMarker(_position, widget.label),
],
),
],
),
Positioned(
top: 8,
right: 8,
child: _MapButton(
icon: Icons.filter_list,
tooltip: context.l10n.map_filterNodes,
onPressed: () =>
_showFilterDialog(context, settingsService),
),
),
Positioned(
left: 8,
top: 8,
child: Column(
children: [
_MapButton(
icon: Icons.add,
tooltip: 'Zoom in',
onPressed: () => _zoomBy(1),
),
const SizedBox(height: 6),
_MapButton(
icon: Icons.remove,
tooltip: 'Zoom out',
onPressed: () => _zoomBy(-1),
),
const SizedBox(height: 6),
_MapButton(
icon: Icons.my_location,
tooltip: 'Center map',
onPressed: () =>
_mapController.move(_position, _initialZoom),
),
],
),
),
],
),
);
},
),
),
);
}
List<Contact> _filteredContacts(
MeshCoreConnector connector,
AppSettings settings,
) {
final contacts = settings.mapShowDiscoveryContacts
? connector.allContacts
: connector.allContacts.where((contact) => contact.isActive).toList();
return contacts.where((contact) {
if (!contact.hasLocation) return false;
if (contact.publicKeyHex == widget.contactPublicKeyHex) return false;
if (contact.type == advTypeChat) return settings.mapShowChatNodes;
if (contact.type == advTypeRepeater) return settings.mapShowRepeaters;
return settings.mapShowOtherNodes;
}).toList();
}
Marker _buildTelemetryMarker() {
return Marker(
point: _position,
width: 44,
height: 44,
child: IgnorePointer(
child: _MarkerBubble(
color: Colors.red,
icon: _getNodeIcon(widget.contactType),
size: 24,
),
),
);
}
Marker _buildContactMarker(Contact contact) {
return Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 34,
height: 34,
child: IgnorePointer(
child: _MarkerBubble(
color: _getNodeColor(contact.type),
icon: _getNodeIcon(contact.type),
size: 18,
),
),
);
}
Marker _buildLabelMarker(LatLng point, String label) {
return Marker(
point: point,
width: 140,
height: 24,
alignment: Alignment.topCenter,
child: IgnorePointer(
child: Transform.translate(
offset: const Offset(0, -24),
child: FittedBox(
fit: BoxFit.contain,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w500,
),
),
),
),
),
),
);
}
void _zoomBy(double delta) {
final camera = _mapController.camera;
final nextZoom = (camera.zoom + delta).clamp(_minZoom, _maxZoom).toDouble();
_mapController.move(camera.center, nextZoom);
}
bool _isDesktopPlatform(TargetPlatform platform) {
return platform == TargetPlatform.linux ||
platform == TargetPlatform.windows ||
platform == TargetPlatform.macOS;
}
Color _getNodeColor(int type) {
switch (type) {
case advTypeChat:
return Colors.blue;
case advTypeRepeater:
return Colors.green;
case advTypeRoom:
return Colors.purple;
case advTypeSensor:
return Colors.orange;
default:
return Colors.grey;
}
}
IconData _getNodeIcon(int type) {
switch (type) {
case advTypeChat:
return Icons.person;
case advTypeRepeater:
return Icons.router;
case advTypeRoom:
return Icons.meeting_room;
case advTypeSensor:
return Icons.sensors;
default:
return Icons.device_unknown;
}
}
void _showFilterDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.map_filterNodes),
content: SingleChildScrollView(
child: Consumer<AppSettingsService>(
builder: (consumerContext, service, child) {
final settings = service.settings;
// Reuse the global map filters so the telemetry preview and the
// main map stay consistent without another settings model.
return Column(
mainAxisSize: MainAxisSize.min,
children: [
CheckboxListTile(
title: Text(context.l10n.map_chatNodes),
value: settings.mapShowChatNodes,
onChanged: (value) {
service.setMapShowChatNodes(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_repeaters),
value: settings.mapShowRepeaters,
onChanged: (value) {
service.setMapShowRepeaters(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_otherNodes),
value: settings.mapShowOtherNodes,
onChanged: (value) {
service.setMapShowOtherNodes(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showDiscoveryContacts),
value: settings.mapShowDiscoveryContacts,
onChanged: (value) {
service.setMapShowDiscoveryContacts(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
],
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_close),
),
],
),
);
}
}
class _MarkerBubble extends StatelessWidget {
final Color color;
final IconData icon;
final double size;
const _MarkerBubble({
required this.color,
required this.icon,
required this.size,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Icon(icon, color: Colors.white, size: size),
);
}
}
class _MapButton extends StatelessWidget {
final IconData icon;
final String tooltip;
final VoidCallback onPressed;
const _MapButton({
required this.icon,
required this.tooltip,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
elevation: 3,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: IconButton(
icon: Icon(icon),
tooltip: tooltip,
onPressed: onPressed,
constraints: const BoxConstraints.tightFor(width: 40, height: 40),
padding: EdgeInsets.zero,
iconSize: 20,
),
);
}
}