diff --git a/l10n.yaml b/l10n.yaml index a4d323c..b89567e 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -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 diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 377e0f5..ca8e338 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -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.", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ba8f4d0..bd9faad 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cc47077..4850169 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 571bc65..eb8474d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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)", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d7f4b22..769c189 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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)", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f2f5d28..0c63a16 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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)", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 95e6935..07b721b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -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: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 7cbb2d6..4e6a190 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -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 => 'Пътеки пъзел'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 5107d23..0e51676 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -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'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index abb776a..c87fbc6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -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'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8ab2c19..404aeb0 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -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'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 224f6a1..2e06d80 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -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'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b034425..ebd3f57 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -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'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a938b50..3a1afcb 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index adb1f62..bab42cd 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 6bea4b7..f0f4c21 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 0c26184..4fa1f0b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index c33c9d6..e918f4f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -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'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index d876dd9..ed11747 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -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'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index f1dd506..aa40f26 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -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 => '数据包路径'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 9131b4e..9c39f66 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -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)", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 81d4df0..a3aebdc 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -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)", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6f6471f..e30495a 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -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.", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 7b23791..c4d07f7 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -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)", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index e07b1f2..4667eac 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -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)", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 020e8b2..d8a294d 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -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)", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 52973fa..8da91c8 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -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 间隔(秒)", diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbours_screen.dart new file mode 100644 index 0000000..b606188 --- /dev/null +++ b/lib/screens/neighbours_screen.dart @@ -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 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 _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(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(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 _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} - $_neighbourCount", + ), + ], + ), + ), + ), + ); + } + + 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 + : 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), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 5089f59..5a545f3 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -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, 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)), + ], + ); + } +} diff --git a/tools/translate.py b/tools/translate.py index 8a82100..06a95f2 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -891,4 +891,4 @@ def translate_locale( if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file diff --git a/untranslated.json b/untranslated.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/untranslated.json @@ -0,0 +1 @@ +{} \ No newline at end of file