mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-14 22:55:12 +10:00
Telemetry request: map and autorefresh
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 => 'Получени данни за съседи';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => '近隣住民のデータを受信';
|
||||
|
||||
|
||||
@@ -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 => '이웃 정보 수집';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Полученные данные о соседях';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 => 'Дані сусідів отримано';
|
||||
|
||||
|
||||
@@ -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 => '已接收邻居信息';
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_requestTimeout),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
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;
|
||||
});
|
||||
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_receivedData),
|
||||
backgroundColor: Colors.green,
|
||||
);
|
||||
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,17 +221,76 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_isLoaded = false;
|
||||
if (isAutoRefresh) {
|
||||
_autoRefreshLastAttemptFailed = true;
|
||||
}
|
||||
_activeTelemetryRequestIsAutoRefresh = false;
|
||||
});
|
||||
if (isAutoRefresh) {
|
||||
_scheduleNextAutoRefreshAttempt();
|
||||
}
|
||||
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
if (!isAutoRefresh) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.telemetry_errorLoading(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
if (selection == null) return;
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user