diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 4c747da..c85db5f 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Повторители", "listFilter_roomServers": "Сървъри на стая", "listFilter_unreadOnly": "Само непрочетените", - "listFilter_newGroup": "Нова група" + "listFilter_newGroup": "Нова група", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.", + "repeater_neighbours": "Съседи", + "neighbors_ReceivedData": "Получени данни за съседи", + "neighbors_RequestTimedOut": "Съседите поискат изтичане на време.", + "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", + "neighbors_repeatersNeighbours": "Повторители Съседи", + "neighbors_noData": "Няма налични данни за съседи." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9044962..3d58892 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Wiederholer", "listFilter_roomServers": "Raumserver", "listFilter_unreadOnly": "Nur nicht gelesen", - "listFilter_newGroup": "Neue Gruppe" + "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." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dc53f73..608af2d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -753,6 +753,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", @@ -1055,6 +1057,18 @@ "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.", + "channelPath_title": "Packet Path", "channelPath_viewMap": "View map", "channelPath_otherObservedPaths": "Other Observed Paths", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1515eb6..b3f188f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de la sala", "listFilter_unreadOnly": "Solo sin leer", - "listFilter_newGroup": "Nuevo grupo" + "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." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4b8af01..566bf1b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Répéteurs", "listFilter_roomServers": "Serveurs de pièce", "listFilter_unreadOnly": "Messages non lus seulement", - "listFilter_newGroup": "Nouvelle groupe" + "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." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 72be091..731be02 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Ripetitori", "listFilter_roomServers": "Server della stanza", "listFilter_unreadOnly": "Solo non letto", - "listFilter_newGroup": "Nuovo gruppo" + "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." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a2c7ddb..6e3d895 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2831,6 +2831,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: @@ -3970,6 +3982,36 @@ 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 @channelPath_title. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 5def822..3134001 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1567,6 +1567,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Настройки'; @@ -2252,6 +2258,23 @@ class AppLocalizationsBg 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 get channelPath_title => 'Пътеки пъзел'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 591de39..963debe 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1565,6 +1565,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliSubtitle => 'Sende Befehle an den Repeater'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Einstellungen'; @@ -2254,6 +2260,23 @@ class AppLocalizationsDe 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 get channelPath_title => 'Paketpfad'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9e04923..17684e0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1543,6 +1543,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'; @@ -2216,6 +2222,23 @@ 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 get channelPath_title => 'Packet Path'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 82259cf..0b7356b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1564,6 +1564,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliSubtitle => 'Enviar comandos al repetidor'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Configuración'; @@ -2248,6 +2254,23 @@ class AppLocalizationsEs 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 get channelPath_title => 'Ruta del Paquete'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index acdb5b0..b26c81d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1570,6 +1570,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Paramètres'; @@ -2261,6 +2267,23 @@ class AppLocalizationsFr 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 get channelPath_title => 'Chemin de paquet'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index cd2d022..cebdda4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1562,6 +1562,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliSubtitle => 'Invia comandi al ripetitore'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Impostazioni'; @@ -2248,6 +2254,23 @@ class AppLocalizationsIt 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 get channelPath_title => 'Percorso Pacchetto'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3484c0a..f9296ed 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1558,6 +1558,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliSubtitle => 'Verzend commando\'s naar de herhaaldere'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Instellingen'; @@ -2241,6 +2247,23 @@ class AppLocalizationsNl 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 get channelPath_title => 'Pakketpad'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 180d8e2..d77596f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1566,6 +1566,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Ustawienia'; @@ -2246,6 +2252,23 @@ class AppLocalizationsPl 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 get channelPath_title => 'Ścieżka pakietu'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 403c4d6..803f7e7 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1564,6 +1564,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliSubtitle => 'Enviar comandos ao repetidor'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Configurações'; @@ -2248,6 +2254,23 @@ class AppLocalizationsPt 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 get channelPath_title => 'Rótulo de Caminho de Pacote'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index e9eb10c..ec71b6e 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1560,6 +1560,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Nastavenia'; @@ -2237,6 +2243,23 @@ class AppLocalizationsSk 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 get channelPath_title => 'Cesta balíka'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f95797b..eb73d01 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1561,6 +1561,12 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliSubtitle => 'Pošlji ukazne povelje na ponovitveno enoto.'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Nastavitve'; @@ -2242,6 +2248,23 @@ class AppLocalizationsSl 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 get channelPath_title => 'Pot do paketa'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6f97659..88679d7 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1548,6 +1548,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Inställningar'; @@ -2225,6 +2231,23 @@ class AppLocalizationsSv 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 get channelPath_title => 'Paketväg'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index e5b5a9f..9778d8f 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1495,6 +1495,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_cliSubtitle => '发送命令到重复器'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => '设置'; @@ -2125,6 +2131,23 @@ class AppLocalizationsZh 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 get channelPath_title => '数据包路径'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index da33f11..375ca35 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Herhalingen", "listFilter_roomServers": "Kamervirtualisatie", "listFilter_unreadOnly": "Alleen ongelezen", - "listFilter_newGroup": "Nieuwe groep" + "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." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index bab67b5..b900ee9 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Powtarzacze", "listFilter_roomServers": "Serwery pokoju", "listFilter_unreadOnly": "Tylko nieprzeczytane", - "listFilter_newGroup": "Nowa grupa" + "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." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 59f4a47..c25cedd 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de sala", "listFilter_unreadOnly": "Apenas não lido", - "listFilter_newGroup": "Novo grupo" + "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." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 67043a7..6b75982 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Opakovadlá", "listFilter_roomServers": "Servéry miestnosti", "listFilter_unreadOnly": "Nezaregistrované len", - "listFilter_newGroup": "Nová skupina" + "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." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 8a8dc59..8a6f49d 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Ponovitve", "listFilter_roomServers": "Smeti za prostore", "listFilter_unreadOnly": "Nezbrani samo", - "listFilter_newGroup": "Nova skupina" + "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." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 866e438..acf8d9c 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Upprepare", "listFilter_roomServers": "Rumservrar", "listFilter_unreadOnly": "Endast oinlästa", - "listFilter_newGroup": "Ny grupp" + "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." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f1349e8..20fb6e7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "重复器", "listFilter_roomServers": "房间服务器", "listFilter_unreadOnly": "未读消息", - "listFilter_newGroup": "新组" + "listFilter_newGroup": "新组", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighboursSubtitle": "查看零跳邻居。", + "repeater_neighbours": "邻居", + "neighbors_ReceivedData": "收到邻居数据", + "neighbors_RequestTimedOut": "邻居请求超时处理。", + "neighbors_errorLoading": "加载邻居时出错:{error}", + "neighbors_repeatersNeighbours": "重复器邻居", + "neighbors_noData": "没有可用的邻居数据。" } diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbours_screen.dart new file mode 100644 index 0000000..b31295c --- /dev/null +++ b/lib/screens/neighbours_screen.dart @@ -0,0 +1,456 @@ +import 'dart:async'; +import 'dart:math'; +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 createState() => _NeighboursScreenState(); +} + +class _NeighboursScreenState extends State { + 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 _timeEstment = 0; + int _neighbourCount = 0; + + bool _isLoading = false; + bool _isLoaded = false; + bool _hasData = false; + Timer? _statusTimeout; + StreamSubscription? _frameSubscription; + RepeaterCommandService? _commandService; + PathSelection? _pendingStatusSelection; + List>? _parsedNeighbours; + + @override + void initState() { + super.initState(); + final connector = Provider.of(context, listen: false); + _commandService = RepeaterCommandService(connector); + _setupMessageListener(); + _loadNeighbours(); + _hasData = false; + } + + void _setupMessageListener() { + final connector = Provider.of(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(context, 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> parseNeighboursData( + BufferReader buffer, + int resultsCount, + ) { + final Map> neighbours = {}; + for (var i = 0; i < resultsCount; i++) { + final neighbourData = neighbours.putIfAbsent( + i, + () => { + 'contact': null, + 'publicKey': {}, + 'lastHeard': {}, + 'snr': {}, + }, + ); + neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen); + neighbourData['lastHeard'] = buffer.readUInt32LE(); + neighbourData['snr'] = buffer.readInt8() / 4.0; + } + + return neighbours.values.toList(); + } + + void _handleNeighboursResponse( + BuildContext context, + 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, + ) { + parsedNeighbours.forEach((neighbourData) { + 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 _loadNeighbours() async { + if (_commandService == null) return; + + setState(() { + _isLoading = true; + _isLoaded = false; + }); + try { + final connector = Provider.of(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(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(); + 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( + 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), + ], + ), + ), + ), + ); + } + + Widget _buildNeighboursInfoCard(String title) { + final connector = Provider.of(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 + : 'Unknown (${pubKeyToHex(entry.value['publicKey'])})', + 'Heard: ${fmtDuration(entry.value['lastHeard'] + 0.0)} ago', + 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), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 88a58fa..a919c59 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -5,6 +5,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; @@ -145,12 +146,32 @@ class RepeaterHubScreen extends StatelessWidget { ), const SizedBox(height: 12), // Settings 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, diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart new file mode 100644 index 0000000..da68a65 --- /dev/null +++ b/lib/widgets/snr_indicator.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +List 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 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)), + ], + ); + } +}