Merge pull request #42 from wel97459/dev-neighbours

Added Neighbors to the repeater hub and a screen to display the Neighbors
This commit is contained in:
zjs81 2026-01-19 19:17:00 -07:00 committed by GitHub
commit f790604d23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1436 additions and 9 deletions

View file

@ -3,3 +3,4 @@ template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
untranslated-messages-file: untranslated.json

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Сървъри на стая",
"listFilter_unreadOnly": "Само непрочетените",
"listFilter_newGroup": "Нова група",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbours": "Съседи",
"neighbors_receivedData": "Получени данни за съседи",
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
"neighbors_repeatersNeighbours": "Повторители Съседи",
"neighbors_noData": "Няма налични данни за съседи.",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
"channels_createPrivateChannelDesc": "Защитено с таен ключ.",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
"channels_enterHashtag": "Въведете хаштаг",
"channels_hashtagHint": "напр. #отбор",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Слушано преди {time}.",
"neighbors_unknownContact": "Неизвестна {pubkey}",
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
"settings_locationGPSEnable": "Активиране на GPS",
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Raumserver",
"listFilter_unreadOnly": "Nur nicht gelesen",
"listFilter_newGroup": "Neue Gruppe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Nachbarn",
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarendaten",
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
"neighbors_noData": "Keine Nachbardaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
@ -1348,5 +1362,27 @@
"channels_scanQrCode": "Scannen Sie einen QR-Code",
"channels_scanQrCodeComingSoon": "Bald verfügbar",
"channels_enterHashtag": "Gib Hashtag ein",
"channels_hashtagHint": "z.B. #team"
"channels_hashtagHint": "z.B. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hörte: {time} vor her.",
"neighbors_unknownContact": "Unbekannte {pubkey}",
"settings_locationGPSEnable": "GPS aktivieren",
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
"settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.",
"contacts_manageRoom": "Raum-Server verwalten",
"room_management": "Raum-Server-Verwaltung"
}

View file

@ -773,6 +773,8 @@
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Send commands to the repeater",
"repeater_neighbours": "Neighbors",
"repeater_neighboursSubtitle": "View zero hop neighbors.",
"repeater_settings": "Settings",
"repeater_settingsSubtitle": "Configure repeater parameters",
@ -1075,6 +1077,29 @@
"fahrenheit": {"type": "String"}
}
},
"neighbors_receivedData": "Received Neighbours Data",
"neighbors_requestTimedOut": "Neighbours request timed out.",
"neighbors_errorLoading": "Error loading neighbors: {error}",
"@neighbors_errorLoading": {
"placeholders": {
"error": {"type": "String"}
}
},
"neighbors_repeatersNeighbours": "Repeaters Neighbours",
"neighbors_noData": "No neighbours data available.",
"neighbors_unknownContact": "Unknown {pubkey}",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {"type": "String"}
}
},
"neighbors_heardAgo": "Heard: {time} ago",
"@neighbors_heardAgo": {
"placeholders": {
"time": {"type": "String"}
}
},
"channelPath_title": "Packet Path",
"channelPath_viewMap": "View map",
"channelPath_otherObservedPaths": "Other Observed Paths",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Servidores de la sala",
"listFilter_unreadOnly": "Solo sin leer",
"listFilter_newGroup": "Nuevo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vecinos",
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
"neighbors_receivedData": "Recibidas Datos de Vecinos",
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
"neighbors_noData": "No hay datos de vecinos disponibles.",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
"channels_createPrivateChannelDesc": "Cifrado con una clave secreta.",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Próximamente",
"channels_enterHashtag": "Introducir hashtag",
"channels_hashtagHint": "ej. #equipo",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clave pública desconocida {pubkey}",
"neighbors_heardAgo": "Escuchado: {time} hace atrás",
"settings_locationGPSEnable": "Habilitar GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Serveurs de pièce",
"listFilter_unreadOnly": "Messages non lus seulement",
"listFilter_newGroup": "Nouvelle groupe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Voisins",
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
"neighbors_receivedData": "Données des voisins reçues",
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
"channels_createPrivateChannel": "Créer un Canal Privé",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #équipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant",
"settings_locationGPSEnable": "Habilita GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Server della stanza",
"listFilter_unreadOnly": "Solo non letto",
"listFilter_newGroup": "Nuovo gruppo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vicini",
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
"neighbors_receivedData": "Ricevute dati vicini",
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Sentito: {time} fa",
"neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}",
"settings_locationGPSEnable": "Abilita GPS",
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",

View file

@ -2945,6 +2945,18 @@ abstract class AppLocalizations {
/// **'Send commands to the repeater'**
String get repeater_cliSubtitle;
/// No description provided for @repeater_neighbours.
///
/// In en, this message translates to:
/// **'Neighbors'**
String get repeater_neighbours;
/// No description provided for @repeater_neighboursSubtitle.
///
/// In en, this message translates to:
/// **'View zero hop neighbors.'**
String get repeater_neighboursSubtitle;
/// No description provided for @repeater_settings.
///
/// In en, this message translates to:
@ -4084,6 +4096,48 @@ abstract class AppLocalizations {
/// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @neighbors_receivedData.
///
/// In en, this message translates to:
/// **'Received Neighbours Data'**
String get neighbors_receivedData;
/// No description provided for @neighbors_requestTimedOut.
///
/// In en, this message translates to:
/// **'Neighbours request timed out.'**
String get neighbors_requestTimedOut;
/// No description provided for @neighbors_errorLoading.
///
/// In en, this message translates to:
/// **'Error loading neighbors: {error}'**
String neighbors_errorLoading(String error);
/// No description provided for @neighbors_repeatersNeighbours.
///
/// In en, this message translates to:
/// **'Repeaters Neighbours'**
String get neighbors_repeatersNeighbours;
/// No description provided for @neighbors_noData.
///
/// In en, this message translates to:
/// **'No neighbours data available.'**
String get neighbors_noData;
/// No description provided for @neighbors_unknownContact.
///
/// In en, this message translates to:
/// **'Unknown {pubkey}'**
String neighbors_unknownContact(String pubkey);
/// No description provided for @neighbors_heardAgo.
///
/// In en, this message translates to:
/// **'Heard: {time} ago'**
String neighbors_heardAgo(String time);
/// No description provided for @channelPath_title.
///
/// In en, this message translates to:

View file

@ -1630,6 +1630,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
@override
String get repeater_neighbours => 'Съседи';
@override
String get repeater_neighboursSubtitle =>
'Преглед на съседни възли с нулев скок.';
@override
String get repeater_settings => 'Настройки';
@ -2315,6 +2322,33 @@ class AppLocalizationsBg extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Получени данни за съседи';
@override
String get neighbors_requestTimedOut => 'Съседите поискат изтичане на време.';
@override
String neighbors_errorLoading(String error) {
return 'Грешка при зареждане на съседи: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
@override
String get neighbors_noData => 'Няма налични данни за съседи.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Неизвестна $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Слушано преди $time.';
}
@override
String get channelPath_title => 'Пътеки пъзел';

View file

@ -201,18 +201,18 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.';
@override
String get settings_locationGPSEnable => 'GPS Enable';
String get settings_locationGPSEnable => 'GPS aktivieren';
@override
String get settings_locationGPSEnableSubtitle =>
'Enables GPS to automatically update location.';
'Aktiviert GPS zur automatischen Aktualisierung des Standorts.';
@override
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)';
@override
String get settings_locationIntervalInvalid =>
'Interval must be at least 60 seconds, and less than 86400 seconds.';
'Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.';
@override
String get settings_latitude => 'Breitengrad';
@ -662,7 +662,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get contacts_manageRepeater => 'Wiederholungen verwalten';
@override
String get contacts_manageRoom => 'Manage Room Server';
String get contacts_manageRoom => 'Raum-Server verwalten';
@override
String get contacts_roomLogin => 'Raum-Login';
@ -1604,7 +1604,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_management => 'Repeater-Verwaltung';
@override
String get room_management => 'Room Server Management';
String get room_management => 'Raum-Server-Verwaltung';
@override
String get repeater_managementTools => 'Verwaltungs-Tools';
@ -1629,6 +1629,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Sende Befehle an den Repeater';
@override
String get repeater_neighbours => 'Nachbarn';
@override
String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.';
@override
String get repeater_settings => 'Einstellungen';
@ -2318,6 +2324,33 @@ class AppLocalizationsDe extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Empfangene Nachbarendaten';
@override
String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.';
@override
String neighbors_errorLoading(String error) {
return 'Fehler beim Laden der Nachbarn: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Wiederholer Nachbarn';
@override
String get neighbors_noData => 'Keine Nachbardaten verfügbar.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unbekannte $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hörte: $time vor her.';
}
@override
String get channelPath_title => 'Paketpfad';

View file

@ -1604,6 +1604,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Send commands to the repeater';
@override
String get repeater_neighbours => 'Neighbors';
@override
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
@override
String get repeater_settings => 'Settings';
@ -2277,6 +2283,33 @@ class AppLocalizationsEn extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Received Neighbours Data';
@override
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
@override
String neighbors_errorLoading(String error) {
return 'Error loading neighbors: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
@override
String get neighbors_noData => 'No neighbours data available.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unknown $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Heard: $time ago';
}
@override
String get channelPath_title => 'Packet Path';

View file

@ -1628,6 +1628,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
@override
String get repeater_neighbours => 'Vecinos';
@override
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
@override
String get repeater_settings => 'Configuración';
@ -2312,6 +2318,34 @@ class AppLocalizationsEs extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Recibidas Datos de Vecinos';
@override
String get neighbors_requestTimedOut =>
'Los vecinos solicitan que se desconecte.';
@override
String neighbors_errorLoading(String error) {
return 'Error al cargar vecinos: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
@override
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clave pública desconocida $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Escuchado: $time hace atrás';
}
@override
String get channelPath_title => 'Ruta del Paquete';

View file

@ -1634,6 +1634,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
@override
String get repeater_neighbours => 'Voisins';
@override
String get repeater_neighboursSubtitle =>
'Afficher les voisins de saut nuls.';
@override
String get repeater_settings => 'Paramètres';
@ -2326,6 +2333,34 @@ class AppLocalizationsFr extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Données des voisins reçues';
@override
String get neighbors_requestTimedOut => 'Les voisins demandent un délai.';
@override
String neighbors_errorLoading(String error) {
return 'Erreur lors du chargement des voisins : $error';
}
@override
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
@override
String get neighbors_noData =>
'Aucune donnée concernant les voisins disponible.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clé publique inconnue $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Écouté : $time auparavant';
}
@override
String get channelPath_title => 'Chemin de paquet';

View file

@ -1626,6 +1626,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
@override
String get repeater_neighbours => 'Vicini';
@override
String get repeater_neighboursSubtitle =>
'Visualizza vicini di salto pari a zero.';
@override
String get repeater_settings => 'Impostazioni';
@ -2312,6 +2319,33 @@ class AppLocalizationsIt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ricevute dati vicini';
@override
String get neighbors_requestTimedOut => 'I vicini richiedono un timeout.';
@override
String neighbors_errorLoading(String error) {
return 'Errore nel caricamento dei vicini: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
@override
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Chiave pubblica sconosciuta $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Sentito: $time fa';
}
@override
String get channelPath_title => 'Percorso Pacchetto';

View file

@ -1621,6 +1621,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
@override
String get repeater_neighbours => 'Buren';
@override
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
@override
String get repeater_settings => 'Instellingen';
@ -2302,6 +2308,34 @@ class AppLocalizationsNl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ontvangen Buurdata';
@override
String get neighbors_requestTimedOut =>
'Buren vragen om tijdelijk uitgeschakeld.';
@override
String neighbors_errorLoading(String error) {
return 'Fout bij het laden van buren: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Onbekende $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Horen: $time geleden';
}
@override
String get channelPath_title => 'Pakketpad';

View file

@ -1630,6 +1630,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
@override
String get repeater_neighbours => 'Sąsiedzi';
@override
String get repeater_neighboursSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
@override
String get repeater_settings => 'Ustawienia';
@ -2310,6 +2317,34 @@ class AppLocalizationsPl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa';
@override
String get neighbors_requestTimedOut =>
'Sąsiedzi proszą o wyłączenie timingu.';
@override
String neighbors_errorLoading(String error) {
return 'Błąd podczas ładowania sąsiadów: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nieznana $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Usłyszano: $time temu';
}
@override
String get channelPath_title => 'Ścieżka pakietu';

View file

@ -1628,6 +1628,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
@override
String get repeater_neighbours => 'Vizinhos';
@override
String get repeater_neighboursSubtitle =>
'Visualizar vizinhos de salto zero.';
@override
String get repeater_settings => 'Configurações';
@ -2312,6 +2319,34 @@ class AppLocalizationsPt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos';
@override
String get neighbors_requestTimedOut =>
'Vizinhos solicitam tempo limite esgotado.';
@override
String neighbors_errorLoading(String error) {
return 'Erro ao carregar vizinhos: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
@override
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
@override
String neighbors_unknownContact(String pubkey) {
return '$pubkey Desconhecido';
}
@override
String neighbors_heardAgo(String time) {
return 'Ouvido: $time atrás';
}
@override
String get channelPath_title => 'Rótulo de Caminho de Pacote';

View file

@ -1623,6 +1623,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
@override
String get repeater_neighbours => 'Súsezný';
@override
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
@override
String get repeater_settings => 'Nastavenia';
@ -2300,6 +2306,34 @@ class AppLocalizationsSk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Obdielo dáta suseda';
@override
String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.';
@override
String neighbors_errorLoading(String error) {
return 'Chyba pri načítaní susedov: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
@override
String get neighbors_noData =>
'Nie je dostupná žiadna informácia o susedoch.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Neznáma $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Počuli sme to: $time dozadu';
}
@override
String get channelPath_title => 'Cesta balíka';

View file

@ -1624,6 +1624,12 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_cliSubtitle =>
'Pošlji ukazne povelje na ponovitveno enoto.';
@override
String get repeater_neighbours => 'Sosedi';
@override
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
@override
String get repeater_settings => 'Nastavitve';
@ -2305,6 +2311,34 @@ class AppLocalizationsSl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Prejeto podatke o sosedih';
@override
String get neighbors_requestTimedOut =>
'Sosedi zahtevajo izklop po dogovoru.';
@override
String neighbors_errorLoading(String error) {
return 'Napaka pri obnašanju sosedov: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
@override
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nepoznano $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Udeleženec je prejel sporočilo $time nazaj.';
}
@override
String get channelPath_title => 'Pot do paketa';

View file

@ -1612,6 +1612,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
@override
String get repeater_neighbours => 'Grannar';
@override
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
@override
String get repeater_settings => 'Inställningar';
@ -2289,6 +2295,33 @@ class AppLocalizationsSv extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Mottagna grannars data';
@override
String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.';
@override
String neighbors_errorLoading(String error) {
return 'Fel vid inläsning av grannar: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
@override
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Okänd $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hördes: $time sedan';
}
@override
String get channelPath_title => 'Paketväg';

View file

@ -1552,6 +1552,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_cliSubtitle => '发送命令到重复器';
@override
String get repeater_neighbours => '邻居';
@override
String get repeater_neighboursSubtitle => '查看零跳邻居。';
@override
String get repeater_settings => '设置';
@ -2182,6 +2188,33 @@ class AppLocalizationsZh extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => '收到邻居数据';
@override
String get neighbors_requestTimedOut => '邻居请求超时处理。';
@override
String neighbors_errorLoading(String error) {
return '加载邻居时出错:$error';
}
@override
String get neighbors_repeatersNeighbours => '重复器邻居';
@override
String get neighbors_noData => '没有可用的邻居数据。';
@override
String neighbors_unknownContact(String pubkey) {
return '未知$pubkey';
}
@override
String neighbors_heardAgo(String time) {
return '听到的时间:$time前';
}
@override
String get channelPath_title => '数据包路径';

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Roomservers",
"listFilter_unreadOnly": "Alleen ongelezen",
"listFilter_newGroup": "Nieuwe groep",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Buren",
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbours": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
"channels_hashtagHint": "bijv. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Onbekende {pubkey}",
"neighbors_heardAgo": "Horen: {time} geleden",
"settings_locationGPSEnable": "GPS inschakelen",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Serwery pokoju",
"listFilter_unreadOnly": "Tylko nieprzeczytane",
"listFilter_newGroup": "Nowa grupa",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Sąsiedzi",
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
"channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Wkrótce",
"channels_enterHashtag": "Wprowadź hashtag",
"channels_hashtagHint": "np. #zespół",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Usłyszano: {time} temu",
"neighbors_unknownContact": "Nieznana {pubkey}",
"settings_locationGPSEnable": "Włącz GPS",
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Servidores de sala",
"listFilter_unreadOnly": "Apenas não lido",
"listFilter_newGroup": "Novo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vizinhos",
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
"channels_createPrivateChannel": "Criar um Canal Privado",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Em breve",
"channels_enterHashtag": "Insira hashtag",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Ouvido: {time} atrás",
"neighbors_unknownContact": "{pubkey} Desconhecido",
"settings_locationGPSEnable": "Ativar GPS",
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Servéry miestnosti",
"listFilter_unreadOnly": "Nezaregistrované len",
"listFilter_newGroup": "Nová skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
"neighbors_receivedData": "Obdielo dáta suseda",
"repeater_neighbours": "Súsezný",
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
"channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Čoskoro",
"channels_enterHashtag": "Zadajte hashtag",
"channels_hashtagHint": "napr. #tím",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Počuli sme to: {time} dozadu",
"neighbors_unknownContact": "Neznáma {pubkey}",
"settings_locationGPSEnable": "Aktivovať GPS",
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Smeti za prostore",
"listFilter_unreadOnly": "Nezbrani samo",
"listFilter_newGroup": "Nova skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbours": "Sosedi",
"neighbors_receivedData": "Prejeto podatke o sosedih",
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
"neighbors_noData": "Niso na voljo podatki o sosedih.",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
"channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Prihajajoča",
"channels_enterHashtag": "Vnesite hashtag",
"channels_hashtagHint": "npr. #ekipa",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Nepoznano {pubkey}",
"neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.",
"settings_locationGPSEnable": "Omogoči GPS",
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "Rumservrar",
"listFilter_unreadOnly": "Endast oinlästa",
"listFilter_newGroup": "Ny grupp",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Grannar",
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
"neighbors_receivedData": "Mottagna grannars data",
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
"neighbors_repeatersNeighbours": "Upprepar grannar",
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
"channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "Kommer snart",
"channels_enterHashtag": "Ange hashtag",
"channels_hashtagHint": "t.ex. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hördes: {time} sedan",
"neighbors_unknownContact": "Okänd {pubkey}",
"settings_locationGPSEnable": "Aktivera GPS",
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",

View file

@ -1337,6 +1337,20 @@
"listFilter_roomServers": "房间服务器",
"listFilter_unreadOnly": "未读消息",
"listFilter_newGroup": "新组",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "查看零跳邻居。",
"repeater_neighbours": "邻居",
"neighbors_receivedData": "收到邻居数据",
"neighbors_requestTimedOut": "邻居请求超时处理。",
"neighbors_errorLoading": "加载邻居时出错:{error}",
"neighbors_repeatersNeighbours": "重复器邻居",
"neighbors_noData": "没有可用的邻居数据。",
"channels_joinPrivateChannel": "加入私密频道",
"channels_createPrivateChannelDesc": "使用密钥保护。",
"channels_joinPrivateChannelDesc": "手动输入密钥。",
@ -1349,6 +1363,22 @@
"channels_scanQrCodeComingSoon": "即将到来",
"channels_enterHashtag": "输入标签",
"channels_hashtagHint": "例如 #团队",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "听到的时间:{time}前",
"neighbors_unknownContact": "未知{pubkey}",
"settings_locationGPSEnable": "启用GPS",
"settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。",
"settings_locationIntervalSec": "GPS 间隔(秒)",

View file

@ -0,0 +1,456 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
class NeighboursScreen extends StatefulWidget {
final Contact repeater;
final String password;
const NeighboursScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<NeighboursScreen> createState() => _NeighboursScreenState();
}
class _NeighboursScreenState extends State<NeighboursScreen> {
static const int _reqNeighboursKeyLen = 4;
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _neighbourCount = 0;
bool _isLoading = false;
bool _isLoaded = false;
bool _hasData = false;
Timer? _statusTimeout;
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbours;
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadNeighbours();
_hasData = false;
}
void _setupMessageListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleNeighboursResponse(connector, frame.sublist(6));
}
});
}
String fmtDuration(double seconds) {
if (seconds < 60) {
return '${seconds.toStringAsFixed(1)}s';
}
final int m = (seconds ~/ 60).toInt();
final double s = seconds - (60 * m);
if (m < 60) {
return '${m}m ${s.toStringAsFixed(0)}s';
}
final int h = m ~/ 60;
final int m2 = m % 60;
return '${h}h ${m2}m';
}
static List<Map<String, dynamic>> parseNeighboursData(
BufferReader buffer,
int resultsCount,
) {
final Map<int, Map<String, dynamic>> neighbours = {};
for (var i = 0; i < resultsCount; i++) {
final neighbourData = neighbours.putIfAbsent(
i,
() => {
'contact': null,
'publicKey': <Uint8List>{},
'lastHeard': <int>{},
'snr': <double>{},
},
);
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
neighbourData['lastHeard'] = buffer.readUInt32LE();
neighbourData['snr'] = buffer.readInt8() / 4.0;
}
return neighbours.values.toList();
}
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final neighbourCount = buffer.readUInt16LE();
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighbourData in parsedNeighbours) {
final publicKey = neighbourData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
publicKey,
)) {
neighbourData['contact'] = repeater;
}
}
});
setState(() {
_parsedNeighbours = parsedNeighbours;
_neighbourCount = neighbourCount;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
);
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = true;
_hasData = true;
});
}
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadNeighbours() async {
if (_commandService == null) return;
setState(() {
_isLoading = true;
_isLoaded = false;
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
final selection = await connector.preparePathForContactSend(repeater);
_pendingStatusSelection = selection;
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([
reqTypeGetNeighbours,
0x00,
0x0F,
0x00,
0x00,
0x00,
_reqNeighboursKeyLen,
]),
);
await connector.sendFrame(frame);
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
final messageBytes = frame.length >= _statusResponseBytes
? frame.length
: _statusResponseBytes;
final timeoutMs = connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: messageBytes,
);
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
),
);
_recordStatusResult(false);
});
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
void _recordStatusResult(bool success) {
final selection = _pendingStatusSelection;
if (selection == null) return;
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
connector.recordRepeaterPathResult(repeater, selection, success, null);
_pendingStatusSelection = null;
}
@override
void dispose() {
_frameSubscription?.cancel();
_commandService?.dispose();
_statusTimeout?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.neighbors_repeatersNeighbours,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
repeater.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadNeighbours,
tooltip: l10n.repeater_refresh,
),
],
),
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadNeighbours,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbours == null ||
_parsedNeighbours!.isEmpty))
_buildNeighboursInfoCard(
"${l10n.repeater_neighbours} - $_neighbourCount",
),
],
),
),
),
);
}
Widget _buildNeighboursInfoCard(String title) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
for (final entry in _parsedNeighbours!.asMap().entries)
_buildInfoRow(
entry.value['contact'] != null
? entry.value['contact'].name
: context.l10n.neighbors_unknownContact(
"<${pubKeyToHex(entry.value['publicKey'])}>",
),
context.l10n.neighbors_heardAgo(
fmtDuration(entry.value['lastHeard'] + 0.0),
),
entry.value['snr'],
connector.currentSf!,
),
],
),
),
);
}
Widget _buildInfoRow(
String label,
String value,
double snr,
int spreadingFactor,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(value),
trailing: SNRIcon(
snr: snr,
snrLevels: getSNRfromSF(spreadingFactor),
),
),
),
],
),
);
}
}

View file

@ -6,6 +6,7 @@ import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart';
import 'repeater_settings_screen.dart';
import 'telemetry_screen.dart';
import 'neighbours_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
@ -169,13 +170,33 @@ class RepeaterHubScreen extends StatelessWidget {
},
),
const SizedBox(height: 12),
// Neighbors button
_buildManagementCard(
context,
icon: Icons.group,
title: l10n.repeater_neighbours,
subtitle: l10n.repeater_neighboursSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NeighboursScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.orange,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,

View file

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
List<double> getSNRfromSF(int spreadingFactor) {
switch (spreadingFactor) {
case 7:
return [4.0, -2.0, -4.0, -6.0];
case 8:
return [4.0, -4.0, -6.0, -8.0];
case 9:
return [4.0, -6.0, -8.0, -10.0];
case 10:
return [4.0, -8.0, -10.0, -13.0];
case 11:
return [4.0, -10.0, -12.5, -15.0];
case 12:
return [4.0, -12.5, -15.0, -18.0];
default:
return []; // Or throw Exception('Invalid SF: $spreadingFactor');
}
}
class SNRIcon extends StatelessWidget {
final double snr;
final List<double> snrLevels;
const SNRIcon({
super.key,
required this.snr,
this.snrLevels = const [4.0, -2.0, -4.0, -6.0],
});
@override
Widget build(BuildContext context) {
IconData icon;
Color color;
if (snr >= snrLevels[0]) {
icon = Icons.signal_cellular_alt;
color = Colors.green;
} else if (snr >= snrLevels[1]) {
icon = Icons.signal_cellular_alt;
color = Colors.lightGreen;
} else if (snr >= snrLevels[2]) {
icon = Icons.signal_cellular_alt;
color = Colors.yellow;
} else if (snr >= snrLevels[3]) {
icon = Icons.signal_cellular_alt_2_bar;
color = Colors.orange;
} else {
icon = Icons.signal_cellular_alt_1_bar;
color = Colors.red;
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Text('$snr dB', style: TextStyle(fontSize: 10, color: color)),
],
);
}
}

View file

@ -891,4 +891,4 @@ def translate_locale(
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())

1
untranslated.json Normal file
View file

@ -0,0 +1 @@
{}