From fe23e9f7a00f365232cd98e91c413dc6d3f6ead0 Mon Sep 17 00:00:00 2001 From: 446564 Date: Mon, 9 Feb 2026 05:36:25 -0800 Subject: [PATCH 01/99] add support for whipseros needed a new ble prefix --- lib/connector/meshcore_connector.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2c56c373..2bb0a17b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -658,7 +658,8 @@ class MeshCoreConnector extends ChangeNotifier { _scanResults.clear(); for (var result in results) { if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-")) { + result.advertisementData.advName.startsWith("MeshCore-") || + result.advertisementData.advName.startsWith("Whisper-")) { _scanResults.add(result); } } From 04021a39a1d477c1c330f0a01e538eaf6544705a Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:12:51 +0100 Subject: [PATCH 02/99] Better french translations --- lib/l10n/app_fr.arb | 72 ++++++++-------- lib/l10n/app_localizations_fr.dart | 73 ++++++++-------- untranslated.json | 128 +---------------------------- 3 files changed, 72 insertions(+), 201 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 044b8060..08275768 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -210,8 +210,8 @@ "appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)", "appSettings_batteryLipo": "LiPo (3,0-4,2V)", "appSettings_mapDisplay": "Affichage de la carte", - "appSettings_showRepeaters": "Afficher les répétiteurs", - "appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte", + "appSettings_showRepeaters": "Afficher les répéteurs", + "appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte", "appSettings_showChatNodes": "Afficher les nœuds de discussion", "appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte", "appSettings_showOtherNodes": "Afficher d'autres nœuds", @@ -266,7 +266,7 @@ } } }, - "contacts_manageRepeater": "Gérer le répétiteur", + "contacts_manageRepeater": "Gérer le répéteur", "contacts_roomLogin": "Connexion Salle", "contacts_openChat": "Ouverture du Chat", "contacts_editGroup": "Modifier le groupe", @@ -542,9 +542,9 @@ "chat_forceFloodMode": "Mode tout le réseau forcé", "chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :", "chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.", - "chat_hopSingular": "Sautez", - "chat_hopPlural": "sautez", - "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", + "chat_hopSingular": "saut", + "chat_hopPlural": "sauts", + "chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -636,7 +636,7 @@ } }, "map_chat": "Chat", - "map_repeater": "Répétiteur", + "map_repeater": "Répéteur", "map_room": "Salle", "map_sensor": "Capteur", "map_pinDm": "Clé (DM)", @@ -677,7 +677,7 @@ "map_lastSeenTime": "Dernière fois vu", "map_sharedPin": "Clé partagée", "map_joinRoom": "Rejoindre la salle", - "map_manageRepeater": "Gérer le répétiteur", + "map_manageRepeater": "Gérer le répéteur", "mapCache_title": "Cache de Carte Hors Ligne", "mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier", "mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.", @@ -800,13 +800,13 @@ "time_allTime": "Tout le temps", "dialog_disconnect": "Déconnecter", "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", - "login_repeaterLogin": "Connexion au répétiteur", + "login_repeaterLogin": "Connexion au répéteur", "login_roomLogin": "Connexion Salle", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", "login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.", - "login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.", + "login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.", "login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.", "login_routing": "Redirection", "login_routingMode": "Mode de routage", @@ -871,17 +871,17 @@ }, "path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.", "path_setPath": "Définir le chemin", - "repeater_management": "Gestion des répétiteurs", + "repeater_management": "Gestion des répéteurs", "repeater_managementTools": "Outils de Gestion", "repeater_status": "État", - "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur", + "repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur", "repeater_telemetry": "Télémetrie", "repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système", "repeater_cli": "CLI", - "repeater_cliSubtitle": "Envoyer des commandes au répétiteur", + "repeater_cliSubtitle": "Envoyer des commandes au répéteur", "repeater_settings": "Paramètres", - "repeater_settingsSubtitle": "Configurer les paramètres du répétiteur", - "repeater_statusTitle": "État du répétiteur", + "repeater_settingsSubtitle": "Configurer les paramètres du répéteur", + "repeater_statusTitle": "État du répéteur", "repeater_routingMode": "Mode de routage", "repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)", "repeater_forceFloodMode": "Mode tout le réseau forcé", @@ -976,10 +976,10 @@ } } }, - "repeater_settingsTitle": "Paramètres du répétiteur", + "repeater_settingsTitle": "Paramètres du répéteur", "repeater_basicSettings": "Paramètres de base", - "repeater_repeaterName": "Nom du répétiteur", - "repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur", + "repeater_repeaterName": "Nom du répéteur", + "repeater_repeaterNameHelper": "Afficher le nom de ce répéteur", "repeater_adminPassword": "Mot de passe Administrateur", "repeater_adminPasswordHelper": "Mot de passe d'accès complet", "repeater_guestPassword": "Mot de passe invité", @@ -999,7 +999,7 @@ "repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)", "repeater_features": "Fonctionnalités", "repeater_packetForwarding": "Transfert de paquets", - "repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets", + "repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets", "repeater_guestAccess": "Accès Invité", "repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule", "repeater_privacyMode": "Mode de confidentialité", @@ -1026,14 +1026,14 @@ "repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées", "repeater_dangerZone": "Zone dangereuse", "repeater_rebootRepeater": "Redémarrer Répéteur", - "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur", - "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?", + "repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur", + "repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?", "repeater_regenerateIdentityKey": "Ré générer la clé d'identité", "repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée", - "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?", + "repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?", "repeater_eraseFileSystem": "Supprimer le système de fichiers", - "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur", - "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !", + "repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur", + "repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !", "repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.", "repeater_commandSent": "Commande envoyée : {command}", "@repeater_commandSent": { @@ -1085,7 +1085,7 @@ } } }, - "repeater_cliTitle": "Répétiteur CLI", + "repeater_cliTitle": "Répéteur CLI", "repeater_debugNextCommand": "Déboguer Prochaine Commande", "repeater_commandHelp": "Aide", "repeater_clearHistory": "Effacer l'historique", @@ -1119,13 +1119,13 @@ "repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.", "repeater_cliHelpSetAf": "Définit le facteur de temps d'air.", "repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).", - "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.", + "repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.", "repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)", "repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).", "repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.", "repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.", "repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».", - "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.", + "repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.", "repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.", "repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")", "repeater_cliHelpSetName": "Définit le nom de l'annonce.", @@ -1147,7 +1147,7 @@ "repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.", "repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.", "repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.", - "repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", + "repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4", "repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.", "repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).", "repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.", @@ -1171,8 +1171,8 @@ "repeater_settingsCategory": "Paramètres", "repeater_bridge": "Pont", "repeater_logging": "Journalisation", - "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)", - "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)", + "repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)", + "repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)", "repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.", "repeater_gpsManagement": "Gestion GPS", "repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.", @@ -1241,7 +1241,7 @@ "channelPath_title": "Chemin de paquet", "channelPath_viewMap": "Afficher la carte", "channelPath_otherObservedPaths": "Autres chemins observés", - "channelPath_repeaterHops": "Sauts du répétiteur", + "channelPath_repeaterHops": "Sauts du répéteur", "channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.", "channelPath_messageDetails": "Détails du message", "channelPath_senderLabel": "Expéditeur", @@ -1306,7 +1306,7 @@ } }, "channelPath_mapTitle": "Carte du chemin", - "channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.", + "channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.", "channelPath_primaryPath": "Chemin {index} (Principal)", "@channelPath_primaryPath": { "placeholders": { @@ -1558,9 +1558,9 @@ "appSettings_languageRu": "Russe", "contacts_clipboardEmpty": "Le presse-papiers est vide.", "contacts_contactImported": "Le contact a été importé.", - "contacts_floodAdvert": "Annonce de crue", + "contacts_floodAdvert": "Annonce à tout le réseau", "contacts_contactImportFailed": "Échec de l'importation du contact.", - "contacts_zeroHopAdvert": "Annonce Zero Hop", + "contacts_zeroHopAdvert": "Annonce Zero saut", "contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers", "contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers", "contacts_ShareContact": "Copier le contact dans le presse-papiers", @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}", "notification_newTypeDiscovered": "Nouveau {contactType} découvert", "notification_receivedNewMessage": "Nouveau message reçu", - "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.", "settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX", "settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.", "settings_gpxExportNoContacts": "Aucun contact à exporter.", @@ -1591,6 +1590,5 @@ "settings_gpxExportAllContacts": "Tous les emplacements des contacts", "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", - "pathTrace_someHopsNoLocation": "Une ou plusieurs des houblons manquent d'une localisation !" - + "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !" } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 474d528d..eb6d50f5 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -560,11 +560,11 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_mapDisplay => 'Affichage de la carte'; @override - String get appSettings_showRepeaters => 'Afficher les répétiteurs'; + String get appSettings_showRepeaters => 'Afficher les répéteurs'; @override String get appSettings_showRepeatersSubtitle => - 'Afficher les nœuds répétiteurs sur la carte'; + 'Afficher les nœuds répéteurs sur la carte'; @override String get appSettings_showChatNodes => 'Afficher les nœuds de discussion'; @@ -671,7 +671,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get contacts_manageRepeater => 'Gérer le répétiteur'; + String get contacts_manageRepeater => 'Gérer le répéteur'; @override String get contacts_manageRoom => 'Gérer le Room Server'; @@ -1094,18 +1094,18 @@ class AppLocalizationsFr extends AppLocalizations { 'L\'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.'; @override - String get chat_hopSingular => 'Sautez'; + String get chat_hopSingular => 'saut'; @override - String get chat_hopPlural => 'sautez'; + String get chat_hopPlural => 'sauts'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: 'hops', - one: 'hop', + other: 'sauts', + one: 'saut', ); return '$count $_temp0'; } @@ -1259,7 +1259,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_chat => 'Chat'; @override - String get map_repeater => 'Répétiteur'; + String get map_repeater => 'Répéteur'; @override String get map_room => 'Salle'; @@ -1365,7 +1365,7 @@ class AppLocalizationsFr extends AppLocalizations { String get map_joinRoom => 'Rejoindre la salle'; @override - String get map_manageRepeater => 'Gérer le répétiteur'; + String get map_manageRepeater => 'Gérer le répéteur'; @override String get mapCache_title => 'Cache de Carte Hors Ligne'; @@ -1509,7 +1509,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?'; @override - String get login_repeaterLogin => 'Connexion au répétiteur'; + String get login_repeaterLogin => 'Connexion au répéteur'; @override String get login_roomLogin => 'Connexion Salle'; @@ -1529,7 +1529,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get login_repeaterDescription => - 'Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l\'état.'; + 'Entrez le mot de passe du répéteur pour accéder aux paramètres et à l\'état.'; @override String get login_roomDescription => @@ -1634,7 +1634,7 @@ class AppLocalizationsFr extends AppLocalizations { String get path_setPath => 'Définir le chemin'; @override - String get repeater_management => 'Gestion des répétiteurs'; + String get repeater_management => 'Gestion des répéteurs'; @override String get room_management => 'Administración del Servidor de Habitación'; @@ -1647,7 +1647,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_statusSubtitle => - 'Afficher l\'état, les statistiques et les voisins du répétiteur'; + 'Afficher l\'état, les statistiques et les voisins du répéteur'; @override String get repeater_telemetry => 'Télémetrie'; @@ -1660,7 +1660,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cli => 'CLI'; @override - String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur'; + String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; @override String get repeater_neighbours => 'Voisins'; @@ -1674,10 +1674,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_settingsSubtitle => - 'Configurer les paramètres du répétiteur'; + 'Configurer les paramètres du répéteur'; @override - String get repeater_statusTitle => 'État du répétiteur'; + String get repeater_statusTitle => 'État du répéteur'; @override String get repeater_routingMode => 'Mode de routage'; @@ -1783,16 +1783,16 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_settingsTitle => 'Paramètres du répétiteur'; + String get repeater_settingsTitle => 'Paramètres du répéteur'; @override String get repeater_basicSettings => 'Paramètres de base'; @override - String get repeater_repeaterName => 'Nom du répétiteur'; + String get repeater_repeaterName => 'Nom du répéteur'; @override - String get repeater_repeaterNameHelper => 'Afficher le nom de ce répétiteur'; + String get repeater_repeaterNameHelper => 'Afficher le nom de ce répéteur'; @override String get repeater_adminPassword => 'Mot de passe Administrateur'; @@ -1856,7 +1856,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_packetForwardingSubtitle => - 'Activer le répétiteur pour transmettre des paquets'; + 'Activer le répéteur pour transmettre des paquets'; @override String get repeater_guestAccess => 'Accès Invité'; @@ -1905,11 +1905,11 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_rebootRepeaterSubtitle => - 'Réinitialiser l\'appareil répétiteur'; + 'Réinitialiser l\'appareil répéteur'; @override String get repeater_rebootRepeaterConfirm => - 'Êtes-vous sûr de vouloir redémarrer ce répétiteur ?'; + 'Êtes-vous sûr de vouloir redémarrer ce répéteur ?'; @override String get repeater_regenerateIdentityKey => 'Ré générer la clé d\'identité'; @@ -1920,18 +1920,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_regenerateIdentityKeyConfirm => - 'Cela générera une nouvelle identité pour le répétiteur. Continuer ?'; + 'Cela générera une nouvelle identité pour le répéteur. Continuer ?'; @override String get repeater_eraseFileSystem => 'Supprimer le système de fichiers'; @override String get repeater_eraseFileSystemSubtitle => - 'Formater le système de fichiers du répétiteur'; + 'Formater le système de fichiers du répéteur'; @override String get repeater_eraseFileSystemConfirm => - 'AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !'; + 'AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !'; @override String get repeater_eraseSerialOnly => @@ -1999,7 +1999,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get repeater_cliTitle => 'Répétiteur CLI'; + String get repeater_cliTitle => 'Répéteur CLI'; @override String get repeater_debugNextCommand => 'Déboguer Prochaine Commande'; @@ -2091,7 +2091,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpSetRepeat => - 'Active ou désactive le rôle du répétiteur pour ce nœud.'; + 'Active ou désactive le rôle du répéteur pour ce nœud.'; @override String get repeater_cliHelpSetAllowReadOnly => @@ -2115,7 +2115,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpSetAdvertInterval => - 'Définit l\'intervalle du minuteur pour envoyer un paquet d\'annonce local (sans relais). Définir sur 0 pour désactiver.'; + 'Définit l\'intervalle entre chaque émission d\'une annonce locale (sans relais). Définir sur 0 pour désactiver.'; @override String get repeater_cliHelpSetFloodAdvertInterval => @@ -2201,7 +2201,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliHelpNeighbors => - 'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; + 'Affiche une liste d\'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4'; @override String get repeater_cliHelpNeighborRemove => @@ -2289,12 +2289,11 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_logging => 'Journalisation'; @override - String get repeater_neighborsRepeaterOnly => - 'Voisins (Uniquement répétiteur)'; + String get repeater_neighborsRepeaterOnly => 'Voisins (Uniquement répéteur)'; @override String get repeater_regionManagementRepeaterOnly => - 'Gestion des régions (uniquement pour le répétiteur)'; + 'Gestion des régions (uniquement pour le répéteur)'; @override String get repeater_regionNote => @@ -2399,7 +2398,7 @@ class AppLocalizationsFr extends AppLocalizations { String get channelPath_otherObservedPaths => 'Autres chemins observés'; @override - String get channelPath_repeaterHops => 'Sauts du répétiteur'; + String get channelPath_repeaterHops => 'Sauts du répéteur'; @override String get channelPath_noHopDetails => @@ -2467,7 +2466,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channelPath_noRepeaterLocations => - 'Aucune position de répétiteur disponible pour ce chemin.'; + 'Aucune position de répéteur disponible pour ce chemin.'; @override String channelPath_primaryPath(int index) { @@ -2713,7 +2712,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => - 'Une ou plusieurs des houblons manquent d\'une localisation !'; + 'Un ou plusieurs des sauts manquent d\'une localisation !'; @override String get contacts_pathTrace => 'Traçage de chemin'; @@ -2756,10 +2755,10 @@ class AppLocalizationsFr extends AppLocalizations { 'Échec de l\'importation du contact.'; @override - String get contacts_zeroHopAdvert => 'Annonce Zero Hop'; + String get contacts_zeroHopAdvert => 'Annonce Zero saut'; @override - String get contacts_floodAdvert => 'Annonce de crue'; + String get contacts_floodAdvert => 'Annonce à tout le réseau'; @override String get contacts_copyAdvertToClipboard => diff --git a/untranslated.json b/untranslated.json index e0ad904b..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,127 +1 @@ -{ - "bg": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "de": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "es": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "fr": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "it": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "nl": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "pl": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "pt": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "ru": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "sk": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "sl": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "sv": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "uk": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ], - - "zh": [ - "notification_activityTitle", - "notification_messagesCount", - "notification_channelMessagesCount", - "notification_newNodesCount", - "notification_newTypeDiscovered", - "notification_receivedNewMessage" - ] -} +{} \ No newline at end of file From c26174ad18740fcfa13b0521594ff673f557042a Mon Sep 17 00:00:00 2001 From: Zach Date: Tue, 10 Feb 2026 09:01:56 -0700 Subject: [PATCH 03/99] Chore bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6474c5f9..e1ae0231 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 5.0.0+6 +version: 6.0.0+1 environment: sdk: ^3.9.2 From 607583060a9565b41406c61661d16efadb153cbd Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 10 Feb 2026 22:55:39 +0100 Subject: [PATCH 04/99] translations to german updated. --- lib/l10n/app_de.arb | 78 +++++++++++++++--------------- lib/l10n/app_localizations_de.dart | 77 ++++++++++++++--------------- 2 files changed, 78 insertions(+), 77 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3dcd0ca1..66bc049a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -96,14 +96,14 @@ "settings_privacyModeEnabled": "Datenschutzmodus aktiviert", "settings_privacyModeDisabled": "Datenschutzmodus deaktiviert", "settings_actions": "Aktionen", - "settings_sendAdvertisement": "Sende eine Ankündigung", - "settings_sendAdvertisementSubtitle": "Sende Ankündigung", + "settings_sendAdvertisement": "Sende Ankündigung", + "settings_sendAdvertisementSubtitle": "Sende eine Ankündigung", "settings_advertisementSent": "Ankündigung gesendet", "settings_syncTime": "Zeitsynchronisierung", "settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein", "settings_timeSynchronized": "Zeit synchronisiert", "settings_refreshContacts": "Kontakte aktualisieren", - "settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden", + "settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden", "settings_rebootDevice": "Gerät neu starten", "settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten", "settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.", @@ -540,7 +540,7 @@ "chat_routingMode": "Routenmodus", "chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)", "chat_forceFloodMode": "Flut-Modus erzwingen", - "chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):", + "chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):", "chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.", "chat_hopSingular": "Sprung", "chat_hopPlural": "Sprünge", @@ -554,7 +554,7 @@ }, "chat_successes": "Erfolgreich", "chat_removePath": "Pfad entfernen", - "chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.", + "chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.", "chat_pathActions": "Pfadaktionen:", "chat_setCustomPath": "Lege benutzerdefinierten Pfad fest", "chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen", @@ -717,7 +717,7 @@ "mapCache_cacheArea": "Zwischenspeicherbereich", "mapCache_useCurrentView": "Aktuelle Ansicht verwenden", "mapCache_zoomRange": "Zoom Bereich", - "mapCache_estimatedTiles": "Geschätzte Fliesen: {count}", + "mapCache_estimatedTiles": "Geschätzte Kacheln: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -854,7 +854,7 @@ }, "path_enterCustomPath": "Gebe Pfad ein", "path_currentPathLabel": "Aktueller Pfad", - "path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.", + "path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.", "path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)", "path_labelHexPrefixes": "Pfad (Hex-Präfixe)", "path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)", @@ -887,7 +887,7 @@ "repeater_forceFloodMode": "Flut-Modus erzwingen", "repeater_pathManagement": "Pfadverwaltung", "repeater_refresh": "Aktualisieren", - "repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.", + "repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.", "repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { @@ -957,7 +957,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}", + "repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -983,7 +983,7 @@ "repeater_adminPassword": "Admin-Passwort", "repeater_adminPasswordHelper": "Vollzugriffspasswort", "repeater_guestPassword": "Gast-Passwort", - "repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort", + "repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort", "repeater_radioSettings": "Funk Einstellungen", "repeater_frequencyMhz": "Frequenz (MHz)", "repeater_frequencyHelper": "300-2500 MHz", @@ -1086,11 +1086,11 @@ } }, "repeater_cliTitle": "Repeater CLI", - "repeater_debugNextCommand": "Fehlersuche Nächster Befehl", + "repeater_debugNextCommand": "Fehlersuche des nächsten Befehls", "repeater_commandHelp": "Hilfe", "repeater_clearHistory": "Löschen der Historie", "repeater_noCommandsSent": "Noch keine Befehle gesendet.", - "repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle", + "repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle", "repeater_enterCommandHint": "Geben Sie den Befehl ein...", "repeater_previousCommand": "Vorhergehende Aktion", "repeater_nextCommand": "Nächste Aktion", @@ -1132,7 +1132,7 @@ "repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)", "repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)", "repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.", - "repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", + "repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.", "repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).", "repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.", "repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.", @@ -1143,14 +1143,14 @@ "repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).", "repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).", "repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)", - "repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.", + "repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.", "repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.", "repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.", "repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.", "repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4", "repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.", "repeater_cliHelpRegion": "Listet alle definierten Regionen auf.", - "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.", + "repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.", "repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".", "repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.", "repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)", @@ -1243,7 +1243,7 @@ "channelPath_otherObservedPaths": "Sonstige beobachtete Pfade", "channelPath_repeaterHops": "Repeater-Sprünge", "channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.", - "channelPath_messageDetails": "Nachrichtsdetails", + "channelPath_messageDetails": "Nachrichtendetails", "channelPath_senderLabel": "Sender", "channelPath_timeLabel": "Zeit", "channelPath_repeatsLabel": "Wiederholungen", @@ -1347,7 +1347,7 @@ "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", - "listFilter_unreadOnly": "Nur nicht gelesen", + "listFilter_unreadOnly": "Nicht gelesen", "listFilter_newGroup": "Neue Gruppe", "@neighbors_errorLoading": { "placeholders": { @@ -1358,11 +1358,11 @@ }, "repeater_neighbours": "Nachbarn", "repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", - "neighbors_receivedData": "Empfangene Nachbarendaten", - "neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.", + "neighbors_receivedData": "Empfangene Nachbarsdaten", + "neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", "neighbors_repeatersNeighbours": "Nachbarn", - "neighbors_noData": "Keine Nachbardaten verfügbar.", + "neighbors_noData": "Keine Nachbarsdaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", "channels_createPrivateChannel": "Erstelle einen privaten Kanal", @@ -1389,8 +1389,8 @@ } } }, - "neighbors_heardAgo": "Hörte: {time} vor her.", - "neighbors_unknownContact": "Unbekannte {pubkey}", + "neighbors_heardAgo": "Gehört vor: {time}", + "neighbors_unknownContact": "Unbekannt {pubkey}", "settings_locationGPSEnable": "GPS aktivieren", "settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.", "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", @@ -1493,9 +1493,9 @@ "community_deleted": "Community \"{name}\" verlassen", "community_addHashtagChannel": "Füge einen Community-Hashtag hinzu", "community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu", - "community_selectCommunity": "Wählen Sie Community", + "community_selectCommunity": "Wählen Sie eine Community", "community_regularHashtag": "Regulärer Hashtag", - "community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)", + "community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)", "community_communityHashtagDesc": "Nur für Mitglieder der Community", "community_forCommunity": "Für {name}", "community_communityHashtag": "Community Hashtag", @@ -1559,16 +1559,16 @@ "appSettings_languageUk": "Ukrainisch", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", - "contacts_zeroHopAdvert": "Zero-Hop-Anzeige", - "contacts_floodAdvert": "Überflutungsanzeige", + "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", + "contacts_floodAdvert": "Flut-Ankündigung", "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", - "contacts_copyAdvertToClipboard": "Werbung in die Zwischenablage kopieren", + "contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren", "contacts_ShareContact": "Kontakt in die Zwischenablage kopieren", "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", + "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", "notification_activityTitle": "MeshCore Aktivität", "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", @@ -1596,22 +1596,22 @@ } }, "notification_receivedNewMessage": "Neue Nachricht empfangen", - "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.", - "settings_gpxExportAll": "Alle Kontakte nach GPX exportieren", - "settings_gpxExportAllSubtitle": "Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.", - "settings_gpxExportRepeaters": "Repeater und Raumserver nach GPX exportieren", - "settings_gpxExportContacts": "Begleiter nach GPX exportieren", + "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", + "settings_gpxExportAll": "Alle Knoten als GPX exportieren", + "settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.", + "settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren", + "settings_gpxExportContacts": "Kontakte als GPX exportieren", "settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.", - "settings_gpxExportContactsSubtitle": "Exportiert Begleiter mit einem Ort in eine GPX-Datei.", + "settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.", "settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte", - "settings_gpxExportChat": "Begleiterstandorte", + "settings_gpxExportChat": "Kontaktstandorte", "settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.", "settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.", "settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt", - "settings_gpxExportSuccess": "Erfolgreich GPX-Datei exportiert.", + "settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.", "settings_gpxExportAllContacts": "Alle Kontaktstandorte", - "settings_gpxExportShareSubject": "meshcore-open GPX-Kartendaten exportieren", - "settings_gpxExportShareText": "Kartendaten aus meshcore-open exportiert", - "pathTrace_someHopsNoLocation": "Eine oder mehrere der Hopfen fehlen einen Standort!" + "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", + "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", + "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!" } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 8ad7f1eb..09672278 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -244,10 +244,10 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_actions => 'Aktionen'; @override - String get settings_sendAdvertisement => 'Sende eine Ankündigung'; + String get settings_sendAdvertisement => 'Sende Ankündigung'; @override - String get settings_sendAdvertisementSubtitle => 'Sende Ankündigung'; + String get settings_sendAdvertisementSubtitle => 'Sende eine Ankündigung'; @override String get settings_advertisementSent => 'Ankündigung gesendet'; @@ -267,7 +267,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_refreshContactsSubtitle => - 'Kontakte-Liste vom Gerät neu laden'; + 'Kontakt-Liste vom Gerät neu laden'; @override String get settings_rebootDevice => 'Gerät neu starten'; @@ -1086,7 +1086,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_recentAckPaths => - 'Aktuelle ACK-Pfade (tasten, um zu verwenden):'; + 'Aktuelle ACK-Pfade (antippen, um zu verwenden):'; @override String get chat_pathHistoryFull => @@ -1117,7 +1117,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_noPathHistoryYet => - 'Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.'; + 'Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.'; @override String get chat_pathActions => 'Pfadaktionen:'; @@ -1418,7 +1418,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String mapCache_estimatedTiles(int count) { - return 'Geschätzte Fliesen: $count'; + return 'Geschätzte Kacheln: $count'; } @override @@ -1592,7 +1592,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get path_hexPrefixInstructions => - 'Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.'; + 'Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.'; @override String get path_hexPrefixExample => @@ -1689,7 +1689,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_statusRequestTimeout => - 'Statusanfrage zeitweise fehlgeschlagen.'; + 'Statusanfrage durch Timeout fehlgeschlagen.'; @override String repeater_errorLoadingStatus(String error) { @@ -1766,7 +1766,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return 'Überflut: $flood, Direkt: $direct'; + return 'Flut: $flood, Direkt: $direct'; } @override @@ -1797,7 +1797,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_guestPasswordHelper => - 'Schreibgeschützter Zugriffspasswort'; + 'Schreibgeschütztes Zugriffspasswort'; @override String get repeater_radioSettings => 'Funk Einstellungen'; @@ -1992,7 +1992,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliTitle => 'Repeater CLI'; @override - String get repeater_debugNextCommand => 'Fehlersuche Nächster Befehl'; + String get repeater_debugNextCommand => 'Fehlersuche des nächsten Befehls'; @override String get repeater_commandHelp => 'Hilfe'; @@ -2005,7 +2005,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_typeCommandOrUseQuick => - 'Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle'; + 'Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle'; @override String get repeater_enterCommandHint => 'Geben Sie den Befehl ein...'; @@ -2131,7 +2131,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpSetRxDelay => - 'Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; + 'Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.'; @override String get repeater_cliHelpSetTxDelay => @@ -2175,7 +2175,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpGetBridgeType => - 'Ruft Brückentyp none, rs232, espnow ab.'; + 'Ruft Brückentyp: none, rs232, espnow ab.'; @override String get repeater_cliHelpLogStart => @@ -2202,7 +2202,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliHelpRegionLoad => - 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.'; + 'Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.'; @override String get repeater_cliHelpRegionGet => @@ -2351,10 +2351,11 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get neighbors_receivedData => 'Empfangene Nachbarendaten'; + String get neighbors_receivedData => 'Empfangene Nachbarsdaten'; @override - String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.'; + String get neighbors_requestTimedOut => + 'Anfrage durch Timeout fehlgeschlagen.'; @override String neighbors_errorLoading(String error) { @@ -2365,16 +2366,16 @@ class AppLocalizationsDe extends AppLocalizations { String get neighbors_repeatersNeighbours => 'Nachbarn'; @override - String get neighbors_noData => 'Keine Nachbardaten verfügbar.'; + String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; @override String neighbors_unknownContact(String pubkey) { - return 'Unbekannte $pubkey'; + return 'Unbekannt $pubkey'; } @override String neighbors_heardAgo(String time) { - return 'Hörte: $time vor her.'; + return 'Gehört vor: $time'; } @override @@ -2394,7 +2395,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Die Detailangaben für dieses Paket sind nicht verfügbar.'; @override - String get channelPath_messageDetails => 'Nachrichtsdetails'; + String get channelPath_messageDetails => 'Nachrichtendetails'; @override String get channelPath_senderLabel => 'Sender'; @@ -2630,14 +2631,14 @@ class AppLocalizationsDe extends AppLocalizations { 'Füge einen Hashtag-Kanal für diese Community hinzu'; @override - String get community_selectCommunity => 'Wählen Sie Community'; + String get community_selectCommunity => 'Wählen Sie eine Community'; @override String get community_regularHashtag => 'Regulärer Hashtag'; @override String get community_regularHashtagDesc => - 'Öffentliches Hashtag (jeder kann teilnehmen)'; + 'Öffentlicher Hashtag (jeder kann teilnehmen)'; @override String get community_communityHashtag => 'Community Hashtag'; @@ -2682,7 +2683,7 @@ class AppLocalizationsDe extends AppLocalizations { String get listFilter_roomServers => 'Raumserver'; @override - String get listFilter_unreadOnly => 'Nur nicht gelesen'; + String get listFilter_unreadOnly => 'Nicht gelesen'; @override String get listFilter_newGroup => 'Neue Gruppe'; @@ -2701,7 +2702,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => - 'Eine oder mehrere der Hopfen fehlen einen Standort!'; + 'Bei einer oder mehreren Knoten fehlt der Standort!'; @override String get contacts_pathTrace => 'Pfadverfolgung'; @@ -2743,14 +2744,14 @@ class AppLocalizationsDe extends AppLocalizations { 'Kontakt konnte nicht importiert werden'; @override - String get contacts_zeroHopAdvert => 'Zero-Hop-Anzeige'; + String get contacts_zeroHopAdvert => 'Zero-Hop-Ankündigung'; @override - String get contacts_floodAdvert => 'Überflutungsanzeige'; + String get contacts_floodAdvert => 'Flut-Ankündigung'; @override String get contacts_copyAdvertToClipboard => - 'Werbung in die Zwischenablage kopieren'; + 'Ankündigung in die Zwischenablage kopieren'; @override String get contacts_addContactFromClipboard => @@ -2776,7 +2777,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contacts_contactAdvertCopyFailed => - 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; + 'Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.'; @override String get notification_activityTitle => 'MeshCore Aktivität'; @@ -2824,28 +2825,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_gpxExportRepeaters => - 'Repeater und Raumserver nach GPX exportieren'; + 'Repeater und Raumserver als GPX exportieren'; @override String get settings_gpxExportRepeatersSubtitle => 'Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.'; @override - String get settings_gpxExportContacts => 'Begleiter nach GPX exportieren'; + String get settings_gpxExportContacts => 'Kontakte als GPX exportieren'; @override String get settings_gpxExportContactsSubtitle => - 'Exportiert Begleiter mit einem Ort in eine GPX-Datei.'; + 'Exportiert Kontakte mit einem Ort in eine GPX-Datei.'; @override - String get settings_gpxExportAll => 'Alle Kontakte nach GPX exportieren'; + String get settings_gpxExportAll => 'Alle Knoten als GPX exportieren'; @override String get settings_gpxExportAllSubtitle => - 'Exportiert alle Kontakte mit einem Standort in eine GPX-Datei.'; + 'Exportiert alle Knoten mit einem Standort in eine GPX-Datei.'; @override - String get settings_gpxExportSuccess => 'Erfolgreich GPX-Datei exportiert.'; + String get settings_gpxExportSuccess => 'GPX-Datei erfolgreich exportiert.'; @override String get settings_gpxExportNoContacts => 'Keine Kontakte zum Exportieren.'; @@ -2863,16 +2864,16 @@ class AppLocalizationsDe extends AppLocalizations { 'Repeater- und Raumserver-Standorte'; @override - String get settings_gpxExportChat => 'Begleiterstandorte'; + String get settings_gpxExportChat => 'Kontaktstandorte'; @override String get settings_gpxExportAllContacts => 'Alle Kontaktstandorte'; @override String get settings_gpxExportShareText => - 'Kartendaten aus meshcore-open exportiert'; + 'GPX-Kartendaten aus meshcore-open exportiert'; @override String get settings_gpxExportShareSubject => - 'meshcore-open GPX-Kartendaten exportieren'; + 'GPX-Kartendaten aus meshcore-open exportieren'; } From 84a32c1e672ffa35b5f93e0e401d494a5f36ee54 Mon Sep 17 00:00:00 2001 From: 446564 Date: Tue, 10 Feb 2026 19:38:46 -0800 Subject: [PATCH 05/99] remove wakelock was being used to keep ble active which is not what it does in early testing the ble remains connected with display off and also when switching apps --- lib/connector/meshcore_connector.dart | 9 --------- lib/services/background_service.dart | 1 - pubspec.lock | 20 ++++++++++---------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2c56c373..37c9c740 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -5,7 +5,6 @@ import 'package:crypto/crypto.dart' as crypto; import 'package:pointycastle/export.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; @@ -775,9 +774,6 @@ class MeshCoreConnector extends ChangeNotifier { _setState(MeshCoreConnectionState.connected); - // Enable wake lock to prevent BLE disconnection when screen turns off - await WakelockPlus.enable(); - await _requestDeviceInfo(); _startBatteryPolling(); final gotSelfInfo = await _waitForSelfInfo( @@ -886,9 +882,6 @@ class MeshCoreConnector extends ChangeNotifier { _setState(MeshCoreConnectionState.disconnecting); _stopBatteryPolling(); - // Disable wake lock when disconnecting - await WakelockPlus.disable(); - await _notifySubscription?.cancel(); _notifySubscription = null; @@ -3214,8 +3207,6 @@ class MeshCoreConnector extends ChangeNotifier { } void _handleDisconnection() { - // Disable wake lock when connection is lost - WakelockPlus.disable(); _stopBatteryPolling(); for (final entry in _pendingRepeaterAcks.values) { diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index fce77a1d..6599fbc4 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -28,7 +28,6 @@ class BackgroundService { foregroundTaskOptions: const ForegroundTaskOptions( interval: 5000, autoRunOnBoot: false, - allowWakeLock: true, allowWifiLock: false, ), ); diff --git a/pubspec.lock b/pubspec.lock index fc116566..207ff51e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -489,26 +489,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: From 675083fa01b03fe77e2ce029007bf7a23f9c6588 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:10:49 +0100 Subject: [PATCH 06/99] Update .gitignore to exclude .gradle/ --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b9181133..ab7ac25d 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,5 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json + +.gradle/ \ No newline at end of file From a4d3d248a5707eecc3b99b9349108e7a10656fd8 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:11:00 +0100 Subject: [PATCH 07/99] Add flake.nix for development environment --- flake.nix | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 flake.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..e30165ed --- /dev/null +++ b/flake.nix @@ -0,0 +1,116 @@ +{ + description = "MeshCore Flutter Application"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Flutter and Dart + flutter + dart + + # Java (required for Android development) + jdk17 + + # Android development tools + android-tools + gradle + + # iOS development (macOS only) + ] ++ ( + if pkgs.stdenv.isDarwin then + with pkgs.darwin.apple_sdk.frameworks; [ + Cocoa + Security + IOKit + ] + else + [] + ) ++ [ + # Common development tools + git + curl + wget + pkg-config + + # Build tools + cmake + ninja + clang + llvm + # Linux desktop development + gtk3 + glib + libdatrie + sysprof + xorg.libX11 + xorg.libXext + xorg.libXrender + xorg.libXinerama + xorg.libXcursor + xorg.libXi + xorg.libXrandr + xorg.libXdamage + # Optional: testing + lcov + ]; + + shellHook = '' + echo "MeshCore Flutter Development Environment" + export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH" + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH" + export CMAKE_INSTALL_PREFIX="$PWD/build/bundle" + mkdir -p "$PWD/build/bundle" + + # Setup Android SDK in home directory (standard location) + export ANDROID_HOME="$HOME/Android/Sdk" + export ANDROID_SDK_ROOT="$ANDROID_HOME" + export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH" + + # Use gradle wrapper + export GRADLE_USER_HOME="$PWD/.gradle" + + echo "Android SDK: $ANDROID_HOME" + echo "Gradle Home: $GRADLE_USER_HOME" + echo "" + + # Check if Android SDK exists and offer to download if not + if [ ! -d "$ANDROID_HOME" ]; then + echo "WARNING: Android SDK not found at $ANDROID_HOME" + echo "" + echo "To download and set up the Android SDK, run this command:" + echo "" + cat << 'EOF' +mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \ +curl -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip && \ +unzip -q cmdline-tools.zip && \ +mkdir -p cmdline-tools/latest && \ +mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || true && \ +rm cmdline-tools.zip && \ +cd cmdline-tools/latest/bin && \ +yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' 'platforms;android-34' 'build-tools;34.0.0' && \ +echo "Android SDK setup complete!" +EOF + echo "" + echo "Then run 'flutter doctor' again to verify." + echo "" + else + echo "Android SDK found at $ANDROID_HOME" + fi + + echo "Running flutter doctor..." + flutter doctor + ''; + }; + } + ); +} From 5b699cd624b5235e26a4bd82523be7fc573c5437 Mon Sep 17 00:00:00 2001 From: Ded Date: Wed, 11 Feb 2026 08:16:07 -0800 Subject: [PATCH 08/99] keep ignores organized --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ab7ac25d..2d9a3fcc 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ secrets.dart **/ios/Flutter/Flutter.podspec # Android +.gradle/ **/android/.gradle/ **/android/captures/ **/android/local.properties @@ -81,5 +82,3 @@ keystore.properties # IDE .vscode/launch.json .vscode/settings.json - -.gradle/ \ No newline at end of file From 4afab3f62946a3f76cad918f8dfae017575a62e2 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:25:44 +0100 Subject: [PATCH 09/99] remove unnessisary bits and nix darwin stuff --- flake.nix | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/flake.nix b/flake.nix index e30165ed..68f7bcae 100644 --- a/flake.nix +++ b/flake.nix @@ -24,44 +24,6 @@ # Android development tools android-tools gradle - - # iOS development (macOS only) - ] ++ ( - if pkgs.stdenv.isDarwin then - with pkgs.darwin.apple_sdk.frameworks; [ - Cocoa - Security - IOKit - ] - else - [] - ) ++ [ - # Common development tools - git - curl - wget - pkg-config - - # Build tools - cmake - ninja - clang - llvm - # Linux desktop development - gtk3 - glib - libdatrie - sysprof - xorg.libX11 - xorg.libXext - xorg.libXrender - xorg.libXinerama - xorg.libXcursor - xorg.libXi - xorg.libXrandr - xorg.libXdamage - # Optional: testing - lcov ]; shellHook = '' From dfd38b19e9b3a4639c578abe16442cac81c6900c Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:26:43 +0100 Subject: [PATCH 10/99] add flake.lock --- flake.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 flake.lock diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..4d0355b2 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} From aa350aa4aecbbf92132d293ac7f77c49d8b6906c Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:33:31 +0100 Subject: [PATCH 11/99] fixing copilot issues --- flake.nix | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 68f7bcae..841f5537 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,19 @@ # Android development tools android-tools gradle + + # For the shell hook to set up the environment for Flutter development + gtk3 + glib + sysprof + libclang + cmake + ninja + pkg-config + + # Additional tools for installing Android SDK if not present + curl + unzip ]; shellHook = '' @@ -53,10 +66,10 @@ echo "" cat << 'EOF' mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \ -curl -o cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip && \ +curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \ unzip -q cmdline-tools.zip && \ mkdir -p cmdline-tools/latest && \ -mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || true && \ +mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \ rm cmdline-tools.zip && \ cd cmdline-tools/latest/bin && \ yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' 'platforms;android-34' 'build-tools;34.0.0' && \ @@ -69,8 +82,7 @@ EOF echo "Android SDK found at $ANDROID_HOME" fi - echo "Running flutter doctor..." - flutter doctor + echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues." ''; }; } From 4e6e7b60611292429470e7b3a093e8e8d8647e5c Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 17:45:15 +0100 Subject: [PATCH 12/99] fix smaller copilot issues --- flake.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 841f5537..16711455 100644 --- a/flake.nix +++ b/flake.nix @@ -33,6 +33,7 @@ cmake ninja pkg-config + libdatrie # Additional tools for installing Android SDK if not present curl @@ -44,18 +45,13 @@ export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH" export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH" export CMAKE_INSTALL_PREFIX="$PWD/build/bundle" - mkdir -p "$PWD/build/bundle" # Setup Android SDK in home directory (standard location) export ANDROID_HOME="$HOME/Android/Sdk" export ANDROID_SDK_ROOT="$ANDROID_HOME" export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH" - # Use gradle wrapper - export GRADLE_USER_HOME="$PWD/.gradle" - echo "Android SDK: $ANDROID_HOME" - echo "Gradle Home: $GRADLE_USER_HOME" echo "" # Check if Android SDK exists and offer to download if not @@ -72,7 +68,7 @@ mkdir -p cmdline-tools/latest && \ mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \ rm cmdline-tools.zip && \ cd cmdline-tools/latest/bin && \ -yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' 'platforms;android-34' 'build-tools;34.0.0' && \ +yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \ echo "Android SDK setup complete!" EOF echo "" From 9ce00556ec7a3b8a652161514c9f656c6f801913 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 22:40:42 +0100 Subject: [PATCH 13/99] Add warning when bluetooth is off --- lib/l10n/app_en.arb | 3 ++ lib/screens/scanner_screen.dart | 55 +++++++++++++++++++++ untranslated.json | 86 ++++++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0c54be35..dcbc7cb4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -66,6 +66,9 @@ }, "scanner_stop": "Stop", "scanner_scan": "Scan", + "scanner_bluetoothOff": "Bluetooth is off", + "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", + "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 75819a0a..2049dab7 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:provider/provider.dart'; @@ -18,6 +20,8 @@ class ScannerScreen extends StatefulWidget { class _ScannerScreenState extends State { bool _changedNavigation = false; late final VoidCallback _connectionListener; + BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown; + late StreamSubscription _bluetoothStateSubscription; @override void initState() { @@ -39,12 +43,22 @@ class _ScannerScreenState extends State { }; connector.addListener(_connectionListener); + + _bluetoothStateSubscription = + FlutterBluePlus.adapterState.listen((state) { + if (mounted) { + setState(() { + _bluetoothState = state; + }); + } + }); } @override void dispose() { final connector = Provider.of(context, listen: false); connector.removeListener(_connectionListener); + _bluetoothStateSubscription.cancel(); super.dispose(); } @@ -62,6 +76,10 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { return Column( children: [ + // Bluetooth off warning + if (_bluetoothState != BluetoothAdapterState.on) + _bluetoothOffWarning(context), + // Status bar _buildStatusBar(context, connector), @@ -205,4 +223,41 @@ class _ScannerScreenState extends State { } } } + + Widget _bluetoothOffWarning(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + color: Colors.red.withValues(alpha: 0.15), + child: Row( + children: [ + Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.scanner_bluetoothOff, + style: const TextStyle( + color: Colors.red, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.scanner_bluetoothOffMessage, + style: TextStyle( + color: Colors.red.withValues(alpha: 0.85), + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ); + } } diff --git a/untranslated.json b/untranslated.json index 9e26dfee..2b5dc439 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,85 @@ -{} \ No newline at end of file +{ + "bg": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "de": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "es": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "fr": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "it": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "nl": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "pl": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "pt": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "ru": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "sk": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "sl": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "sv": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "uk": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ], + + "zh": [ + "scanner_bluetoothOff", + "scanner_bluetoothOffMessage", + "scanner_enableBluetooth" + ] +} From 9332d8126f34abb128ea4059864ab33e5f23b475 Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 22:58:15 +0100 Subject: [PATCH 14/99] linted and added greying out --- lib/screens/scanner_screen.dart | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 2049dab7..e52afb35 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -44,12 +44,16 @@ class _ScannerScreenState extends State { connector.addListener(_connectionListener); - _bluetoothStateSubscription = - FlutterBluePlus.adapterState.listen((state) { + _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) { if (mounted) { setState(() { _bluetoothState = state; }); + // Cancel scan if Bluetooth turns off while scanning + if (state != BluetoothAdapterState.on && + connector.state == MeshCoreConnectionState.scanning) { + connector.stopScan(); + } } }); } @@ -94,15 +98,23 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { final isScanning = connector.state == MeshCoreConnectionState.scanning; + final isBluetoothOn = _bluetoothState == BluetoothAdapterState.on; return FloatingActionButton.extended( - onPressed: () { - if (isScanning) { - connector.stopScan(); - } else { - connector.startScan(); - } - }, + onPressed: isBluetoothOn + ? () { + if (isScanning) { + connector.stopScan(); + } else { + connector.startScan(); + } + } + : null, + backgroundColor: isBluetoothOn ? null : Colors.grey, + foregroundColor: isBluetoothOn ? null : Colors.white, + mouseCursor: isBluetoothOn + ? SystemMouseCursors.click + : SystemMouseCursors.forbidden, icon: isScanning ? const SizedBox( width: 20, From bc77f7e28753f5a0490245902853e6d891cb34df Mon Sep 17 00:00:00 2001 From: Leah <45321184+ChaoticLeah@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:03:41 +0100 Subject: [PATCH 15/99] Remove unused translation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/l10n/app_en.arb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dcbc7cb4..668f72e9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -68,7 +68,6 @@ "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", - "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", From 6a666839b628af8b70db5f1bf4c68d339a388352 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:05:00 -0500 Subject: [PATCH 16/99] Fix battery chemistry dropdown layout overflow --- lib/screens/app_settings_screen.dart | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 135babd4..4e317335 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -384,6 +384,7 @@ class AppSettingsScreen extends StatelessWidget { ); } + // Fixed rendering issues Widget _buildBatteryCard( BuildContext context, AppSettingsService settingsService, @@ -399,6 +400,7 @@ class AppSettingsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + const SizedBox(height: 4), Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( @@ -406,6 +408,8 @@ class AppSettingsScreen extends StatelessWidget { style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), ), + + // Main tile (icon + text only) ListTile( leading: const Icon(Icons.battery_full), title: Text(context.l10n.appSettings_batteryChemistry), @@ -416,8 +420,19 @@ class AppSettingsScreen extends StatelessWidget { ) : context.l10n.appSettings_batteryChemistryConnectFirst, ), - trailing: DropdownButton( - value: selection, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + ), + + // Dropdown (separate full-width row) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: DropdownButtonFormField( + initialValue: selection, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, + ), onChanged: isConnected ? (value) { if (value != null) { From c05f813d653d0500a936d3dd50768bc6eb3582de Mon Sep 17 00:00:00 2001 From: Leah <45321184+ChaoticLeah@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:02:56 +0100 Subject: [PATCH 17/99] Update lib/screens/scanner_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/screens/scanner_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index e52afb35..1aeba47f 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -243,7 +243,7 @@ class _ScannerScreenState extends State { color: Colors.red.withValues(alpha: 0.15), child: Row( children: [ - Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), + const Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), const SizedBox(width: 12), Expanded( child: Column( From 01c83909897c8969c5b982548a3c7f401b5c3e4e Mon Sep 17 00:00:00 2001 From: Leah Date: Thu, 12 Feb 2026 20:14:56 +0100 Subject: [PATCH 18/99] make stuff unawaited + maybe fix edge case? --- lib/screens/scanner_screen.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index e52afb35..ef7f90f4 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -50,9 +50,8 @@ class _ScannerScreenState extends State { _bluetoothState = state; }); // Cancel scan if Bluetooth turns off while scanning - if (state != BluetoothAdapterState.on && - connector.state == MeshCoreConnectionState.scanning) { - connector.stopScan(); + if (state != BluetoothAdapterState.on) { + unawaited(connector.stopScan()); } } }); @@ -62,7 +61,7 @@ class _ScannerScreenState extends State { void dispose() { final connector = Provider.of(context, listen: false); connector.removeListener(_connectionListener); - _bluetoothStateSubscription.cancel(); + unawaited(_bluetoothStateSubscription.cancel()); super.dispose(); } From fac062a100d224b45f6b724198638313f5d33a25 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:46:28 -0500 Subject: [PATCH 19/99] Refine device info layout and add collapsible map legend (#164) --- lib/screens/map_screen.dart | 143 ++++++++++++++++++++----------- lib/screens/settings_screen.dart | 140 ++++++++++++++++++++---------- 2 files changed, 190 insertions(+), 93 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index f522407b..bc213f97 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -51,6 +51,7 @@ class _MapScreenState extends State { bool _isSelectingPoi = false; bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; + bool _legendExpanded = false; @override void initState() { @@ -503,60 +504,102 @@ class _MapScreenState extends State { top: 16, right: 16, child: Card( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.l10n.map_nodesCount(nodeCount), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + setState(() { + _legendExpanded = !_legendExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 10, 12, 10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.map_nodesCount(nodeCount), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + Text( + context.l10n.map_pinsCount(markerCount), + style: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ], + ), + const SizedBox(width: 8), + AnimatedRotation( + turns: _legendExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: const Icon(Icons.expand_more, size: 20), + ), + ], ), ), - Text( - context.l10n.map_pinsCount(markerCount), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, + ), + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 6), + _buildLegendItem( + Icons.person, + context.l10n.map_chat, + Colors.blue, + ), + _buildLegendItem( + Icons.router, + context.l10n.map_repeater, + Colors.green, + ), + _buildLegendItem( + Icons.meeting_room, + context.l10n.map_room, + Colors.purple, + ), + _buildLegendItem( + Icons.sensors, + context.l10n.map_sensor, + Colors.orange, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinDm, + Colors.blue, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinPrivate, + Colors.purple, + ), + _buildLegendItem( + Icons.flag, + context.l10n.map_pinPublic, + Colors.orange, + ), + ], ), ), - const SizedBox(height: 8), - _buildLegendItem( - Icons.person, - context.l10n.map_chat, - Colors.blue, - ), - _buildLegendItem( - Icons.router, - context.l10n.map_repeater, - Colors.green, - ), - _buildLegendItem( - Icons.meeting_room, - context.l10n.map_room, - Colors.purple, - ), - _buildLegendItem( - Icons.sensors, - context.l10n.map_sensor, - Colors.orange, - ), - _buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinPrivate, - Colors.purple, - ), - _buildLegendItem( - Icons.flag, - context.l10n.map_pinPublic, - Colors.orange, - ), - ], - ), + crossFadeState: _legendExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 200), + ), + ], ), ), ); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 2212b8db..4943284a 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -21,6 +21,7 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { bool _showBatteryVoltage = false; + bool _deviceInfoExpanded = false; String _appVersion = ''; @override @@ -74,43 +75,84 @@ class _SettingsScreenState extends State { MeshCoreConnector connector, ) { final l10n = context.l10n; + return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.settings_deviceInfo, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 16), - _buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName), - _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), - _buildInfoRow( - l10n.settings_infoStatus, - connector.isConnected - ? l10n.common_connected - : l10n.common_disconnected, - ), - _buildBatteryInfoRow(context, connector), - if (connector.selfName != null) - _buildInfoRow(l10n.settings_nodeName, connector.selfName!), - if (connector.selfPublicKey != null) - _buildInfoRow( - l10n.settings_infoPublicKey, - '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + setState(() { + _deviceInfoExpanded = !_deviceInfoExpanded; + }); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Row( + children: [ + Expanded( + child: Text( + l10n.settings_deviceInfo, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + AnimatedRotation( + turns: _deviceInfoExpanded ? 0.5 : 0, + duration: const Duration(milliseconds: 200), + child: const Icon(Icons.expand_more), + ), + ], ), - _buildInfoRow( - l10n.settings_infoContactsCount, - '${connector.contacts.length}', ), - _buildInfoRow( - l10n.settings_infoChannelCount, - '${connector.channels.length}', + ), + + AnimatedCrossFade( + firstChild: const SizedBox.shrink(), + secondChild: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildInfoRow( + l10n.settings_infoName, + connector.deviceDisplayName, + ), + _buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel), + _buildInfoRow( + l10n.settings_infoStatus, + connector.isConnected + ? l10n.common_connected + : l10n.common_disconnected, + ), + _buildBatteryInfoRow(context, connector), + if (connector.selfName != null) + _buildInfoRow(l10n.settings_nodeName, connector.selfName!), + if (connector.selfPublicKey != null) + _buildInfoRow( + l10n.settings_infoPublicKey, + '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...', + ), + _buildInfoRow( + l10n.settings_infoContactsCount, + '${connector.contacts.length}', + ), + _buildInfoRow( + l10n.settings_infoChannelCount, + '${connector.channels.length}', + ), + ], + ), ), - ], - ), + crossFadeState: _deviceInfoExpanded + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, + duration: const Duration(milliseconds: 200), + ), + ], ), ); } @@ -355,22 +397,33 @@ class _SettingsScreenState extends State { Color? valueColor, VoidCallback? onTap, }) { + final theme = Theme.of(context); + final row = Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ if (leading != null) ...[leading, const SizedBox(width: 8)], - Text(label, style: TextStyle(color: Colors.grey[600])), + Expanded( + child: Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ), ], ), - Flexible( - child: Text( - value, - style: TextStyle(fontWeight: FontWeight.w500, color: valueColor), - overflow: TextOverflow.ellipsis, + const SizedBox(height: 4), + Text( + value, + style: theme.textTheme.bodyLarge?.copyWith( + color: valueColor, + fontWeight: FontWeight.w500, ), ), ], @@ -379,11 +432,12 @@ class _SettingsScreenState extends State { if (onTap != null) { return InkWell( + borderRadius: BorderRadius.circular(6), onTap: onTap, - borderRadius: BorderRadius.circular(4), child: row, ); } + return row; } From 73081862ad132cdc7b51616af605b94f54a3e89e Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 14 Feb 2026 00:10:34 -0800 Subject: [PATCH 20/99] Add path tracing functionality (#165) - Implemented path tracing feature in the map screen, allowing users to add nodes to a path and visualize it on the map. - Added buttons for starting path tracing, removing the last node, and running the path trace. - Introduced a new overlay to display current path information and distance. - Updated localization files for multiple languages to include new strings related to path tracing. - Refactored map rendering logic to accommodate path tracing visuals. --- lib/l10n/app_bg.arb | 9 +- lib/l10n/app_de.arb | 26 +++-- lib/l10n/app_en.arb | 5 + lib/l10n/app_es.arb | 26 +++-- lib/l10n/app_fr.arb | 7 +- lib/l10n/app_it.arb | 9 +- lib/l10n/app_localizations.dart | 30 ++++++ lib/l10n/app_localizations_bg.dart | 16 +++ lib/l10n/app_localizations_de.dart | 16 +++ lib/l10n/app_localizations_en.dart | 15 +++ lib/l10n/app_localizations_es.dart | 15 +++ lib/l10n/app_localizations_fr.dart | 16 +++ lib/l10n/app_localizations_it.dart | 15 +++ lib/l10n/app_localizations_nl.dart | 16 +++ lib/l10n/app_localizations_pl.dart | 15 +++ lib/l10n/app_localizations_pt.dart | 15 +++ lib/l10n/app_localizations_ru.dart | 15 +++ lib/l10n/app_localizations_sk.dart | 15 +++ lib/l10n/app_localizations_sl.dart | 15 +++ lib/l10n/app_localizations_sv.dart | 15 +++ lib/l10n/app_localizations_uk.dart | 15 +++ lib/l10n/app_localizations_zh.dart | 15 +++ lib/l10n/app_nl.arb | 9 +- lib/l10n/app_pl.arb | 9 +- lib/l10n/app_pt.arb | 9 +- lib/l10n/app_ru.arb | 9 +- lib/l10n/app_sk.arb | 9 +- lib/l10n/app_sl.arb | 9 +- lib/l10n/app_sv.arb | 9 +- lib/l10n/app_uk.arb | 9 +- lib/l10n/app_zh.arb | 9 +- lib/screens/map_screen.dart | 150 +++++++++++++++++++++++++++- lib/screens/path_trace_map.dart | 153 +++++++++++++++-------------- 33 files changed, 599 insertions(+), 126 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 01afb0cb..54d792e6 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}", "notification_newTypeDiscovered": "Открит нов {contactType}", "notification_receivedNewMessage": "Получено ново съобщение", - "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.", "settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.", "settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.", "settings_gpxExportAll": "Експортирай всички контакти в GPX", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Местоположения на всички контакти", "settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open", "settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX", - "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!" - + "pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!", + "map_pathTraceCancelled": "Отменен е следването на пътя.", + "pathTrace_clearTooltip": "Изчисти пътя", + "map_removeLast": "Премахни Последно", + "map_runTrace": "Изпълни Път на Следване", + "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 66bc049a..2cece8ae 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1569,34 +1569,40 @@ "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", - "notification_activityTitle": "MeshCore Aktivität", "notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}", "@notification_messagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}", "@notification_channelMessagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}", "@notification_newNodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newTypeDiscovered": "Neuer {contactType} entdeckt", "@notification_newTypeDiscovered": { "placeholders": { - "contactType": {"type": "String"} + "contactType": { + "type": "String" + } } }, "notification_receivedNewMessage": "Neue Nachricht empfangen", - "contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.", "settings_gpxExportAll": "Alle Knoten als GPX exportieren", "settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.", "settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren", @@ -1612,6 +1618,10 @@ "settings_gpxExportAllContacts": "Alle Kontaktstandorte", "settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren", "settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert", - "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!" - + "pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!", + "map_removeLast": "Letztes Entfernen", + "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", + "map_runTrace": "Pfadverlauf ausführen", + "pathTrace_clearTooltip": "Pfad löschen", + "map_pathTraceCancelled": "Pfadverfolgung abgebrochen." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0c54be35..9fdf51ff 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -619,6 +619,10 @@ "map_sharedPin": "Shared pin", "map_joinRoom": "Join Room", "map_manageRepeater": "Manage Repeater", + "map_tapToAdd": "Tap on nodes to add them to the path.", + "map_runTrace": "Run Path Trace", + "map_removeLast": "Remove Last", + "map_pathTraceCancelled": "Path trace cancelled.", "mapCache_title": "Offline Map Cache", "mapCache_selectAreaFirst": "Select an area to cache first", "mapCache_noTilesToDownload": "No tiles to download for this area", @@ -1317,6 +1321,7 @@ "pathTrace_notAvailable": "Path trace not available.", "pathTrace_refreshTooltip": "Refresh Path Trace.", "pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!", + "pathTrace_clearTooltip": "Clear path.", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3d5ab639..f1a0651d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1569,34 +1569,40 @@ "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", - "notification_activityTitle": "Actividad de MeshCore", "notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}", "@notification_messagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}", "@notification_channelMessagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}", "@notification_newNodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newTypeDiscovered": "Nuevo {contactType} descubierto", "@notification_newTypeDiscovered": { "placeholders": { - "contactType": {"type": "String"} + "contactType": { + "type": "String" + } } }, "notification_receivedNewMessage": "Nuevo mensaje recibido", - "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.", "settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX", "settings_gpxExportSuccess": "Archivo GPX exportado con éxito.", @@ -1612,6 +1618,10 @@ "settings_gpxExportAllContacts": "Todas las ubicaciones de contactos", "settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX", - "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación" - + "pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación", + "pathTrace_clearTooltip": "Borrar ruta", + "map_runTrace": "Ejecutar Rastreo de Ruta", + "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", + "map_removeLast": "Eliminar último", + "map_pathTraceCancelled": "Rastreo de ruta cancelado." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 08275768..39d71761 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1590,5 +1590,10 @@ "settings_gpxExportAllContacts": "Tous les emplacements des contacts", "settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX", - "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !" + "pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !", + "map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.", + "pathTrace_clearTooltip": "Effacer le chemin", + "map_pathTraceCancelled": "Traçage de chemin annulé", + "map_removeLast": "Supprimer le dernier", + "map_runTrace": "Exécuter la traçage de chemin" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index dd9c3730..99f11f2d 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}", "notification_newTypeDiscovered": "Nuovo {contactType} scoperto", "notification_receivedNewMessage": "Nuovo messaggio ricevuto", - "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.", "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", "settings_gpxExportContacts": "Esporta compagni in GPX", "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Tutte le posizioni dei contatti", "settings_gpxExportShareText": "Dati mappa esportati da meshcore-open", "settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX", - "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!" - + "pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!", + "map_removeLast": "Rimuovi ultimo", + "map_pathTraceCancelled": "Tracciamento del percorso annullato.", + "pathTrace_clearTooltip": "Pulisci percorso", + "map_runTrace": "Esegui Path Trace", + "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 8f4d693e..20ac664e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2518,6 +2518,30 @@ abstract class AppLocalizations { /// **'Manage Repeater'** String get map_manageRepeater; + /// No description provided for @map_tapToAdd. + /// + /// In en, this message translates to: + /// **'Tap on nodes to add them to the path.'** + String get map_tapToAdd; + + /// No description provided for @map_runTrace. + /// + /// In en, this message translates to: + /// **'Run Path Trace'** + String get map_runTrace; + + /// No description provided for @map_removeLast. + /// + /// In en, this message translates to: + /// **'Remove Last'** + String get map_removeLast; + + /// No description provided for @map_pathTraceCancelled. + /// + /// In en, this message translates to: + /// **'Path trace cancelled.'** + String get map_pathTraceCancelled; + /// No description provided for @mapCache_title. /// /// In en, this message translates to: @@ -4730,6 +4754,12 @@ abstract class AppLocalizations { /// **'One or more of the hops is missing a location!'** String get pathTrace_someHopsNoLocation; + /// No description provided for @pathTrace_clearTooltip. + /// + /// In en, this message translates to: + /// **'Clear path.'** + String get pathTrace_clearTooltip; + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 68e821e3..30a7ca77 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1363,6 +1363,19 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_manageRepeater => 'Управление на Повтарящ се Елемент'; + @override + String get map_tapToAdd => + 'Натиснете върху възлите, за да ги добавите към пътя.'; + + @override + String get map_runTrace => 'Изпълни Път на Следване'; + + @override + String get map_removeLast => 'Премахни Последно'; + + @override + String get map_pathTraceCancelled => 'Отменен е следването на пътя.'; + @override String get mapCache_title => 'Кеш на офлайн карти'; @@ -2699,6 +2712,9 @@ class AppLocalizationsBg extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Един или повече от хмелите липсва местоположение!'; + @override + String get pathTrace_clearTooltip => 'Изчисти пътя'; + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 09672278..4c3245d8 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1362,6 +1362,19 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_manageRepeater => 'Repeater verwalten'; + @override + String get map_tapToAdd => + 'Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.'; + + @override + String get map_runTrace => 'Pfadverlauf ausführen'; + + @override + String get map_removeLast => 'Letztes Entfernen'; + + @override + String get map_pathTraceCancelled => 'Pfadverfolgung abgebrochen.'; + @override String get mapCache_title => 'Offline-Karten-Cache'; @@ -2704,6 +2717,9 @@ class AppLocalizationsDe extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Bei einer oder mehreren Knoten fehlt der Standort!'; + @override + String get pathTrace_clearTooltip => 'Pfad löschen'; + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8eb76e82..120d2426 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1342,6 +1342,18 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_manageRepeater => 'Manage Repeater'; + @override + String get map_tapToAdd => 'Tap on nodes to add them to the path.'; + + @override + String get map_runTrace => 'Run Path Trace'; + + @override + String get map_removeLast => 'Remove Last'; + + @override + String get map_pathTraceCancelled => 'Path trace cancelled.'; + @override String get mapCache_title => 'Offline Map Cache'; @@ -2659,6 +2671,9 @@ class AppLocalizationsEn extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'One or more of the hops is missing a location!'; + @override + String get pathTrace_clearTooltip => 'Clear path.'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index cc9bff79..069e3c72 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1360,6 +1360,18 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_manageRepeater => 'Gestionar Repetidor'; + @override + String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.'; + + @override + String get map_runTrace => 'Ejecutar Rastreo de Ruta'; + + @override + String get map_removeLast => 'Eliminar último'; + + @override + String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.'; + @override String get mapCache_title => 'Caché de Mapa Offline'; @@ -2698,6 +2710,9 @@ class AppLocalizationsEs extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Uno o más de los lúpulos carecen de una ubicación'; + @override + String get pathTrace_clearTooltip => 'Borrar ruta'; + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index eb6d50f5..44b70af5 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1367,6 +1367,19 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_manageRepeater => 'Gérer le répéteur'; + @override + String get map_tapToAdd => + 'Appuyez sur les nœuds pour les ajouter au chemin.'; + + @override + String get map_runTrace => 'Exécuter la traçage de chemin'; + + @override + String get map_removeLast => 'Supprimer le dernier'; + + @override + String get map_pathTraceCancelled => 'Traçage de chemin annulé'; + @override String get mapCache_title => 'Cache de Carte Hors Ligne'; @@ -2714,6 +2727,9 @@ class AppLocalizationsFr extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Un ou plusieurs des sauts manquent d\'une localisation !'; + @override + String get pathTrace_clearTooltip => 'Effacer le chemin'; + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 68465ddf..0bce5b4c 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1359,6 +1359,18 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_manageRepeater => 'Gestisci Ripetitore'; + @override + String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.'; + + @override + String get map_runTrace => 'Esegui Path Trace'; + + @override + String get map_removeLast => 'Rimuovi ultimo'; + + @override + String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.'; + @override String get mapCache_title => 'Cache Mappa Offline'; @@ -2699,6 +2711,9 @@ class AppLocalizationsIt extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Uno o più dei luppoli mancano di una posizione!'; + @override + String get pathTrace_clearTooltip => 'Pulisci percorso'; + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a093a35b..432de6c2 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1355,6 +1355,19 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_manageRepeater => 'Beheer Repeater'; + @override + String get map_tapToAdd => + 'Tik op knooppunten om ze toe te voegen aan het pad'; + + @override + String get map_runTrace => 'Padeshulp traceren'; + + @override + String get map_removeLast => 'Verwijder Laatste'; + + @override + String get map_pathTraceCancelled => 'Pad traceren geannuleerd'; + @override String get mapCache_title => 'Offline Kaarten Cache'; @@ -2689,6 +2702,9 @@ class AppLocalizationsNl extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Een of meer van de hops ontbreken een locatie!'; + @override + String get pathTrace_clearTooltip => 'Weg wissen'; + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 895e3c76..fb538934 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1361,6 +1361,18 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_manageRepeater => 'Zarządzaj Powtórzami'; + @override + String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.'; + + @override + String get map_runTrace => 'Uruchom ślad ścieżki'; + + @override + String get map_removeLast => 'Usuń ostatni'; + + @override + String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.'; + @override String get mapCache_title => 'Bufor Map Offline'; @@ -2697,6 +2709,9 @@ class AppLocalizationsPl extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Jeden lub więcej z chmieli nie ma określonej lokalizacji!'; + @override + String get pathTrace_clearTooltip => 'Wyczyść ścieżkę'; + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index ce8d07c4..f9d84154 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1361,6 +1361,18 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_manageRepeater => 'Gerenciar Repetidor'; + @override + String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.'; + + @override + String get map_runTrace => 'Executar Traçado de Caminho'; + + @override + String get map_removeLast => 'Remover Último'; + + @override + String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.'; + @override String get mapCache_title => 'Cache de Mapa Offline'; @@ -2700,6 +2712,9 @@ class AppLocalizationsPt extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Um ou mais dos lúpulos estão sem localização!'; + @override + String get pathTrace_clearTooltip => 'Limpar caminho'; + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9e4cd958..f8a90ad3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1362,6 +1362,18 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_manageRepeater => 'Управление репитером'; + @override + String get map_tapToAdd => 'Нажимайте на узлы, чтобы добавить их в путь.'; + + @override + String get map_runTrace => 'Запустить трассировку пути'; + + @override + String get map_removeLast => 'Удалить последний'; + + @override + String get map_pathTraceCancelled => 'Отмена трассировки пути'; + @override String get mapCache_title => 'Кэш офлайн-карты'; @@ -2702,6 +2714,9 @@ class AppLocalizationsRu extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Одному или нескольким хмелям не указано местоположение!'; + @override + String get pathTrace_clearTooltip => 'Очистить путь'; + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index ed66f978..7e61cc2e 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1356,6 +1356,18 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_manageRepeater => 'Spravovať Opakovanie'; + @override + String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.'; + + @override + String get map_runTrace => 'Spustiť trasovaním cesty'; + + @override + String get map_removeLast => 'Odstrániť posledný'; + + @override + String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.'; + @override String get mapCache_title => 'Offline Mapa Pamäť'; @@ -2685,6 +2697,9 @@ class AppLocalizationsSk extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Jedna alebo viac chmeľov chýba lokalita!'; + @override + String get pathTrace_clearTooltip => 'Zmazať cestu'; + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 3307547c..53da59a8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1352,6 +1352,18 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_manageRepeater => 'Upravljajte Ponovitve'; + @override + String get map_tapToAdd => 'Pritisnite na vozlišča, da jih dodate poti.'; + + @override + String get map_runTrace => 'Zaženi sledenje poti'; + + @override + String get map_removeLast => 'Odstrani Zadnji'; + + @override + String get map_pathTraceCancelled => 'Spremljanje poti je prekinjeno.'; + @override String get mapCache_title => 'Omrezni predpomnilnik zemljeških zemljejevskih slik'; @@ -2688,6 +2700,9 @@ class AppLocalizationsSl extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Ena ali več hmelju manjka lokacija!'; + @override + String get pathTrace_clearTooltip => 'Počisti pot'; + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 5239b067..d1259793 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1348,6 +1348,18 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_manageRepeater => 'Hantera Upprepare'; + @override + String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.'; + + @override + String get map_runTrace => 'Kör spårsökning'; + + @override + String get map_removeLast => 'Ta bort sista'; + + @override + String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.'; + @override String get mapCache_title => 'Offline Kartcache'; @@ -2673,6 +2685,9 @@ class AppLocalizationsSv extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'En eller flera av humlen saknar en plats!'; + @override + String get pathTrace_clearTooltip => 'Rensa väg'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index b6ff8ce5..de68840f 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1361,6 +1361,18 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_manageRepeater => 'Керувати ретранслятором'; + @override + String get map_tapToAdd => 'Натисніть на вузли, щоб додати їх до шляху'; + + @override + String get map_runTrace => 'Виконати трасування шляху'; + + @override + String get map_removeLast => 'Видалити останній'; + + @override + String get map_pathTraceCancelled => 'Відмінується трасування шляху'; + @override String get mapCache_title => 'Офлайн-кеш карти'; @@ -2709,6 +2721,9 @@ class AppLocalizationsUk extends AppLocalizations { String get pathTrace_someHopsNoLocation => 'Одне або більше хмелів відсутнє місце розташування!'; + @override + String get pathTrace_clearTooltip => 'Очистити шлях'; + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index a529a1bf..040d4ef8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1301,6 +1301,18 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_manageRepeater => '管理重复器'; + @override + String get map_tapToAdd => '点击节点将其添加到路径中'; + + @override + String get map_runTrace => '运行路径跟踪'; + + @override + String get map_removeLast => '删除最后一个'; + + @override + String get map_pathTraceCancelled => '路径跟踪已取消'; + @override String get mapCache_title => '离线地图缓存'; @@ -2557,6 +2569,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_someHopsNoLocation => '其中一个或多个啤酒花缺少位置!'; + @override + String get pathTrace_clearTooltip => '清除路径'; + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 91163ac0..033b4249 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}", "notification_newTypeDiscovered": "Nieuw {contactType} ontdekt", "notification_receivedNewMessage": "Nieuw bericht ontvangen", - "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden", "settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.", "settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX", "settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Alle contactlocaties", "settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open", "settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren", - "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!" - + "pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!", + "map_removeLast": "Verwijder Laatste", + "pathTrace_clearTooltip": "Weg wissen", + "map_pathTraceCancelled": "Pad traceren geannuleerd", + "map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad", + "map_runTrace": "Padeshulp traceren" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3c2a96fc..e0a08ddf 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}", "notification_newTypeDiscovered": "Nowy {contactType} wykryty", "notification_receivedNewMessage": "Otrzymano nową wiadomość", - "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.", "settings_gpxExportContacts": "Eksportuj towarzyszy do GPX", "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX", "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportChat": "Lokalizacje towarzyszy", "settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open", "settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open", - "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!" - + "pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!", + "map_pathTraceCancelled": "Śledzenie ścieżki anulowano.", + "map_runTrace": "Uruchom ślad ścieżki", + "pathTrace_clearTooltip": "Wyczyść ścieżkę", + "map_removeLast": "Usuń ostatni", + "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index dc38c115..20eee884 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}", "notification_newTypeDiscovered": "Novo {contactType} descoberto", "notification_receivedNewMessage": "Nova mensagem recebida", - "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.", "settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX", "settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.", "settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Todos os locais de contatos", "settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open", "settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX", - "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!" - + "pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!", + "map_runTrace": "Executar Traçado de Caminho", + "map_pathTraceCancelled": "Rastreamento de caminho cancelado.", + "pathTrace_clearTooltip": "Limpar caminho", + "map_removeLast": "Remover Último", + "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index ddbbe79d..165ccbbc 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -815,7 +815,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{новый узел} few{новых узла} many{новых узлов} other{новых узлов}}", "notification_newTypeDiscovered": "Обнаружен новый {contactType}", "notification_receivedNewMessage": "Получено новое сообщение", - "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению.", "settings_gpxExportRepeaters": "Экспортировать рипитеры / сервер комнаты в GPX", "settings_gpxExportRepeatersSubtitle": "Экспортирует ретрансляторы / сервер комнат с местоположением в файл GPX.", "settings_gpxExportContacts": "Экспортировать спутников в GPX", @@ -831,6 +830,10 @@ "settings_gpxExportNoContacts": "Нет контактов для экспорта.", "settings_gpxExportShareText": "Данные карты экспортированы из meshcore-open", "settings_gpxExportShareSubject": "meshcore-open экспорт данных карты GPX", - "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!" - + "pathTrace_someHopsNoLocation": "Одному или нескольким хмелям не указано местоположение!", + "map_tapToAdd": "Нажимайте на узлы, чтобы добавить их в путь.", + "map_removeLast": "Удалить последний", + "map_pathTraceCancelled": "Отмена трассировки пути", + "pathTrace_clearTooltip": "Очистить путь", + "map_runTrace": "Запустить трассировку пути" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index c09502a3..08439da4 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}", "notification_newTypeDiscovered": "Nový {contactType} objavený", "notification_receivedNewMessage": "Prijatá nová správa", - "contacts_ShareContact": "Kopírovať kontakt do schránky", "settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.", "settings_gpxExportContacts": "Export sprievodcov do GPX", "settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportChat": "Lokácie sprievodcov", "settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open", "settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov", - "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!" - + "pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!", + "pathTrace_clearTooltip": "Zmazať cestu", + "map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.", + "map_removeLast": "Odstrániť posledný", + "map_runTrace": "Spustiť trasovaním cesty", + "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 97a396a1..d321fd1b 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}", "notification_newTypeDiscovered": "Odkrito novo {contactType}", "notification_receivedNewMessage": "Prejeto novo sporočilo", - "contacts_ShareContact": "Kopiraj stik v Odložišče", "settings_gpxExportAll": "Izvozi vse kontakte v GPX", "settings_gpxExportContacts": "Izvoz spremljevalcev v GPX", "settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportNoContacts": "Ni stikov za izvoz.", "settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu", "settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte", - "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!" - + "pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!", + "map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.", + "map_removeLast": "Odstrani Zadnji", + "map_runTrace": "Zaženi sledenje poti", + "pathTrace_clearTooltip": "Počisti pot", + "map_pathTraceCancelled": "Spremljanje poti je prekinjeno." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6df28bdf..4b014945 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}", "notification_newTypeDiscovered": "Ny {contactType} upptäckt", "notification_receivedNewMessage": "Nytt meddelande mottaget", - "contacts_ShareContactZeroHop": "Dela kontakt via annons", "settings_gpxExportAll": "Exportera alla kontakter till GPX", "settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.", "settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång", @@ -1591,6 +1590,10 @@ "settings_gpxExportAllContacts": "Alla kontakters platser", "settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata", "settings_gpxExportShareText": "Kartdata exporterad från meshcore-open", - "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!" - + "pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!", + "pathTrace_clearTooltip": "Rensa väg", + "map_pathTraceCancelled": "Sökvägsspårning avbruten.", + "map_runTrace": "Kör spårsökning", + "map_tapToAdd": "Tryck på noder för att lägga till dem i banan.", + "map_removeLast": "Ta bort sista" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 9f5e64d2..9ff05fba 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} {count, plural, =1{новий вузол} few{нових вузли} many{нових вузлів} other{нових вузлів}}", "notification_newTypeDiscovered": "Виявлено новий {contactType}", "notification_receivedNewMessage": "Отримано нове повідомлення", - "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням", "settings_gpxExportRepeaters": "Експортувати ретранслятори / сервер кімнати до GPX", "settings_gpxExportRepeatersSubtitle": "Експортує ретранслятори / сервер кімнати з місцезнаходженням у файл GPX.", "settings_gpxExportSuccess": "Успішно експортовано файл GPX.", @@ -1591,6 +1590,10 @@ "settings_gpxExportShareText": "Дані карти експортовані з meshcore-open", "settings_gpxExportAllContacts": "Усі місця контактів", "settings_gpxExportShareSubject": "експорт даних карти meshcore-open у форматі GPX", - "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!" - + "pathTrace_someHopsNoLocation": "Одне або більше хмелів відсутнє місце розташування!", + "map_tapToAdd": "Натисніть на вузли, щоб додати їх до шляху", + "map_runTrace": "Виконати трасування шляху", + "pathTrace_clearTooltip": "Очистити шлях", + "map_removeLast": "Видалити останній", + "map_pathTraceCancelled": "Відмінується трасування шляху" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8c65510a..0f8c079c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1575,7 +1575,6 @@ "notification_newNodesCount": "{count} 个新节点", "notification_newTypeDiscovered": "发现新 {contactType}", "notification_receivedNewMessage": "收到新消息", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", @@ -1591,6 +1590,10 @@ "settings_gpxExportNoContacts": "没有联系人可导出", "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出", - "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!" - + "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!", + "map_tapToAdd": "点击节点将其添加到路径中", + "pathTrace_clearTooltip": "清除路径", + "map_pathTraceCancelled": "路径跟踪已取消", + "map_removeLast": "删除最后一个", + "map_runTrace": "运行路径跟踪" } diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index bc213f97..552f579e 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1,8 +1,10 @@ import 'dart:math'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -48,9 +50,13 @@ class _MapScreenState extends State { final MapMarkerService _markerService = MapMarkerService(); final Set _hiddenMarkerIds = {}; Set _removedMarkerIds = {}; + bool _isBuildingPathTrace = false; bool _isSelectingPoi = false; bool _hasInitializedMap = false; bool _removedMarkersLoaded = false; + final List _pathTrace = []; + final List _points = []; + final List _polylines = []; bool _legendExpanded = false; @override @@ -148,6 +154,19 @@ class _MapScreenState extends State { .where((c) => c.hasLocation) .toList(); + _polylines.clear(); + _polylines.addAll( + _points.length > 1 + ? [ + Polyline( + points: _points, + strokeWidth: 4, + color: Colors.blueAccent, + ), + ] + : [], + ); + // Calculate center and zoom of all nodes, or default to (0, 0) LatLng center = const LatLng(0, 0); double initialZoom = 10.0; @@ -248,6 +267,12 @@ class _MapScreenState extends State { centerTitle: true, automaticallyImplyLeading: false, actions: [ + if (!_isBuildingPathTrace) + IconButton( + icon: const Icon(Icons.radar), + onPressed: () => _startPath(), + tooltip: context.l10n.contacts_pathTrace, + ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( @@ -335,6 +360,8 @@ class _MapScreenState extends State { MapTileCacheService.userAgentPackageName, maxZoom: 19, ), + if (_polylines.isNotEmpty && _isBuildingPathTrace) + PolylineLayer(polylines: _polylines), MarkerLayer( markers: [ if (highlightPosition != null) @@ -391,7 +418,12 @@ class _MapScreenState extends State { ), ], ), - _buildLegend(contactsWithLocation.length, sharedMarkers.length), + if (!_isBuildingPathTrace) + _buildLegend( + contactsWithLocation.length, + sharedMarkers.length, + ), + if (_isBuildingPathTrace) _buildPathTraceOverlay(), ], ), bottomNavigationBar: SafeArea( @@ -435,7 +467,11 @@ class _MapScreenState extends State { width: 35, height: 35, child: GestureDetector( - onTap: () => _showNodeInfo(context, contact), + onLongPress: () => + _isBuildingPathTrace ? _showNodeInfo(context, contact) : null, + onTap: () => _isBuildingPathTrace + ? _addToPath(context, contact) + : _showNodeInfo(context, contact), child: Column( children: [ Container( @@ -607,7 +643,7 @@ class _MapScreenState extends State { Widget _buildLegendItem(IconData icon, String label, Color color) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 1.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -1455,6 +1491,114 @@ class _MapScreenState extends State { return context.l10n.time_allTime; } } + + void _addToPath(BuildContext context, Contact contact) { + setState(() { + _pathTrace.add( + contact.publicKey[0], + ); // Add first 16 bytes of public key to path trace + _points.add(LatLng(contact.latitude!, contact.longitude!)); + }); + } + + void _startPath() { + setState(() { + _isBuildingPathTrace = true; + _pathTrace.clear(); + _points.clear(); + _polylines.clear(); + }); + } + + void _removePath() { + setState(() { + _pathTrace.removeLast(); // Remove last node from path trace + _points.removeLast(); // Remove last point from points list + _polylines.clear(); // Clear polylines + }); + } + + Widget _buildPathTraceOverlay() { + final l10n = context.l10n; + return Positioned( + top: 16, + left: 16, + right: 16, + child: Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.contacts_pathTrace, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (_pathTrace.isEmpty) const SizedBox(height: 8), + if (_pathTrace.isEmpty) + Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), + const SizedBox(height: 6), + if (_pathTrace.isNotEmpty) + Text( + "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}", + style: TextStyle(fontSize: 12, color: Colors.grey[700]), + ), + SelectableText( + _pathTrace + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(','), + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 6), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_pathTrace.isNotEmpty) + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PathTraceMapScreen( + title: l10n.contacts_pathTrace, + path: Uint8List.fromList(_pathTrace), + ), + ), + ); + setState(() { + _isBuildingPathTrace = false; + }); + }, + child: Text(l10n.map_runTrace), + ), + if (_pathTrace.isNotEmpty) + ElevatedButton( + onPressed: _removePath, + child: Text(l10n.map_removeLast), + ), + if (_pathTrace.isEmpty) + ElevatedButton( + onPressed: () { + setState(() { + _isBuildingPathTrace = false; + _pathTrace.clear(); + _points.clear(); + _polylines.clear(); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.map_pathTraceCancelled)), + ); + }, + child: Text(l10n.common_cancel), + ), + ], + ), + ], + ), + ), + ), + ); + } } class _MarkerPayload { diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 39de31e9..5b19c30a 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -13,6 +13,23 @@ import 'package:meshcore_open/services/map_tile_cache_service.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; import 'package:provider/provider.dart'; +double getPathDistanceMeters(List points) { + if (points.length <= 1) return 0.0; + + double distanceMeters = 0.0; + final distanceCalculator = Distance(); + + for (int i = 0; i < points.length - 1; i++) { + distanceMeters += distanceCalculator(points[i], points[i + 1]); + } + + return distanceMeters; +} + +String formatDistance(double distanceMeters) { + return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)'; +} + class PathTraceData { final Uint8List pathData; final Uint8List snrData; @@ -50,7 +67,6 @@ class _PathTraceMapScreenState extends State { bool _isLoading = false; bool _failed2Loaded = false; bool _hasData = false; - bool _noLocationErr = false; PathTraceData? _traceData; List _points = []; List _polylines = []; @@ -58,7 +74,7 @@ class _PathTraceMapScreenState extends State { double _initialZoom = 2.0; LatLngBounds? _bounds; ValueKey _mapKey = const ValueKey('initial'); - double _pathDistance = 0.0; + double _pathDistanceMeters = 0.0; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -93,23 +109,11 @@ class _PathTraceMapScreenState extends State { return traceBytes; } - double getPathDistance() { - double totalDistance = 0.0; - final distanceCalculator = Distance(); - - for (int i = 0; i < _points.length - 1; i++) { - totalDistance += distanceCalculator(_points[i], _points[i + 1]); - } - - return totalDistance; - } - Future _doPathTrace() async { if (mounted) { setState(() { _isLoading = true; _failed2Loaded = false; - _noLocationErr = false; }); } @@ -160,6 +164,14 @@ class _PathTraceMapScreenState extends State { }); } + if (code == respCodeErr) { + _timeoutTimer?.cancel(); + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + } // Check if it's a binary response if (frame.length > 8 && code == pushCodeTraceData && @@ -215,8 +227,6 @@ class _PathTraceMapScreenState extends State { contact.latitude != null && contact.longitude != null) { _points.add(LatLng(contact.latitude!, contact.longitude!)); - } else { - _noLocationErr = true; } } _polylines = _points.length > 1 @@ -235,7 +245,7 @@ class _PathTraceMapScreenState extends State { _mapKey = ValueKey( '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', ); - _pathDistance = getPathDistance(); + _pathDistanceMeters = getPathDistanceMeters(_points); }); } @@ -279,20 +289,7 @@ class _PathTraceMapScreenState extends State { top: false, child: Stack( children: [ - if (_noLocationErr) - Center( - child: Card( - color: Colors.red, - child: Padding( - padding: EdgeInsets.all(12), - child: Text( - context.l10n.pathTrace_someHopsNoLocation, - style: TextStyle(color: Colors.white), - ), - ), - ), - ), - if (!_hasData && !_noLocationErr) + if (!_hasData) Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -304,43 +301,11 @@ class _PathTraceMapScreenState extends State { ], ), ), - if (_hasData && !_noLocationErr) - FlutterMap( - key: _mapKey, - options: MapOptions( - initialCenter: _initialCenter!, - initialZoom: _initialZoom, - initialCameraFit: _bounds == null - ? null - : CameraFit.bounds( - bounds: _bounds!, - padding: const EdgeInsets.all(64), - maxZoom: 16, - ), - minZoom: 2.0, - maxZoom: 18.0, - ), - children: [ - TileLayer( - urlTemplate: kMapTileUrlTemplate, - tileProvider: tileCache.tileProvider, - userAgentPackageName: - MapTileCacheService.userAgentPackageName, - maxZoom: 19, - ), - if (_polylines.isNotEmpty) - PolylineLayer(polylines: _polylines), - if (_traceData!.pathData.isNotEmpty) - MarkerLayer( - markers: _buildHopMarkers(_traceData!.pathData), - ), - ], - ), + if (_hasData) _buildMapPathTrace(context, tileCache), if (_points.isEmpty && !_hasData && !_isLoading && - !_failed2Loaded && - !_noLocationErr) + !_failed2Loaded) Center( child: Card( color: Colors.white.withValues(alpha: 0.9), @@ -352,8 +317,7 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (_hasData && !_noLocationErr) - _buildLegendCard(context, _traceData!), + if (_hasData) _buildLegendCard(context, _traceData!), ], ), ), @@ -365,7 +329,8 @@ class _PathTraceMapScreenState extends State { List _buildHopMarkers(List pathData) { return [ for (final hop in pathData) - if (_traceData!.pathContacts[hop]!.hasLocation) + if (_traceData!.pathContacts[hop] != null && + _traceData!.pathContacts[hop]!.hasLocation) Marker( point: LatLng( _traceData!.pathContacts[hop]!.latitude!, @@ -453,7 +418,9 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } } else { final contactName = @@ -462,7 +429,9 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } } @@ -475,7 +444,9 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } else { return context.l10n.pathTrace_you; } @@ -486,10 +457,46 @@ class _PathTraceMapScreenState extends State { .toRadixString(16) .padLeft(2, '0') .toUpperCase(); - return contactName != null ? "$hex: $contactName" : hex; + return contactName != null + ? "$hex: $contactName" + : "$hex: ${context.l10n.channelPath_unknownRepeater}"; } } + Widget _buildMapPathTrace( + BuildContext context, + MapTileCacheService tileCache, + ) { + return FlutterMap( + key: _mapKey, + options: MapOptions( + interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate), + initialCenter: _initialCenter!, + initialZoom: _initialZoom, + initialCameraFit: _bounds == null + ? null + : CameraFit.bounds( + bounds: _bounds!, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + minZoom: 2.0, + maxZoom: 18.0, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines), + if (_traceData!.pathData.isNotEmpty) + MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)), + ], + ); + } + Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) { final l10n = context.l10n; final maxHeight = MediaQuery.of(context).size.height * 0.35; @@ -509,7 +516,7 @@ class _PathTraceMapScreenState extends State { Padding( padding: const EdgeInsets.all(12), child: Text( - '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}', style: const TextStyle(fontWeight: FontWeight.w600), ), ), From b526175be4ee978dbb136b058c07f08d49a25b5a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 01:13:06 -0700 Subject: [PATCH 21/99] bump version for android --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e1ae0231..fa3bd63c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 6.0.0+1 +version: 6.0.0+7 environment: sdk: ^3.9.2 From 37db955ab2d344bf1cbfd5fb1f0e561038eca0c4 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 01:46:40 -0700 Subject: [PATCH 22/99] Fixed banner flash, added enable bluetooth button fixed theme to use app theme colors removed FAB overrides because material 3 does this for us, fixed missing translations. --- lib/l10n/app_bg.arb | 5 +- lib/l10n/app_de.arb | 5 +- lib/l10n/app_en.arb | 1 + lib/l10n/app_es.arb | 5 +- lib/l10n/app_fr.arb | 5 +- lib/l10n/app_it.arb | 5 +- lib/l10n/app_localizations.dart | 18 +++++++ lib/l10n/app_localizations_bg.dart | 10 ++++ lib/l10n/app_localizations_de.dart | 10 ++++ lib/l10n/app_localizations_en.dart | 10 ++++ lib/l10n/app_localizations_es.dart | 10 ++++ lib/l10n/app_localizations_fr.dart | 10 ++++ lib/l10n/app_localizations_it.dart | 10 ++++ lib/l10n/app_localizations_nl.dart | 10 ++++ lib/l10n/app_localizations_pl.dart | 10 ++++ lib/l10n/app_localizations_pt.dart | 10 ++++ lib/l10n/app_localizations_ru.dart | 10 ++++ lib/l10n/app_localizations_sk.dart | 10 ++++ lib/l10n/app_localizations_sl.dart | 10 ++++ lib/l10n/app_localizations_sv.dart | 10 ++++ lib/l10n/app_localizations_uk.dart | 10 ++++ lib/l10n/app_localizations_zh.dart | 9 ++++ lib/l10n/app_nl.arb | 5 +- lib/l10n/app_pl.arb | 5 +- lib/l10n/app_pt.arb | 5 +- lib/l10n/app_ru.arb | 5 +- lib/l10n/app_sk.arb | 5 +- lib/l10n/app_sl.arb | 5 +- lib/l10n/app_sv.arb | 5 +- lib/l10n/app_uk.arb | 5 +- lib/l10n/app_zh.arb | 5 +- lib/screens/scanner_screen.dart | 32 +++++------ pubspec.lock | 20 +++---- untranslated.json | 86 +----------------------------- 34 files changed, 251 insertions(+), 125 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 54d792e6..a1cfb3ed 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "Изчисти пътя", "map_removeLast": "Премахни Последно", "map_runTrace": "Изпълни Път на Следване", - "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя." + "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", + "scanner_bluetoothOff": "Bluetooth е изключен.", + "scanner_enableBluetooth": "Активирайте Bluetooth", + "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2cece8ae..2e662227 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1623,5 +1623,8 @@ "map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.", "map_runTrace": "Pfadverlauf ausführen", "pathTrace_clearTooltip": "Pfad löschen", - "map_pathTraceCancelled": "Pfadverfolgung abgebrochen." + "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", + "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", + "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", + "scanner_enableBluetooth": "Bluetooth aktivieren" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9f24f46a..890b992e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -68,6 +68,7 @@ "scanner_scan": "Scan", "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", + "scanner_enableBluetooth": "Enable Bluetooth", "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index f1a0651d..e6b22fad 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1623,5 +1623,8 @@ "map_runTrace": "Ejecutar Rastreo de Ruta", "map_tapToAdd": "Pulse en los nodos para agregarlos al camino.", "map_removeLast": "Eliminar último", - "map_pathTraceCancelled": "Rastreo de ruta cancelado." + "map_pathTraceCancelled": "Rastreo de ruta cancelado.", + "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", + "scanner_bluetoothOff": "Bluetooth está desactivado.", + "scanner_enableBluetooth": "Habilitar Bluetooth" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 39d71761..f11238bf 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "Effacer le chemin", "map_pathTraceCancelled": "Traçage de chemin annulé", "map_removeLast": "Supprimer le dernier", - "map_runTrace": "Exécuter la traçage de chemin" + "map_runTrace": "Exécuter la traçage de chemin", + "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", + "scanner_bluetoothOff": "Le Bluetooth est désactivé.", + "scanner_enableBluetooth": "Activer le Bluetooth" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 99f11f2d..601f1af0 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1595,5 +1595,8 @@ "map_pathTraceCancelled": "Tracciamento del percorso annullato.", "pathTrace_clearTooltip": "Pulisci percorso", "map_runTrace": "Esegui Path Trace", - "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso." + "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", + "scanner_bluetoothOff": "Il Bluetooth è disattivato.", + "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", + "scanner_enableBluetooth": "Abilita il Bluetooth" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 20ac664e..bc1cfbd7 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -376,6 +376,24 @@ abstract class AppLocalizations { /// **'Scan'** String get scanner_scan; + /// No description provided for @scanner_bluetoothOff. + /// + /// In en, this message translates to: + /// **'Bluetooth is off'** + String get scanner_bluetoothOff; + + /// No description provided for @scanner_bluetoothOffMessage. + /// + /// In en, this message translates to: + /// **'Please turn on Bluetooth to scan for devices'** + String get scanner_bluetoothOffMessage; + + /// No description provided for @scanner_enableBluetooth. + /// + /// In en, this message translates to: + /// **'Enable Bluetooth'** + String get scanner_enableBluetooth; + /// No description provided for @device_quickSwitch. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 30a7ca77..695bde29 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -143,6 +143,16 @@ class AppLocalizationsBg extends AppLocalizations { @override String get scanner_scan => 'Сканирай'; + @override + String get scanner_bluetoothOff => 'Bluetooth е изключен.'; + + @override + String get scanner_bluetoothOffMessage => + 'Моля, активирайте Bluetooth, за да сканирате за устройства.'; + + @override + String get scanner_enableBluetooth => 'Активирайте Bluetooth'; + @override String get device_quickSwitch => 'Бързо превключване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 4c3245d8..6e04655b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -143,6 +143,16 @@ class AppLocalizationsDe extends AppLocalizations { @override String get scanner_scan => 'Scannen'; + @override + String get scanner_bluetoothOff => 'Bluetooth ist deaktiviert.'; + + @override + String get scanner_bluetoothOffMessage => + 'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.'; + + @override + String get scanner_enableBluetooth => 'Bluetooth aktivieren'; + @override String get device_quickSwitch => 'Schnelles Umschalten'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 120d2426..5ed81627 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -142,6 +142,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get scanner_scan => 'Scan'; + @override + String get scanner_bluetoothOff => 'Bluetooth is off'; + + @override + String get scanner_bluetoothOffMessage => + 'Please turn on Bluetooth to scan for devices'; + + @override + String get scanner_enableBluetooth => 'Enable Bluetooth'; + @override String get device_quickSwitch => 'Quick switch'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 069e3c72..ff4e8f36 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -143,6 +143,16 @@ class AppLocalizationsEs extends AppLocalizations { @override String get scanner_scan => 'Escanea'; + @override + String get scanner_bluetoothOff => 'Bluetooth está desactivado.'; + + @override + String get scanner_bluetoothOffMessage => + 'Por favor, active el Bluetooth para escanear dispositivos.'; + + @override + String get scanner_enableBluetooth => 'Habilitar Bluetooth'; + @override String get device_quickSwitch => 'Cambiar rápidamente'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 44b70af5..f8b77757 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -143,6 +143,16 @@ class AppLocalizationsFr extends AppLocalizations { @override String get scanner_scan => 'Scanner'; + @override + String get scanner_bluetoothOff => 'Le Bluetooth est désactivé.'; + + @override + String get scanner_bluetoothOffMessage => + 'Veuillez activer le Bluetooth pour rechercher des appareils.'; + + @override + String get scanner_enableBluetooth => 'Activer le Bluetooth'; + @override String get device_quickSwitch => 'Basculement rapide'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 0bce5b4c..d8fd6128 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -143,6 +143,16 @@ class AppLocalizationsIt extends AppLocalizations { @override String get scanner_scan => 'Scansiona'; + @override + String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.'; + + @override + String get scanner_bluetoothOffMessage => + 'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.'; + + @override + String get scanner_enableBluetooth => 'Abilita il Bluetooth'; + @override String get device_quickSwitch => 'Passa velocemente'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 432de6c2..de6c9099 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -142,6 +142,16 @@ class AppLocalizationsNl extends AppLocalizations { @override String get scanner_scan => 'Scan'; + @override + String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld'; + + @override + String get scanner_bluetoothOffMessage => + 'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.'; + + @override + String get scanner_enableBluetooth => 'Activeer Bluetooth'; + @override String get device_quickSwitch => 'Snelle overschakeling'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index fb538934..c5d2bd9e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -143,6 +143,16 @@ class AppLocalizationsPl extends AppLocalizations { @override String get scanner_scan => 'Przeskanuj'; + @override + String get scanner_bluetoothOff => 'Bluetooth jest wyłączony'; + + @override + String get scanner_bluetoothOffMessage => + 'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.'; + + @override + String get scanner_enableBluetooth => 'Włącz Bluetooth'; + @override String get device_quickSwitch => 'Szybka zmiana'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index f9d84154..b5ffdd6f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -143,6 +143,16 @@ class AppLocalizationsPt extends AppLocalizations { @override String get scanner_scan => 'Digitalizar'; + @override + String get scanner_bluetoothOff => 'Bluetooth está desativado'; + + @override + String get scanner_bluetoothOffMessage => + 'Por favor, ative o Bluetooth para escanear por dispositivos.'; + + @override + String get scanner_enableBluetooth => 'Ative o Bluetooth'; + @override String get device_quickSwitch => 'Mudar rapidamente'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index f8a90ad3..c41bf20c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -142,6 +142,16 @@ class AppLocalizationsRu extends AppLocalizations { @override String get scanner_scan => 'Сканирование'; + @override + String get scanner_bluetoothOff => 'Bluetooth выключен'; + + @override + String get scanner_bluetoothOffMessage => + 'Пожалуйста, включите Bluetooth, чтобы найти устройства.'; + + @override + String get scanner_enableBluetooth => 'Включите Bluetooth'; + @override String get device_quickSwitch => 'Быстрое переключение'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 7e61cc2e..e0ee455e 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -143,6 +143,16 @@ class AppLocalizationsSk extends AppLocalizations { @override String get scanner_scan => 'Skončiť'; + @override + String get scanner_bluetoothOff => 'Bluetooth je vypnutý'; + + @override + String get scanner_bluetoothOffMessage => + 'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.'; + + @override + String get scanner_enableBluetooth => 'Povolte Bluetooth'; + @override String get device_quickSwitch => 'Rýchle prepínač'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 53da59a8..36445f77 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -143,6 +143,16 @@ class AppLocalizationsSl extends AppLocalizations { @override String get scanner_scan => 'Skeniraj'; + @override + String get scanner_bluetoothOff => 'Bluetooth je izklopljen'; + + @override + String get scanner_bluetoothOffMessage => + 'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.'; + + @override + String get scanner_enableBluetooth => 'Omogočite Bluetooth'; + @override String get device_quickSwitch => 'Hitro preklop'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index d1259793..cbfa45d8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -142,6 +142,16 @@ class AppLocalizationsSv extends AppLocalizations { @override String get scanner_scan => 'Skanna'; + @override + String get scanner_bluetoothOff => 'Bluetooth är avstängt'; + + @override + String get scanner_bluetoothOffMessage => + 'Vänligen aktivera Bluetooth för att söka efter enheter.'; + + @override + String get scanner_enableBluetooth => 'Aktivera Bluetooth'; + @override String get device_quickSwitch => 'Snabb växling'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index de68840f..4dfa2606 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -143,6 +143,16 @@ class AppLocalizationsUk extends AppLocalizations { @override String get scanner_scan => 'Сканувати'; + @override + String get scanner_bluetoothOff => 'Bluetooth вимкнено'; + + @override + String get scanner_bluetoothOffMessage => + 'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.'; + + @override + String get scanner_enableBluetooth => 'Увімкніть Bluetooth'; + @override String get device_quickSwitch => 'Швидке перемикання'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 040d4ef8..4441b222 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -142,6 +142,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get scanner_scan => '扫描'; + @override + String get scanner_bluetoothOff => '蓝牙已关闭'; + + @override + String get scanner_bluetoothOffMessage => '请打开蓝牙功能,以便搜索设备。'; + + @override + String get scanner_enableBluetooth => '启用蓝牙'; + @override String get device_quickSwitch => '快速切换'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 033b4249..b150d62b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "Weg wissen", "map_pathTraceCancelled": "Pad traceren geannuleerd", "map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad", - "map_runTrace": "Padeshulp traceren" + "map_runTrace": "Padeshulp traceren", + "scanner_enableBluetooth": "Activeer Bluetooth", + "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", + "scanner_bluetoothOff": "Bluetooth is uitgeschakeld" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index e0a08ddf..d576fca8 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1595,5 +1595,8 @@ "map_runTrace": "Uruchom ślad ścieżki", "pathTrace_clearTooltip": "Wyczyść ścieżkę", "map_removeLast": "Usuń ostatni", - "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki." + "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", + "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", + "scanner_bluetoothOff": "Bluetooth jest wyłączony", + "scanner_enableBluetooth": "Włącz Bluetooth" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 20eee884..53c43fe7 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1595,5 +1595,8 @@ "map_pathTraceCancelled": "Rastreamento de caminho cancelado.", "pathTrace_clearTooltip": "Limpar caminho", "map_removeLast": "Remover Último", - "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho." + "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", + "scanner_enableBluetooth": "Ative o Bluetooth", + "scanner_bluetoothOff": "Bluetooth está desativado", + "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 165ccbbc..19b49907 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -835,5 +835,8 @@ "map_removeLast": "Удалить последний", "map_pathTraceCancelled": "Отмена трассировки пути", "pathTrace_clearTooltip": "Очистить путь", - "map_runTrace": "Запустить трассировку пути" + "map_runTrace": "Запустить трассировку пути", + "scanner_enableBluetooth": "Включите Bluetooth", + "scanner_bluetoothOff": "Bluetooth выключен", + "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 08439da4..3f612922 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1595,5 +1595,8 @@ "map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.", "map_removeLast": "Odstrániť posledný", "map_runTrace": "Spustiť trasovaním cesty", - "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené." + "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", + "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", + "scanner_bluetoothOff": "Bluetooth je vypnutý", + "scanner_enableBluetooth": "Povolte Bluetooth" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index d321fd1b..d94695e3 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1595,5 +1595,8 @@ "map_removeLast": "Odstrani Zadnji", "map_runTrace": "Zaženi sledenje poti", "pathTrace_clearTooltip": "Počisti pot", - "map_pathTraceCancelled": "Spremljanje poti je prekinjeno." + "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", + "scanner_enableBluetooth": "Omogočite Bluetooth", + "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", + "scanner_bluetoothOff": "Bluetooth je izklopljen" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 4b014945..59b3fca7 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1595,5 +1595,8 @@ "map_pathTraceCancelled": "Sökvägsspårning avbruten.", "map_runTrace": "Kör spårsökning", "map_tapToAdd": "Tryck på noder för att lägga till dem i banan.", - "map_removeLast": "Ta bort sista" + "map_removeLast": "Ta bort sista", + "scanner_enableBluetooth": "Aktivera Bluetooth", + "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", + "scanner_bluetoothOff": "Bluetooth är avstängt" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 9ff05fba..26f3984e 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1595,5 +1595,8 @@ "map_runTrace": "Виконати трасування шляху", "pathTrace_clearTooltip": "Очистити шлях", "map_removeLast": "Видалити останній", - "map_pathTraceCancelled": "Відмінується трасування шляху" + "map_pathTraceCancelled": "Відмінується трасування шляху", + "scanner_enableBluetooth": "Увімкніть Bluetooth", + "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", + "scanner_bluetoothOff": "Bluetooth вимкнено" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 0f8c079c..7b4b3ab5 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1595,5 +1595,8 @@ "pathTrace_clearTooltip": "清除路径", "map_pathTraceCancelled": "路径跟踪已取消", "map_removeLast": "删除最后一个", - "map_runTrace": "运行路径跟踪" + "map_runTrace": "运行路径跟踪", + "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", + "scanner_bluetoothOff": "蓝牙已关闭", + "scanner_enableBluetooth": "启用蓝牙" } diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 74281456..70f00a0a 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -80,7 +80,7 @@ class _ScannerScreenState extends State { return Column( children: [ // Bluetooth off warning - if (_bluetoothState != BluetoothAdapterState.on) + if (_bluetoothState == BluetoothAdapterState.off) _bluetoothOffWarning(context), // Status bar @@ -97,23 +97,18 @@ class _ScannerScreenState extends State { builder: (context, connector, child) { final isScanning = connector.state == MeshCoreConnectionState.scanning; - final isBluetoothOn = _bluetoothState == BluetoothAdapterState.on; + final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off; return FloatingActionButton.extended( - onPressed: isBluetoothOn - ? () { + onPressed: isBluetoothOff + ? null + : () { if (isScanning) { connector.stopScan(); } else { connector.startScan(); } - } - : null, - backgroundColor: isBluetoothOn ? null : Colors.grey, - foregroundColor: isBluetoothOn ? null : Colors.white, - mouseCursor: isBluetoothOn - ? SystemMouseCursors.click - : SystemMouseCursors.forbidden, + }, icon: isScanning ? const SizedBox( width: 20, @@ -236,13 +231,14 @@ class _ScannerScreenState extends State { } Widget _bluetoothOffWarning(BuildContext context) { + final errorColor = Theme.of(context).colorScheme.error; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - color: Colors.red.withValues(alpha: 0.15), + color: errorColor.withValues(alpha: 0.15), child: Row( children: [ - const Icon(Icons.bluetooth_disabled, size: 24, color: Colors.red), + Icon(Icons.bluetooth_disabled, size: 24, color: errorColor), const SizedBox(width: 12), Expanded( child: Column( @@ -250,8 +246,8 @@ class _ScannerScreenState extends State { children: [ Text( context.l10n.scanner_bluetoothOff, - style: const TextStyle( - color: Colors.red, + style: TextStyle( + color: errorColor, fontWeight: FontWeight.w600, fontSize: 14, ), @@ -260,13 +256,17 @@ class _ScannerScreenState extends State { Text( context.l10n.scanner_bluetoothOffMessage, style: TextStyle( - color: Colors.red.withValues(alpha: 0.85), + color: errorColor.withValues(alpha: 0.85), fontSize: 12, ), ), ], ), ), + TextButton( + onPressed: () => FlutterBluePlus.turnOn(), + child: Text(context.l10n.scanner_enableBluetooth), + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 207ff51e..fc116566 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -489,26 +489,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: diff --git a/untranslated.json b/untranslated.json index 2b5dc439..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,85 +1 @@ -{ - "bg": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "de": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "es": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "fr": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "it": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "nl": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "pl": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "pt": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "ru": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "sk": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "sl": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "sv": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "uk": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ], - - "zh": [ - "scanner_bluetoothOff", - "scanner_bluetoothOffMessage", - "scanner_enableBluetooth" - ] -} +{} \ No newline at end of file From 9250dfec31ca68359642d740afc36251c6e62e59 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 01:54:30 -0700 Subject: [PATCH 23/99] Gate the turn on BLE button to android --- lib/screens/scanner_screen.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 70f00a0a..932e29cf 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -263,10 +264,11 @@ class _ScannerScreenState extends State { ], ), ), - TextButton( - onPressed: () => FlutterBluePlus.turnOn(), - child: Text(context.l10n.scanner_enableBluetooth), - ), + if (Platform.isAndroid) + TextButton( + onPressed: () => FlutterBluePlus.turnOn(), + child: Text(context.l10n.scanner_enableBluetooth), + ), ], ), ); From 72f0aa7208cb52b91eb3c21855d4defbc87e1003 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Sat, 14 Feb 2026 02:22:45 -0700 Subject: [PATCH 24/99] Update dependencies and improve code consistency across multiple files --- android/app/build.gradle.kts | 2 +- lib/screens/app_debug_log_screen.dart | 2 +- lib/screens/ble_debug_log_screen.dart | 2 +- lib/screens/channel_message_path_screen.dart | 2 +- lib/screens/path_trace_map.dart | 2 +- lib/screens/settings_screen.dart | 4 +- lib/services/background_service.dart | 16 +-- lib/services/notification_service.dart | 36 +++--- lib/widgets/qr_scanner_widget.dart | 2 +- pubspec.lock | 116 +++++++++---------- pubspec.yaml | 16 +-- windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 98 insertions(+), 103 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 740451b9..e7d2b428 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -83,5 +83,5 @@ flutter { } dependencies { - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") } diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index e8a0aa49..5372ea83 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -55,7 +55,7 @@ class AppDebugLogScreen extends StatelessWidget { child: hasEntries ? ListView.separated( itemCount: entries.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final entry = entries[index]; return ListTile( diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 7675caea..7cebb761 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -100,7 +100,7 @@ class _BleDebugLogScreenState extends State { itemCount: showingFrames ? entries.length : rawEntries.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { if (showingFrames) { final entry = entries[index]; diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 8dea4758..1b0544ca 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -594,7 +594,7 @@ class _ChannelMessagePathMapScreenState : ListView.separated( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: hops.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final hop = hops[index]; return ListTile( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 5b19c30a..7677d0df 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -530,7 +530,7 @@ class _PathTraceMapScreenState extends State { child: ListView.separated( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: pathTraceData.pathData.length + 1, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { return Column( children: [ diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 4943284a..73376f05 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -742,7 +742,7 @@ class _SettingsScreenState extends State { ); } - _gpxExport( + Future _gpxExport( GpxExport exporter, String name, String description, @@ -782,7 +782,7 @@ class _SettingsScreenState extends State { } } - _buildExportCard(MeshCoreConnector connector) { + Widget _buildExportCard(MeshCoreConnector connector) { final l10n = context.l10n; return Card( child: Column( diff --git a/lib/services/background_service.dart b/lib/services/background_service.dart index 6599fbc4..0edd3935 100644 --- a/lib/services/background_service.dart +++ b/lib/services/background_service.dart @@ -1,4 +1,3 @@ -import 'dart:isolate'; import 'dart:io'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; @@ -15,18 +14,13 @@ class BackgroundService { channelDescription: 'Keeps MeshCore running in the background.', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.LOW, - iconData: const NotificationIconData( - resType: ResourceType.mipmap, - resPrefix: ResourcePrefix.ic, - name: 'launcher', - ), ), iosNotificationOptions: const IOSNotificationOptions( showNotification: false, playSound: false, ), - foregroundTaskOptions: const ForegroundTaskOptions( - interval: 5000, + foregroundTaskOptions: ForegroundTaskOptions( + eventAction: ForegroundTaskEventAction.repeat(5000), autoRunOnBoot: false, allowWifiLock: false, ), @@ -63,13 +57,13 @@ void startCallback() { class _MeshCoreTaskHandler extends TaskHandler { @override - void onStart(DateTime timestamp, SendPort? sendPort) {} + Future onStart(DateTime timestamp, TaskStarter starter) async {} @override - void onRepeatEvent(DateTime timestamp, SendPort? sendPort) {} + void onRepeatEvent(DateTime timestamp) {} @override - void onDestroy(DateTime timestamp, SendPort? sendPort) {} + Future onDestroy(DateTime timestamp, bool isTimeout) async {} @override void onNotificationButtonPressed(String id) {} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index 57331aa2..fc979c6b 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -67,7 +67,7 @@ class NotificationService { try { await _notifications.initialize( - initSettings, + settings: initSettings, onDidReceiveNotificationResponse: _onNotificationTapped, ); _isInitialized = true; @@ -149,10 +149,10 @@ class NotificationService { ); await _notifications.show( - contactId?.hashCode ?? 0, - contactName, - message, - notificationDetails, + id: contactId?.hashCode ?? 0, + title: contactName, + body: message, + notificationDetails: notificationDetails, payload: 'message:$contactId', ); } @@ -194,10 +194,10 @@ class NotificationService { ); await _notifications.show( - contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - _l10n.notification_newTypeDiscovered(contactType), - contactName, - notificationDetails, + id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: _l10n.notification_newTypeDiscovered(contactType), + body: contactName, + notificationDetails: notificationDetails, payload: 'advert:$contactId', ); } @@ -248,10 +248,10 @@ class NotificationService { : preview; await _notifications.show( - channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - channelName, - body, - notificationDetails, + id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: channelName, + body: body, + notificationDetails: notificationDetails, payload: 'channel:$channelIndex', ); } @@ -285,7 +285,7 @@ class NotificationService { } Future cancel(int id) async { - await _notifications.cancel(id); + await _notifications.cancel(id: id); } // ───────────────────────────────────────────────────────────────── @@ -469,10 +469,10 @@ class NotificationService { const notificationDetails = NotificationDetails(android: androidDetails); await _notifications.show( - 'batch_summary'.hashCode, - _l10n.notification_activityTitle, - parts.join(', '), - notificationDetails, + id: 'batch_summary'.hashCode, + title: _l10n.notification_activityTitle, + body: parts.join(', '), + notificationDetails: notificationDetails, payload: 'batch', ); } diff --git a/lib/widgets/qr_scanner_widget.dart b/lib/widgets/qr_scanner_widget.dart index 4dc2ee51..5b6cf5e3 100644 --- a/lib/widgets/qr_scanner_widget.dart +++ b/lib/widgets/qr_scanner_widget.dart @@ -156,7 +156,7 @@ class _QrScannerWidgetState extends State MobileScanner( controller: _controller, onDetect: _handleDetection, - errorBuilder: (context, error, child) { + errorBuilder: (context, error) { return _buildErrorWidget(context, error); }, ), diff --git a/pubspec.lock b/pubspec.lock index fc116566..09e93017 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -153,14 +153,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" dbus: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" fake_async: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: ffi - sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" file: dependency: transitive description: @@ -202,10 +210,10 @@ packages: dependency: "direct main" description: name: flutter_blue_plus - sha256: "399b3dbc15562ef59749f04e43a99ccbb91540022380d5f269aff3c2787534e4" + sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" flutter_blue_plus_android: dependency: transitive description: @@ -218,10 +226,10 @@ packages: dependency: transitive description: name: flutter_blue_plus_darwin - sha256: d160a8128e3a016fa58dd65ab6dac05cbc73e0fa799a1f24211d041641ed63ba + sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "8.1.1" flutter_blue_plus_linux: dependency: transitive description: @@ -250,10 +258,10 @@ packages: dependency: transitive description: name: flutter_blue_plus_winrt - sha256: "34be2d8e23d5881b46accebb0e71025f7d52869d72ea98b5082c20764e06aa80" + sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 url: "https://pub.dev" source: hosted - version: "0.0.16" + version: "0.0.18" flutter_cache_manager: dependency: "direct main" description: @@ -266,18 +274,18 @@ packages: dependency: "direct main" description: name: flutter_foreground_task - sha256: "6cf10a27f5e344cd2ecad0752d3a5f4ec32846d82fda8753b3fe2480ebb832a3" + sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "9.2.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.4" flutter_linkify: dependency: "direct main" description: @@ -290,34 +298,42 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610 + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" url: "https://pub.dev" source: hosted - version: "18.0.1" + version: "20.1.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01" + sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "7.0.0" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52" + sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "10.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a + url: "https://pub.dev" + source: hosted + version: "2.0.1" flutter_localizations: dependency: "direct main" description: flutter @@ -327,10 +343,10 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "2ecb34619a4be19df6f40c2f8dce1591675b4eff7a6857bd8f533706977385da" + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "8.2.2" flutter_test: dependency: "direct dev" description: flutter @@ -361,10 +377,10 @@ packages: dependency: transitive description: name: hooks - sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" http: dependency: "direct main" description: @@ -397,14 +413,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: transitive description: @@ -457,10 +465,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.1.0" lists: dependency: transitive description: @@ -529,10 +537,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 url: "https://pub.dev" source: hosted - version: "6.0.11" + version: "7.1.4" native_toolchain_c: dependency: transitive description: @@ -553,10 +561,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "983c7fa1501f6dcc0cb7af4e42072e9993cb28d73604d25ebf4dab08165d997e" + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" url: "https://pub.dev" source: hosted - version: "9.2.5" + version: "9.3.0" octo_image: dependency: transitive description: @@ -569,10 +577,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d url: "https://pub.dev" source: hosted - version: "8.3.1" + version: "9.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -665,18 +673,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" - polylabel: - dependency: transitive - description: - name: polylabel - sha256: "41b9099afb2aa6c1730bdd8a0fab1400d287694ec7615dd8516935fa3144214b" - url: "https://pub.dev" - source: hosted - version: "1.0.1" + version: "4.0.0" posix: dependency: transitive description: @@ -822,10 +822,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" sqflite: dependency: transitive description: @@ -958,10 +958,10 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.6" + version: "6.4.1" url_launcher_linux: dependency: transitive description: @@ -1030,10 +1030,10 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: "61713aa82b7f85c21c9f4cd0a148abd75f38a74ec645fcb1e446f882c82fd09b" + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.4.0" wakelock_plus_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fa3bd63c..f5ceaafc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,19 +41,19 @@ dependencies: provider: ^6.1.5+1 shared_preferences: ^2.2.2 uuid: ^4.3.3 - flutter_map: ^7.0.2 + flutter_map: ^8.2.2 latlong2: ^0.9.1 - flutter_local_notifications: ^18.0.1 + flutter_local_notifications: ^20.1.0 crypto: ^3.0.3 - pointycastle: ^3.7.4 + pointycastle: ^4.0.0 http: ^1.2.0 cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 - flutter_foreground_task: ^6.1.2 + flutter_foreground_task: ^9.2.0 wakelock_plus: ^1.2.8 characters: ^1.4.0 - package_info_plus: ^8.0.0 - mobile_scanner: ^6.0.0 # QR/barcode scanning + package_info_plus: ^9.0.0 + mobile_scanner: ^7.1.4 # QR/barcode scanning qr_flutter: ^4.1.0 # QR code generation url_launcher: ^6.3.0 # Launch URLs in system browser flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text @@ -70,8 +70,8 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 - flutter_launcher_icons: ^0.13.1 + flutter_lints: ^6.0.0 + flutter_launcher_icons: ^0.14.4 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 571addb8..4c358e7f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES) From a9fbf8c7f502fef9a229e9a0ae1084e7d1a2e2f9 Mon Sep 17 00:00:00 2001 From: MGJ <62177301+MGJ520@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:30:19 +0800 Subject: [PATCH 25/99] Correct Chinese translation --- lib/l10n/app_zh.arb | 976 ++++++++++++++++++++++---------------------- 1 file changed, 488 insertions(+), 488 deletions(-) diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 7b4b3ab5..3e4b1d31 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,11 +1,11 @@ { "@@locale": "zh", "appTitle": "MeshCore Open", - "nav_contacts": "联系方式", + "nav_contacts": "联系人", "nav_channels": "频道", "nav_map": "地图", "common_cancel": "取消", - "common_ok": "好的", + "common_ok": "确定", "common_connect": "连接", "common_unknownDevice": "未知设备", "common_save": "保存", @@ -15,9 +15,9 @@ "common_add": "添加", "common_settings": "设置", "common_disconnect": "断开", - "common_connected": "连接", - "common_disconnected": "断开", - "common_create": "创造", + "common_connected": "已连接", + "common_disconnected": "已断开", + "common_create": "创建", "common_continue": "继续", "common_share": "分享", "common_copy": "复制", @@ -26,7 +26,7 @@ "common_remove": "移除", "common_enable": "启用", "common_disable": "禁用", - "common_reboot": "重新启动", + "common_reboot": "重启", "common_loading": "正在加载...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", @@ -45,7 +45,7 @@ } } }, - "scanner_title": "MeshCore 开放", + "scanner_title": "连接设备", "scanner_scanning": "正在搜索设备...", "scanner_connecting": "正在连接...", "scanner_disconnecting": "断开连接...", @@ -59,8 +59,8 @@ } }, "scanner_searchingDevices": "正在搜索 MeshCore 设备...", - "scanner_tapToScan": "点击“扫描”功能,以查找 MeshCore 设备。", - "scanner_connectionFailed": "Connection failed: {error}", + "scanner_tapToScan": "点击“扫描”按钮以查找 MeshCore 设备。", + "scanner_connectionFailed": "连接失败:{error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -71,8 +71,8 @@ "scanner_stop": "停止", "scanner_scan": "扫描", "device_quickSwitch": "快速切换", - "device_meshcore": "网格核心", - "settings_title": "设置", + "device_meshcore": "MeshCore", + "settings_title": " ", "settings_deviceInfo": "设备信息", "settings_appSettings": "应用设置", "settings_appSettingsSubtitle": "通知、消息和地图偏好", @@ -80,43 +80,43 @@ "settings_nodeName": "节点名称", "settings_nodeNameNotSet": "未设置", "settings_nodeNameHint": "请输入节点名称", - "settings_nodeNameUpdated": "姓名已更新", - "settings_radioSettings": "收音机设置", + "settings_nodeNameUpdated": "节点名称已更新", + "settings_radioSettings": "无线电设置", "settings_radioSettingsSubtitle": "频率、功率、扩频因子", - "settings_radioSettingsUpdated": "收音机设置已更新", - "settings_location": "地点", + "settings_radioSettingsUpdated": "无线电设置已更新", + "settings_location": "位置", "settings_locationSubtitle": "GPS 坐标", "settings_locationUpdated": "位置和 GPS 设置已更新", - "settings_locationBothRequired": "请输入经度和纬度。", - "settings_locationInvalid": "无效的经度和纬度。", - "settings_locationGPSEnable": "开启 GPS 功能", - "settings_locationGPSEnableSubtitle": "使 GPS 能够自动更新位置。", - "settings_locationIntervalSec": "GPS 间隔时间(秒)", + "settings_locationBothRequired": "请输入经度和纬度", + "settings_locationInvalid": "无效的经度和纬度", + "settings_locationGPSEnable": "启用 GPS", + "settings_locationGPSEnableSubtitle": "启用 GPS 以自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔(秒)", "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_latitude": "纬度", "settings_longitude": "经度", "settings_privacyMode": "隐私模式", "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", - "settings_privacyModeToggle": "切换隐私模式,以隐藏您的姓名和位置,从而在广告中保护您的个人信息。", + "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", "settings_privacyModeEnabled": "隐私模式已启用", "settings_privacyModeDisabled": "隐私模式已关闭", - "settings_actions": "行动", - "settings_sendAdvertisement": "发布广告", - "settings_sendAdvertisementSubtitle": "现已开始进行广播节目", - "settings_advertisementSent": "已发送广告", + "settings_actions": "操作", + "settings_sendAdvertisement": "发送广播", + "settings_sendAdvertisementSubtitle": "立即发送广播", + "settings_advertisementSent": "已发送广播", "settings_syncTime": "同步时间", "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", - "settings_timeSynchronized": "时间同步", + "settings_timeSynchronized": "时间已同步", "settings_refreshContacts": "刷新联系人", - "settings_refreshContactsSubtitle": "从设备中重新加载联系人列表", + "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", "settings_rebootDevice": "重启设备", - "settings_rebootDeviceSubtitle": "重新启动 MeshCore 设备", - "settings_rebootDeviceConfirm": "您确定要重启设备吗?这将导致您与设备断开连接。", + "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", + "settings_rebootDeviceConfirm": "确定要重启设备吗?这将断开与设备的连接。", "settings_debug": "调试", "settings_bleDebugLog": "BLE 调试日志", "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", - "settings_appDebugLog": "应用程序调试日志", - "settings_appDebugLogSubtitle": "应用程序调试消息", + "settings_appDebugLog": "应用调试日志", + "settings_appDebugLogSubtitle": "应用调试消息", "settings_about": "关于", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -128,29 +128,29 @@ }, "settings_aboutLegalese": "2026 MeshCore 开源项目", "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", - "settings_infoName": "姓名", - "settings_infoId": "ID", + "settings_infoName": "名称", + "settings_infoId": "MAC ID", "settings_infoStatus": "状态", "settings_infoBattery": "电池", "settings_infoPublicKey": "公钥", "settings_infoContactsCount": "联系人数量", - "settings_infoChannelCount": "通道数量", + "settings_infoChannelCount": "频道数量", "settings_presets": "预设", - "settings_preset915Mhz": "915 兆赫", - "settings_preset868Mhz": "868 兆赫", - "settings_preset433Mhz": "433 兆赫", + "settings_preset915Mhz": "915 MHz", + "settings_preset868Mhz": "868 MHz", + "settings_preset433Mhz": "433 MHz", "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "无效频率(300-2500 MHz)", + "settings_frequencyInvalid": "无效频率范围(300-2500 MHz)", "settings_bandwidth": "带宽", - "settings_spreadingFactor": "传播系数", + "settings_spreadingFactor": "扩频因子", "settings_codingRate": "编码速率", - "settings_txPower": "TX 功率(dBm)", + "settings_txPower": "TX 功率 (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", "settings_longRange": "远距离", "settings_fastSpeed": "高速", - "settings_error": "[保存:{message}]\n错误:{message}", + "settings_error": "错误:{message}", "@settings_error": { "placeholders": { "message": { @@ -161,49 +161,49 @@ "appSettings_title": "应用设置", "appSettings_appearance": "外观", "appSettings_theme": "主题", - "appSettings_themeSystem": "系统默认设置", - "appSettings_themeLight": "光", - "appSettings_themeDark": "黑暗", + "appSettings_themeSystem": "跟随系统", + "appSettings_themeLight": "浅色", + "appSettings_themeDark": "深色", "appSettings_language": "语言", - "appSettings_languageSystem": "系统默认设置", + "appSettings_languageSystem": "跟随系统", "appSettings_languageEn": "英语", "appSettings_languageFr": "法语", "appSettings_languageEs": "西班牙语", "appSettings_languageDe": "德语", "appSettings_languagePl": "波兰语", - "appSettings_languageSl": "斯洛文语", + "appSettings_languageSl": "斯洛文尼亚语", "appSettings_languagePt": "葡萄牙语", "appSettings_languageIt": "意大利语", "appSettings_languageZh": "中文", "appSettings_languageSv": "瑞典语", "appSettings_languageNl": "荷兰语", "appSettings_languageSk": "斯洛伐克语", - "appSettings_languageBg": "保加利亚", + "appSettings_languageBg": "保加利亚语", "appSettings_languageRu": "俄语", - "appSettings_languageUk": "乌克兰", + "appSettings_languageUk": "乌克兰语", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", - "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", + "appSettings_enableNotificationsSubtitle": "接收消息和广播的通知", "appSettings_notificationPermissionDenied": "权限被拒绝", "appSettings_notificationsEnabled": "通知已启用", "appSettings_notificationsDisabled": "通知已关闭", "appSettings_messageNotifications": "消息通知", - "appSettings_messageNotificationsSubtitle": "在收到新消息时显示通知", + "appSettings_messageNotificationsSubtitle": "收到新消息时显示通知", "appSettings_channelMessageNotifications": "频道消息通知", - "appSettings_channelMessageNotificationsSubtitle": "在收到频道消息时,显示通知。", - "appSettings_advertisementNotifications": "广告通知", - "appSettings_advertisementNotificationsSubtitle": "在发现新的节点时,显示通知。", - "appSettings_messaging": "信息传递", - "appSettings_clearPathOnMaxRetry": "关于“最大重试”的清晰说明", - "appSettings_clearPathOnMaxRetrySubtitle": "在尝试发送失败后 5 次,重置联系路径。", - "appSettings_pathsWillBeCleared": "如果尝试 5 次后仍然失败,则将重新规划路径。", - "appSettings_pathsWillNotBeCleared": "路径不会自动清除。", + "appSettings_channelMessageNotificationsSubtitle": "收到频道消息时显示通知", + "appSettings_advertisementNotifications": "广播通知", + "appSettings_advertisementNotificationsSubtitle": "发现新节点时显示通知", + "appSettings_messaging": "消息", + "appSettings_clearPathOnMaxRetry": "达到最大重试次数时清除路径", + "appSettings_clearPathOnMaxRetrySubtitle": "在5次发送失败后重置联系路径。", + "appSettings_pathsWillBeCleared": "5次失败后将重新路由", + "appSettings_pathsWillNotBeCleared": "路径不会自动清除", "appSettings_autoRouteRotation": "自动路径轮换", - "appSettings_autoRouteRotationSubtitle": "在最佳路径和防洪模式之间切换", + "appSettings_autoRouteRotationSubtitle": "在最佳路径和泛洪模式之间切换", "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", "appSettings_battery": "电池", - "appSettings_batteryChemistry": "电池化学", + "appSettings_batteryChemistry": "电池类型", "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { @@ -212,20 +212,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "连接到设备以进行选择", - "appSettings_batteryNmc": "18650 型号,NMC 电池(3.0-4.2V)", + "appSettings_batteryChemistryConnectFirst": "请先连接设备", + "appSettings_batteryNmc": "18650 NMC 电池 (3.0-4.2V)", "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", - "appSettings_batteryLipo": "锂离子电池 (3.0-4.2V)", - "appSettings_mapDisplay": "地图展示", - "appSettings_showRepeaters": "显示重复", - "appSettings_showRepeatersSubtitle": "在地图上显示重复节点", + "appSettings_batteryLipo": "锂聚合物电池 (3.0-4.2V)", + "appSettings_mapDisplay": "地图显示", + "appSettings_showRepeaters": "显示转发节点", + "appSettings_showRepeatersSubtitle": "在地图上显示转发节点", "appSettings_showChatNodes": "显示聊天节点", "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", "appSettings_showOtherNodes": "显示其他节点", "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", "appSettings_timeFilter": "时间过滤器", "appSettings_timeFilterShowAll": "显示所有节点", - "appSettings_timeFilterShowLast": "Show nodes from last {hours} hours", + "appSettings_timeFilterShowLast": "显示过去 {hours} 小时内的节点", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -234,7 +234,7 @@ } }, "appSettings_mapTimeFilter": "地图时间筛选", - "appSettings_showNodesDiscoveredWithin": "显示在以下范围内发现的节点:", + "appSettings_showNodesDiscoveredWithin": "显示在此时间段内发现的节点:", "appSettings_allTime": "所有时间", "appSettings_lastHour": "过去一小时", "appSettings_last6Hours": "过去6小时", @@ -242,7 +242,7 @@ "appSettings_lastWeek": "上周", "appSettings_offlineMapCache": "离线地图缓存", "appSettings_noAreaSelected": "未选择任何区域", - "appSettings_areaSelectedZoom": "已选择区域(缩放至 {minZoom} - {maxZoom})", + "appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -254,18 +254,18 @@ } }, "appSettings_debugCard": "调试", - "appSettings_appDebugLogging": "应用程序调试日志", - "appSettings_appDebugLoggingSubtitle": "用于故障排除的日志应用程序调试消息", + "appSettings_appDebugLogging": "应用调试日志", + "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以进行故障排除。", "appSettings_appDebugLoggingEnabled": "调试日志已启用", - "appSettings_appDebugLoggingDisabled": "应用程序调试日志已禁用", - "contacts_title": "联系方式", - "contacts_noContacts": "目前还没有联系人", - "contacts_contactsWillAppear": "当设备发布广告时,联系方式会显示。", + "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", + "contacts_title": " ", + "contacts_noContacts": "暂无联系人", + "contacts_contactsWillAppear": "当设备发送广播时,联系人将显示。", "contacts_searchContacts": "搜索联系人...", - "contacts_noUnreadContacts": "没有未读通讯", - "contacts_noContactsFound": "未找到任何联系人或群组", + "contacts_noUnreadContacts": "没有未读内容", + "contacts_noContactsFound": "未找到任何联系人或群聊", "contacts_deleteContact": "删除联系人", - "contacts_removeConfirm": "Remove {contactName} from contacts?", + "contacts_removeConfirm": "从联系人中移除 {contactName}?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -273,13 +273,13 @@ } } }, - "contacts_manageRepeater": "管理重复器", + "contacts_manageRepeater": "管理转发节点", "contacts_manageRoom": "管理房间服务器", "contacts_roomLogin": "服务器登录", - "contacts_openChat": "开放聊天", - "contacts_editGroup": "编辑小组", - "contacts_deleteGroup": "删除群组", - "contacts_deleteGroupConfirm": "删除\"{groupName}\"?", + "contacts_openChat": "打开聊天", + "contacts_editGroup": "编辑群聊", + "contacts_deleteGroup": "删除群聊", + "contacts_deleteGroupConfirm": "删除群聊 \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { "groupName": { @@ -287,10 +287,10 @@ } } }, - "contacts_newGroup": "新的团体", - "contacts_groupName": "团体名称", - "contacts_groupNameRequired": "需要提供组名称", - "contacts_groupAlreadyExists": "名为\"{name}\"的组已经存在", + "contacts_newGroup": "新建群聊", + "contacts_groupName": "群聊名称", + "contacts_groupNameRequired": "请输入群聊名称", + "contacts_groupAlreadyExists": "名为 \"{name}\" 的群聊已存在", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -299,10 +299,10 @@ } }, "contacts_filterContacts": "筛选联系人...", - "contacts_noContactsMatchFilter": "未找到符合您筛选条件的联系人", - "contacts_noMembers": "没有会员", - "contacts_lastSeenNow": "最后一次被看到的时间", - "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", + "contacts_noContactsMatchFilter": "没有符合条件的联系人", + "contacts_noMembers": "暂无成员", + "contacts_lastSeenNow": "刚刚", + "contacts_lastSeenMinsAgo": "最后在线 {minutes} 分钟前", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -310,8 +310,8 @@ } } }, - "contacts_lastSeenHourAgo": "最后一次被看到的时间:1小时前", - "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", + "contacts_lastSeenHourAgo": "最后在线 1小时前", + "contacts_lastSeenHoursAgo": "最后在线 {hours} 小时前", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -319,8 +319,8 @@ } } }, - "contacts_lastSeenDayAgo": "最后一次被看到的时间是1天前", - "contacts_lastSeenDaysAgo": "Last seen {days} days ago", + "contacts_lastSeenDayAgo": "最后在线 1天前", + "contacts_lastSeenDaysAgo": "最后在线 {days} 天前", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -328,7 +328,7 @@ } } }, - "channels_title": "频道", + "channels_title": " ", "channels_noChannelsConfigured": "未配置任何频道", "channels_addPublicChannel": "添加公共频道", "channels_searchChannels": "搜索频道...", @@ -341,14 +341,14 @@ } } }, - "channels_hashtagChannel": "话题标签频道", - "channels_public": "公众", - "channels_private": "私人", + "channels_hashtagChannel": "标签频道", + "channels_public": "公共", + "channels_private": "私有", "channels_publicChannel": "公共频道", - "channels_privateChannel": "私密频道", + "channels_privateChannel": "私有频道", "channels_editChannel": "编辑频道", "channels_deleteChannel": "删除频道", - "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", + "channels_deleteChannelConfirm": "删除频道 \"{name}\"?此操作不可撤销。", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -356,7 +356,7 @@ } } }, - "channels_channelDeleted": "删除频道 \"{name}\"", + "channels_channelDeleted": "已删除频道 \"{name}\"", "@channels_channelDeleted": { "placeholders": { "name": { @@ -368,12 +368,12 @@ "channels_channelIndexLabel": "频道索引", "channels_channelName": "频道名称", "channels_usePublicChannel": "使用公共频道", - "channels_standardPublicPsk": "标准公共PSK", + "channels_standardPublicPsk": "标准公共 PSK", "channels_pskHex": "PSK (十六进制)", - "channels_generateRandomPsk": "生成随机的PSK(正交相移键控)", - "channels_enterChannelName": "请在此处输入频道名称", - "channels_pskMustBe32Hex": "PSK 必须包含 32 个十六进制字符。", - "channels_channelAdded": "添加频道 \"{name}\"", + "channels_generateRandomPsk": "生成随机 PSK", + "channels_enterChannelName": "请输入频道名称", + "channels_pskMustBe32Hex": "PSK 必须为 32 个十六进制字符", + "channels_channelAdded": "已添加频道 \"{name}\"", "@channels_channelAdded": { "placeholders": { "name": { @@ -399,27 +399,27 @@ } }, "channels_publicChannelAdded": "已添加公共频道", - "channels_sortBy": "按排序", - "channels_sortManual": "手册", - "channels_sortAZ": "A 到 Z", + "channels_sortBy": "排序方式", + "channels_sortManual": "手动", + "channels_sortAZ": "A-Z", "channels_sortLatestMessages": "最新消息", "channels_sortUnread": "未读", - "channels_createPrivateChannel": "创建私密频道", - "channels_createPrivateChannelDesc": "使用秘密密钥进行保护。", - "channels_joinPrivateChannel": "加入私密频道", + "channels_createPrivateChannel": "创建私有频道", + "channels_createPrivateChannelDesc": "使用密钥保护。", + "channels_joinPrivateChannel": "加入私有频道", "channels_joinPrivateChannelDesc": "手动输入密钥。", "channels_joinPublicChannel": "加入公共频道", - "channels_joinPublicChannelDesc": "任何人都可以加入这个频道。", - "channels_joinHashtagChannel": "加入一个带有特定标签的频道", - "channels_joinHashtagChannelDesc": "任何人都可以加入带有特定标签的频道。", + "channels_joinPublicChannelDesc": "任何人都可以加入。", + "channels_joinHashtagChannel": "加入标签频道", + "channels_joinHashtagChannelDesc": "任何人都可以加入标签频道。", "channels_scanQrCode": "扫描二维码", - "channels_scanQrCodeComingSoon": "即将发布", + "channels_scanQrCodeComingSoon": "即将推出", "channels_enterHashtag": "输入标签", "channels_hashtagHint": "例如:#团队", - "chat_noMessages": "目前还没有收到任何消息。", - "chat_sendMessageToStart": "发送消息以开始", - "chat_originalMessageNotFound": "无法找到原始消息", - "chat_replyingTo": "Replying to {name}", + "chat_noMessages": "暂无消息", + "chat_sendMessageToStart": "发送消息开始对话", + "chat_originalMessageNotFound": "找不到原始消息", + "chat_replyingTo": "正在回复 {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -427,7 +427,7 @@ } } }, - "chat_replyTo": "Reply to {name}", + "chat_replyTo": "回复 {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -435,8 +435,8 @@ } } }, - "chat_location": "地点", - "chat_sendMessageTo": "Send a message to {contactName}", + "chat_location": "位置", + "chat_sendMessageTo": "发送消息给 {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -445,7 +445,7 @@ } }, "chat_typeMessage": "输入消息...", - "chat_messageTooLong": "消息内容过长(最大 {maxBytes} 字节)。", + "chat_messageTooLong": "消息过长(最多 {maxBytes} 字节)", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -455,8 +455,8 @@ }, "chat_messageCopied": "消息已复制", "chat_messageDeleted": "消息已删除", - "chat_retryingMessage": "重试消息", - "chat_retryCount": "Retry {current}/{max}", + "chat_retryingMessage": "正在重试消息", + "chat_retryCount": "重试 {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -467,32 +467,32 @@ } } }, - "chat_sendGif": "发送 GIF 动画", + "chat_sendGif": "发送 GIF", "chat_reply": "回复", - "chat_addReaction": "添加评论", + "chat_addReaction": "添加表情", "chat_me": "我", - "emojiCategorySmileys": "表情符号", + "emojiCategorySmileys": "表情", "emojiCategoryGestures": "手势", - "emojiCategoryHearts": "心脏", - "emojiCategoryObjects": "物体", - "gifPicker_title": "选择一个 GIF 动画", - "gifPicker_searchHint": "搜索 GIF 动画...", - "gifPicker_poweredBy": "由 GIPHY 提供支持", - "gifPicker_noGifsFound": "未找到 GIF 动画", - "gifPicker_failedLoad": "无法加载 GIF 动画", - "gifPicker_failedSearch": "未能搜索 GIF 动画", - "gifPicker_noInternet": "没有互联网连接", - "debugLog_appTitle": "应用程序调试日志", + "emojiCategoryHearts": "爱心", + "emojiCategoryObjects": "物品", + "gifPicker_title": "选择 GIF", + "gifPicker_searchHint": "搜索 GIF...", + "gifPicker_poweredBy": "由 GIPHY 提供", + "gifPicker_noGifsFound": "未找到 GIF", + "gifPicker_failedLoad": "加载 GIF 失败", + "gifPicker_failedSearch": "搜索 GIF 失败", + "gifPicker_noInternet": "无网络连接", + "debugLog_appTitle": "应用调试日志", "debugLog_bleTitle": "BLE 调试日志", "debugLog_copyLog": "复制日志", - "debugLog_clearLog": "清晰的日志", + "debugLog_clearLog": "清除日志", "debugLog_copied": "调试日志已复制", "debugLog_bleCopied": "BLE 日志已复制", - "debugLog_noEntries": "目前还没有调试日志", - "debugLog_enableInSettings": "在设置中启用应用程序调试日志功能。", - "debugLog_frames": "框架", - "debugLog_rawLogRx": "原始日志-RX", - "debugLog_noBleActivity": "目前尚未有蓝牙低功耗(BLE)活动。", + "debugLog_noEntries": "暂无调试日志", + "debugLog_enableInSettings": "请在设置中启用应用调试日志。", + "debugLog_frames": "帧", + "debugLog_rawLogRx": "原始日志 RX", + "debugLog_noBleActivity": "暂无 BLE 活动", "debugFrame_length": "帧长度:{count} 字节", "@debugFrame_length": { "placeholders": { @@ -509,7 +509,7 @@ } } }, - "debugFrame_textMessageHeader": "短信模板:", + "debugFrame_textMessageHeader": "文本消息:", "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { @@ -518,7 +518,7 @@ } } }, - "debugFrame_timestamp": "- Timestamp: {timestamp}", + "debugFrame_timestamp": "- 时间戳:{timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -534,7 +534,7 @@ } } }, - "debugFrame_textType": "- Text Type: {type} ({label})", + "debugFrame_textType": "- 文本类型:{type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -545,8 +545,8 @@ } } }, - "debugFrame_textTypeCli": "命令行界面", - "debugFrame_textTypePlain": "简单", + "debugFrame_textTypeCli": "命令行", + "debugFrame_textTypePlain": "纯文本", "debugFrame_text": "- 文本:“{text}”", "@debugFrame_text": { "placeholders": { @@ -558,13 +558,13 @@ "debugFrame_hexDump": "十六进制数据:", "chat_pathManagement": "路径管理", "chat_routingMode": "路由模式", - "chat_autoUseSavedPath": "自动(使用已保存的路径)", - "chat_forceFloodMode": "强制洪水模式", + "chat_autoUseSavedPath": "自动(使用保存的路径)", + "chat_forceFloodMode": "强制泛洪模式", "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", - "chat_pathHistoryFull": "路径历史已满。删除条目以添加新的条目。", - "chat_hopSingular": "跳跃", - "chat_hopPlural": "啤酒花", - "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", + "chat_pathHistoryFull": "路径历史已满,请删除后再添加。", + "chat_hopSingular": "跳", + "chat_hopPlural": "跳", + "chat_hopsCount": "{count} 跳", "@chat_hopsCount": { "placeholders": { "count": { @@ -573,19 +573,19 @@ } }, "chat_successes": "成功", - "chat_removePath": "删除路径", - "chat_noPathHistoryYet": "目前还没有历史记录。\n发送消息以查找路径。", + "chat_removePath": "移除路径", + "chat_noPathHistoryYet": "暂无路径历史。\n发送消息以探索路径。", "chat_pathActions": "路径操作:", "chat_setCustomPath": "设置自定义路径", "chat_setCustomPathSubtitle": "手动指定路由路径", - "chat_clearPath": "明确的道路", - "chat_clearPathSubtitle": "在下一次发送时,重新尝试。", - "chat_pathCleared": "路径已清理。下一条消息将重新确定路线。", - "chat_floodModeSubtitle": "使用应用程序栏中的路由切换功能", - "chat_floodModeEnabled": "防洪模式已启用。通过应用程序栏中的路由图标进行切换。", + "chat_clearPath": "清除路径", + "chat_clearPathSubtitle": "清除当前路径,下次发送将重新尝试。", + "chat_pathCleared": "路径已清除。下一条消息将重新路由。", + "chat_floodModeSubtitle": "在应用栏中切换路由模式。", + "chat_floodModeEnabled": "泛洪模式已启用。可通过应用栏的路由图标切换。", "chat_fullPath": "完整路径", - "chat_pathDetailsNotAvailable": "路径信息尚未提供。请尝试发送消息以刷新。", - "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "路径信息暂不可用,请尝试发送消息刷新。", + "chat_pathSetHops": "路径设置:{hopCount} 跳 - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -596,16 +596,16 @@ } } }, - "chat_pathSavedLocally": "已本地保存。连接以进行同步。", + "chat_pathSavedLocally": "已本地保存,连接设备后可同步。", "chat_pathDeviceConfirmed": "设备已确认。", - "chat_pathDeviceNotConfirmed": "该设备尚未得到确认。", + "chat_pathDeviceNotConfirmed": "设备尚未确认。", "chat_type": "类型", "chat_path": "路径", "chat_publicKey": "公钥", "chat_compressOutgoingMessages": "压缩发送的消息", - "chat_floodForced": "洪水(被迫)", - "chat_directForced": "直接(强制性的)", - "chat_hopsForced": "{count} 根啤酒花(人工种植)", + "chat_floodForced": "泛洪(强制)", + "chat_directForced": "直连(强制)", + "chat_hopsForced": "{count} 跳(强制)", "@chat_hopsForced": { "placeholders": { "count": { @@ -613,10 +613,10 @@ } } }, - "chat_floodAuto": "自动洪水", - "chat_direct": "直接", + "chat_floodAuto": "自动泛洪", + "chat_direct": "直连", "chat_poiShared": "共享位置", - "chat_unread": "Unread: {count}", + "chat_unread": "未读:{count}", "@chat_unread": { "placeholders": { "count": { @@ -625,9 +625,9 @@ } }, "chat_openLink": "打开链接?", - "chat_openLinkConfirmation": "您想用浏览器打开这个链接吗?", - "chat_open": "开放", - "chat_couldNotOpenLink": "[保存:{url}]\n无法打开链接:{url}", + "chat_openLinkConfirmation": "是否使用浏览器打开此链接?", + "chat_open": "打开", + "chat_couldNotOpenLink": "无法打开链接:{url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -636,10 +636,10 @@ } }, "chat_invalidLink": "无效的链接格式", - "map_title": "节点图", + "map_title": " ", "map_noNodesWithLocation": "没有包含位置信息的节点", - "map_nodesNeedGps": "节点需要共享其 GPS 坐标,以便在地图上显示", - "map_nodesCount": "Nodes: {count}", + "map_nodesNeedGps": "节点需要共享 GPS 坐标才能在地图上显示", + "map_nodesCount": "节点:{count}", "@map_nodesCount": { "placeholders": { "count": { @@ -647,7 +647,7 @@ } } }, - "map_pinsCount": "Pins: {count}", + "map_pinsCount": "标记:{count}", "@map_pinsCount": { "placeholders": { "count": { @@ -656,26 +656,26 @@ } }, "map_chat": "聊天", - "map_repeater": "重复器", + "map_repeater": "转发节点", "map_room": "房间", "map_sensor": "传感器", - "map_pinDm": "PIN (直接消息)", - "map_pinPrivate": "私密", - "map_pinPublic": "公开", - "map_lastSeen": "最后一次被看到", - "map_disconnectConfirm": "您确定要断开与此设备的连接吗?", - "map_from": "从", + "map_pinDm": "标记(私信)", + "map_pinPrivate": "私有", + "map_pinPublic": "公共", + "map_lastSeen": "最后在线", + "map_disconnectConfirm": "确定要断开与此设备的连接吗?", + "map_from": "来自", "map_source": "来源", - "map_flags": "旗帜", + "map_flags": "标志", "map_shareMarkerHere": "在此分享标记", "map_pinLabel": "标签", "map_label": "标签", - "map_pointOfInterest": "值得参观的地方", - "map_sendToContact": "发送给联系", + "map_pointOfInterest": "兴趣点", + "map_sendToContact": "发送给联系人", "map_sendToChannel": "发送到频道", "map_noChannelsAvailable": "没有可用的频道", - "map_publicLocationShare": "公共场所共享", - "map_publicLocationShareConfirm": "[保存:{channelLabel}]\n您即将分享一个位置,该位置位于 {channelLabel}。 此频道是公开的,任何拥有 PSK 的人都可以看到它。", + "map_publicLocationShare": "公共位置共享", + "map_publicLocationShareConfirm": "您即将在 {channelLabel} 上分享一个位置。此频道是公开的,任何拥有 PSK 的人都可以看到。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -687,22 +687,22 @@ "map_filterNodes": "过滤节点", "map_nodeTypes": "节点类型", "map_chatNodes": "聊天节点", - "map_repeaters": "重复器", + "map_repeaters": "转发节点", "map_otherNodes": "其他节点", - "map_keyPrefix": "关键前缀", - "map_filterByKeyPrefix": "按关键前缀筛选", - "map_publicKeyPrefix": "公钥前缀", + "map_keyPrefix": "关键字前缀", + "map_filterByKeyPrefix": "按关键字前缀筛选", + "map_publicKeyPrefix": "关键字前缀", "map_markers": "标记", "map_showSharedMarkers": "显示共享标记", - "map_lastSeenTime": "最后一次被看到的时间", - "map_sharedPin": "共享密码", + "map_lastSeenTime": "最后在线时间", + "map_sharedPin": "共享标记", "map_joinRoom": "加入房间", - "map_manageRepeater": "管理重复器", + "map_manageRepeater": "管理转发节点", "mapCache_title": "离线地图缓存", - "mapCache_selectAreaFirst": "选择一个用于缓存的区域", - "mapCache_noTilesToDownload": "此区域没有可下载的瓦片。", - "mapCache_downloadTilesTitle": "下载瓷砖", - "mapCache_downloadTilesPrompt": "[保存:{count}]\n下载 {count} 个图片用于离线使用?", + "mapCache_selectAreaFirst": "请先选择要缓存的区域", + "mapCache_noTilesToDownload": "此区域没有可下载的瓦片", + "mapCache_downloadTilesTitle": "下载瓦片", + "mapCache_downloadTilesPrompt": "这需要下载 {count} 个瓦片", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -711,7 +711,7 @@ } }, "mapCache_downloadAction": "下载", - "mapCache_cachedTiles": "缓存 {count} 个瓦片", + "mapCache_cachedTiles": "已缓存 {count} 个瓦片", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -719,7 +719,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", + "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片({failed} 个失败)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -733,11 +733,11 @@ "mapCache_clearOfflineCacheTitle": "清除离线缓存", "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", "mapCache_offlineCacheCleared": "离线缓存已清除", - "mapCache_noAreaSelected": "未选择任何区域", + "mapCache_noAreaSelected": "未选择区域", "mapCache_cacheArea": "缓存区域", "mapCache_useCurrentView": "使用当前视图", - "mapCache_zoomRange": "变焦范围", - "mapCache_estimatedTiles": "Estimated tiles: {count}", + "mapCache_zoomRange": "缩放范围", + "mapCache_estimatedTiles": "估计瓦片数:{count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -745,7 +745,7 @@ } } }, - "mapCache_downloadedTiles": "Downloaded {completed} / {total}", + "mapCache_downloadedTiles": "已下载 {completed}/{total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -756,9 +756,9 @@ } } }, - "mapCache_downloadTilesButton": "下载瓷砖", + "mapCache_downloadTilesButton": "下载瓦片", "mapCache_clearCacheButton": "清除缓存", - "mapCache_failedDownloads": "Failed downloads: {count}", + "mapCache_failedDownloads": "下载失败:{count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -766,7 +766,7 @@ } } }, - "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", + "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -784,7 +784,7 @@ } }, "time_justNow": "刚才", - "time_minutesAgo": "{minutes}m ago", + "time_minutesAgo": "{minutes}分钟前", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -792,7 +792,7 @@ } } }, - "time_hoursAgo": "{hours}h ago", + "time_hoursAgo": "{hours}小时前", "@time_hoursAgo": { "placeholders": { "hours": { @@ -810,31 +810,31 @@ }, "time_hour": "小时", "time_hours": "小时", - "time_day": "一天", + "time_day": "天", "time_days": "天", - "time_week": "一周", + "time_week": "周", "time_weeks": "周", - "time_month": "月份", - "time_months": "月份", + "time_month": "月", + "time_months": "月", "time_minutes": "分钟", "time_allTime": "所有时间", "dialog_disconnect": "断开", - "dialog_disconnectConfirm": "您确定要断开与此设备的连接吗?", - "login_repeaterLogin": "重复登录", - "login_roomLogin": "服务器登录", + "dialog_disconnectConfirm": "确定要断开与此设备的连接吗?", + "login_repeaterLogin": "转发节点登录", + "login_roomLogin": "房间服务器登录", "login_password": "密码", "login_enterPassword": "请输入密码", "login_savePassword": "保存密码", - "login_savePasswordSubtitle": "密码将安全地存储在 данном设备上", - "login_repeaterDescription": "输入重复器密码,即可访问设置和状态。", - "login_roomDescription": "输入密码进入房间,即可访问设置和状态。", + "login_savePasswordSubtitle": "密码将安全地存储在此设备上", + "login_repeaterDescription": "输入转发节点密码以访问设置和状态。", + "login_roomDescription": "输入房间服务器密码以访问设置和状态。", "login_routing": "路由", "login_routingMode": "路由模式", - "login_autoUseSavedPath": "自动(使用已保存的路径)", - "login_forceFloodMode": "强制洪水模式", + "login_autoUseSavedPath": "自动(使用保存的路径)", + "login_forceFloodMode": "强制泛洪模式", "login_managePaths": "管理路径", "login_login": "登录", - "login_attempt": "Attempt {current}/{max}", + "login_attempt": "尝试 {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -845,7 +845,7 @@ } } }, - "login_failed": "Login failed: {error}", + "login_failed": "登录失败:{error}", "@login_failed": { "placeholders": { "error": { @@ -853,10 +853,10 @@ } } }, - "login_failedMessage": "登录失败。可能是密码错误,也可能是无法连接到服务器。", + "login_failedMessage": "登录失败。可能是密码错误或无法连接到服务器。", "common_reload": "重新加载", - "common_clear": "清晰", - "path_currentPath": "Current path: {path}", + "common_clear": "清除", + "path_currentPath": "当前路径:{path}", "@path_currentPath": { "placeholders": { "path": { @@ -864,7 +864,7 @@ } } }, - "path_usingHopsPath": "使用 {count} {count, plural, =1{hop} other{hops}} 条路径", + "path_usingHopsPath": "使用 {count} 跳路径", "@path_usingHopsPath": { "placeholders": { "count": { @@ -874,14 +874,14 @@ }, "path_enterCustomPath": "输入自定义路径", "path_currentPathLabel": "当前路径", - "path_hexPrefixInstructions": "请输入每个跳跃步骤的 2 个字符的十六进制前缀,用逗号分隔。", - "path_hexPrefixExample": "例如:A1, F2, 3C (每个节点使用其公钥的第一字节)", + "path_hexPrefixInstructions": "请输入每个中继节点的2字符十六进制前缀,用逗号分隔。", + "path_hexPrefixExample": "例如:A1, F2, 3C(每个节点使用其公钥的第一字节)", "path_labelHexPrefixes": "路径(十六进制前缀)", - "path_helperMaxHops": "最大 64 个“hop”(跳跃)。每个前缀由 2 个十六进制字符(1 字节)组成。", - "path_selectFromContacts": "或者从联系人列表中选择:", - "path_noRepeatersFound": "未找到任何重复设备或房间服务器。", - "path_customPathsRequire": "自定义路径需要中间节点,这些节点可以转发消息。", - "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", + "path_helperMaxHops": "最多 64 跳。每个前缀由 2 个十六进制字符(1 字节)组成。", + "path_selectFromContacts": "或从联系人列表中选择:", + "path_noRepeatersFound": "未找到任何转发节点或房间服务器。", + "path_customPathsRequire": "自定义路径需要中间节点转发消息。", + "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -889,29 +889,29 @@ } } }, - "path_tooLong": "路径太长。允许的最大跳跃次数为 64 次。", + "path_tooLong": "路径过长,最多允许 64 跳。", "path_setPath": "设置路径", - "repeater_management": "重复器管理", - "room_management": "服务器管理", + "repeater_management": "转发节点管理", + "room_management": "房间服务器管理", "repeater_managementTools": "管理工具", "repeater_status": "状态", - "repeater_statusSubtitle": "查看重复器状态、统计信息和邻居", - "repeater_telemetry": "远程监控", - "repeater_telemetrySubtitle": "查看传感器和系统状态的数据。", - "repeater_cli": "命令行界面", - "repeater_cliSubtitle": "向复用器发送指令", + "repeater_statusSubtitle": "查看转发节点状态、统计和邻居", + "repeater_telemetry": "遥测", + "repeater_telemetrySubtitle": "查看传感器和系统状态数据", + "repeater_cli": "命令行", + "repeater_cliSubtitle": "向转发节点发送命令", "repeater_neighbours": "邻居", - "repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。", + "repeater_neighboursSubtitle": "查看邻居节点(零跳)", "repeater_settings": "设置", - "repeater_settingsSubtitle": "配置重复器参数", - "repeater_statusTitle": "重复器状态", + "repeater_settingsSubtitle": "配置转发节点参数", + "repeater_statusTitle": "转发节点状态", "repeater_routingMode": "路由模式", - "repeater_autoUseSavedPath": "自动(使用已保存的路径)", - "repeater_forceFloodMode": "强制洪水模式", + "repeater_autoUseSavedPath": "自动(使用保存的路径)", + "repeater_forceFloodMode": "强制泛洪模式", "repeater_pathManagement": "路径管理", - "repeater_refresh": "更新", - "repeater_statusRequestTimeout": "状态请求超时。", - "repeater_errorLoadingStatus": "Error loading status: {error}", + "repeater_refresh": "刷新", + "repeater_statusRequestTimeout": "状态请求超时", + "repeater_errorLoadingStatus": "加载状态时出错:{error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -921,19 +921,19 @@ }, "repeater_systemInformation": "系统信息", "repeater_battery": "电池", - "repeater_clockAtLogin": "登录时的时间", - "repeater_uptime": "正常运行时间", - "repeater_queueLength": "排队长度", + "repeater_clockAtLogin": "登录时的时钟", + "repeater_uptime": "运行时间", + "repeater_queueLength": "队列长度", "repeater_debugFlags": "调试标志", - "repeater_radioStatistics": "广播统计", - "repeater_lastRssi": "上次的 RSSI 值", - "repeater_lastSnr": "最后一次信噪比", - "repeater_noiseFloor": "噪声水平", - "repeater_txAirtime": "TX 频道预留时间", - "repeater_rxAirtime": "RX 空时", + "repeater_radioStatistics": "无线电统计", + "repeater_lastRssi": "上次 RSSI", + "repeater_lastSnr": "上次 SNR", + "repeater_noiseFloor": "底噪", + "repeater_txAirtime": "发送空中时间", + "repeater_rxAirtime": "接收空中时间", "repeater_packetStatistics": "数据包统计", "repeater_sent": "发送", - "repeater_received": "已收到", + "repeater_received": "接收", "repeater_duplicates": "重复", "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}秒", "@repeater_daysHoursMinsSecs": { @@ -952,7 +952,7 @@ } } }, - "repeater_packetTxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", + "repeater_packetTxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -966,7 +966,7 @@ } } }, - "repeater_packetRxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", + "repeater_packetRxTotal": "总计:{total},泛洪:{flood},直连:{direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -980,7 +980,7 @@ } } }, - "repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}", + "repeater_duplicatesFloodDirect": "泛洪:{flood},直连:{direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -991,7 +991,7 @@ } } }, - "repeater_duplicatesTotal": "Total: {total}", + "repeater_duplicatesTotal": "总计:{total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -999,37 +999,37 @@ } } }, - "repeater_settingsTitle": "重复器设置", + "repeater_settingsTitle": "转发节点设置", "repeater_basicSettings": "基本设置", - "repeater_repeaterName": "重复器名称", - "repeater_repeaterNameHelper": "此复播器的显示名称", + "repeater_repeaterName": "转发节点名称", + "repeater_repeaterNameHelper": "此转发节点的显示名称", "repeater_adminPassword": "管理员密码", "repeater_adminPasswordHelper": "完整访问密码", "repeater_guestPassword": "访客密码", "repeater_guestPasswordHelper": "只读访问密码", - "repeater_radioSettings": "收音机设置", + "repeater_radioSettings": "无线电设置", "repeater_frequencyMhz": "频率 (MHz)", - "repeater_frequencyHelper": "300-2500 兆赫", + "repeater_frequencyHelper": "300-2500 MHz", "repeater_txPower": "TX 功率", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "带宽", - "repeater_spreadingFactor": "传播系数", + "repeater_spreadingFactor": "扩频因子", "repeater_codingRate": "编码速率", "repeater_locationSettings": "位置设置", "repeater_latitude": "纬度", - "repeater_latitudeHelper": "十进制度(例如:37.7749)", + "repeater_latitudeHelper": "十进制,例如 37.7749", "repeater_longitude": "经度", - "repeater_longitudeHelper": "十进制度(例如:-122.4194)", - "repeater_features": "特点", + "repeater_longitudeHelper": "十进制,例如 -122.4194", + "repeater_features": "功能", "repeater_packetForwarding": "数据包转发", - "repeater_packetForwardingSubtitle": "启用重复器,使其能够转发数据包", + "repeater_packetForwardingSubtitle": "启用转发节点转发数据包", "repeater_guestAccess": "访客访问", - "repeater_guestAccessSubtitle": "允许访客仅限读取权限", + "repeater_guestAccessSubtitle": "允许访客只读权限", "repeater_privacyMode": "隐私模式", - "repeater_privacyModeSubtitle": "在广告中隐藏姓名/位置", - "repeater_advertisementSettings": "广告设置", - "repeater_localAdvertInterval": "本地广告投放时间段", - "repeater_localAdvertIntervalMinutes": "{minutes} minutes", + "repeater_privacyModeSubtitle": "在广播中隐藏姓名/位置", + "repeater_advertisementSettings": "广播设置", + "repeater_localAdvertInterval": "本地广播间隔", + "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1037,8 +1037,8 @@ } } }, - "repeater_floodAdvertInterval": "洪水广告播放间隔", - "repeater_floodAdvertIntervalHours": "{hours} hours", + "repeater_floodAdvertInterval": "泛洪广播间隔", + "repeater_floodAdvertIntervalHours": "{hours} 小时", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1046,19 +1046,19 @@ } } }, - "repeater_encryptedAdvertInterval": "加密的广告投放时间段", - "repeater_dangerZone": "危险区域", - "repeater_rebootRepeater": "重启重复器", - "repeater_rebootRepeaterSubtitle": "重新启动重复器设备", - "repeater_rebootRepeaterConfirm": "您确定要重新启动这个中继器吗?", + "repeater_encryptedAdvertInterval": "加密广播间隔", + "repeater_dangerZone": "危险设置", + "repeater_rebootRepeater": "重启转发节点", + "repeater_rebootRepeaterSubtitle": "重启转发节点设备", + "repeater_rebootRepeaterConfirm": "确定要重启此转发节点吗?", "repeater_regenerateIdentityKey": "重新生成身份密钥", "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", - "repeater_regenerateIdentityKeyConfirm": "这将为复用器生成一个新的身份。继续吗?", - "repeater_eraseFileSystem": "删除文件系统", - "repeater_eraseFileSystemSubtitle": "格式化重复文件系统", - "repeater_eraseFileSystemConfirm": "警告:此操作将清除复用器上的所有数据。 无法恢复!", - "repeater_eraseSerialOnly": "“Erase”功能仅可通过串行控制台使用。", - "repeater_commandSent": "Command sent: {command}", + "repeater_regenerateIdentityKeyConfirm": "这将为转发节点生成新身份,继续吗?", + "repeater_eraseFileSystem": "擦除文件系统", + "repeater_eraseFileSystemSubtitle": "格式化转发节点文件系统", + "repeater_eraseFileSystemConfirm": "警告:此操作将清除转发节点上的所有数据,且无法恢复!", + "repeater_eraseSerialOnly": "擦除功能仅可通过串行控制台使用。", + "repeater_commandSent": "命令已发送:{command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1066,7 +1066,7 @@ } } }, - "repeater_errorSendingCommand": "Error sending command: {error}", + "repeater_errorSendingCommand": "发送命令时出错:{error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1075,8 +1075,8 @@ } }, "repeater_confirm": "确认", - "repeater_settingsSaved": "设置已成功保存", - "repeater_errorSavingSettings": "Error saving settings: {error}", + "repeater_settingsSaved": "设置保存成功", + "repeater_errorSavingSettings": "保存设置时出错:{error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1084,15 +1084,15 @@ } } }, - "repeater_refreshBasicSettings": "重置基本设置", - "repeater_refreshRadioSettings": "重置收音机设置", - "repeater_refreshTxPower": "重置 TX 电源", - "repeater_refreshLocationSettings": "重置位置设置", + "repeater_refreshBasicSettings": "刷新基本设置", + "repeater_refreshRadioSettings": "刷新无线电设置", + "repeater_refreshTxPower": "刷新 TX 功率", + "repeater_refreshLocationSettings": "刷新位置设置", "repeater_refreshPacketForwarding": "刷新包转发", - "repeater_refreshGuestAccess": "重新获取访客访问权限", - "repeater_refreshPrivacyMode": "重置隐私模式", - "repeater_refreshAdvertisementSettings": "重置广告设置", - "repeater_refreshed": "{label} refreshed", + "repeater_refreshGuestAccess": "刷新访客权限", + "repeater_refreshPrivacyMode": "刷新隐私模式", + "repeater_refreshAdvertisementSettings": "刷新广播设置", + "repeater_refreshed": "{label} 已刷新", "@repeater_refreshed": { "placeholders": { "label": { @@ -1100,7 +1100,7 @@ } } }, - "repeater_errorRefreshing": "[保存:{label}]\n刷新 {label} 时出错", + "repeater_errorRefreshing": "刷新 {label} 时出错", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1108,18 +1108,18 @@ } } }, - "repeater_cliTitle": "重复器命令行界面", + "repeater_cliTitle": "转发节点命令行", "repeater_debugNextCommand": "调试下一条命令", "repeater_commandHelp": "帮助", - "repeater_clearHistory": "清晰的历史", - "repeater_noCommandsSent": "尚未发送任何指令", - "repeater_typeCommandOrUseQuick": "在下方输入命令,或使用快捷命令。", + "repeater_clearHistory": "清除历史", + "repeater_noCommandsSent": "尚未发送命令", + "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", "repeater_enterCommandHint": "输入命令...", - "repeater_previousCommand": "之前的命令", - "repeater_nextCommand": "下一个指令", - "repeater_enterCommandFirst": "首先输入一个命令", - "repeater_cliCommandFrameTitle": "CLI 命令框架", - "repeater_cliCommandError": "Error: {error}", + "repeater_previousCommand": "上一条命令", + "repeater_nextCommand": "下一条命令", + "repeater_enterCommandFirst": "请先输入命令", + "repeater_cliCommandFrameTitle": "CLI 命令帧", + "repeater_cliCommandError": "错误:{error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1127,81 +1127,81 @@ } } }, - "repeater_cliQuickGetName": "获取姓名", - "repeater_cliQuickGetRadio": "收听广播", + "repeater_cliQuickGetName": "获取名称", + "repeater_cliQuickGetRadio": "获取无线电设置", "repeater_cliQuickGetTx": "获取 TX", "repeater_cliQuickNeighbors": "邻居", "repeater_cliQuickVersion": "版本", - "repeater_cliQuickAdvertise": "发布广告", + "repeater_cliQuickAdvertise": "发送广播", "repeater_cliQuickClock": "时钟", - "repeater_cliHelpAdvert": "发送广告资料包", - "repeater_cliHelpReboot": "重置设备。 (请注意,您可能会收到“超时”错误,这是正常的现象)", - "repeater_cliHelpClock": "显示每个设备的当前时间。", - "repeater_cliHelpPassword": "为设备设置新的管理员密码。", - "repeater_cliHelpVersion": "显示设备版本和固件构建日期。", - "repeater_cliHelpClearStats": "重置各种统计指标,将其设置为零。", - "repeater_cliHelpSetAf": "设置时间因素。", - "repeater_cliHelpSetTx": "设置 LoRa 传输功率,单位为 dBm (相对于参考值)。 (重启以应用更改)", - "repeater_cliHelpSetRepeat": "启用或禁用此节点的重复器功能。", - "repeater_cliHelpSetAllowReadOnly": "(房间服务器)如果设置为“开启”,则允许使用空密码登录,但无法向房间发送消息(只能进行读取)。", - "repeater_cliHelpSetFloodMax": "设置最大传入数据包的跳数(如果大于或等于最大值,则不进行转发)。", - "repeater_cliHelpSetIntThresh": "设置干扰阈值(以dB为单位)。默认值为14。将设置为0以禁用频道干扰检测。", - "repeater_cliHelpSetAgcResetInterval": "设置间隔时间,用于重置自动增益控制器。设置为 0 以禁用。", - "repeater_cliHelpSetMultiAcks": "启用或禁用“双重确认”功能。", - "repeater_cliHelpSetAdvertInterval": "设置定时器间隔,单位为分钟,用于发送本地(无中继)的广告数据包。 将设置为 0 以禁用。", - "repeater_cliHelpSetFloodAdvertInterval": "设置定时器间隔时间为小时,以便发送广告信息包。将设置为 0 以禁用。", - "repeater_cliHelpSetGuestPassword": "设置/更新访客密码。 (对于访客,登录请求可以发送“获取统计”请求)", - "repeater_cliHelpSetName": "设置广告名称。", - "repeater_cliHelpSetLat": "设置广告地图的纬度。(以十进制表示)", - "repeater_cliHelpSetLon": "设置广告地图的经度。 (十进制度)", - "repeater_cliHelpSetRadio": "完全重新设置无线电参数,并保存到偏好设置。需要执行“重启”命令才能生效。", - "repeater_cliHelpSetRxDelay": "设置(实验性):设置一个基础值(必须大于1才能生效),用于对接收到的数据包进行轻微延迟处理,该延迟值基于信号强度/评分。将该值设置为0以禁用。", - "repeater_cliHelpSetTxDelay": "通过将一个因子与“浮动模式”数据包的时间在空中停留时间相乘,并结合随机的“时隙”系统,来延迟其转发,从而降低数据包冲突的概率。", - "repeater_cliHelpSetDirectTxDelay": "与txdelay相同,但用于对直接模式数据包的转发进行随机延迟。", - "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接。", - "repeater_cliHelpSetBridgeDelay": "在重新发送数据包之前,设置延迟时间。", - "repeater_cliHelpSetBridgeSource": "选择桥接器是否会转发收到的数据包,还是转发发送的数据包。", - "repeater_cliHelpSetBridgeBaud": "为 RS232 桥接设置串行链路的波特率。", - "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥的秘密。", - "repeater_cliHelpSetAdcMultiplier": "设置自定义因子,用于调整报告的电池电压(仅在特定板上支持)。", - "repeater_cliHelpTempRadio": "设置临时收音机参数,持续指定分钟数,之后恢复到原始收音机参数。(不保存到偏好设置)。", - "repeater_cliHelpSetPerm": "修改 ACL。如果 \"permissions\" 的值为 0,则删除与 pubkey 相关的条目。如果 pubkey-hex 完整且当前不在 ACL 中,则添加新的条目。通过匹配 pubkey 相关的前缀来更新条目。不同固件角色的权限位有所不同,但低 2 位分别对应:0 (访客)、1 (只读)、2 (读写)、3 (管理员)。", - "repeater_cliHelpGetBridgeType": "支持桥接模式、RS232、ESPNOW。", - "repeater_cliHelpLogStart": "开始将数据包记录到文件系统。", - "repeater_cliHelpLogStop": "停止将数据包记录写入文件系统。", - "repeater_cliHelpLogErase": "从文件系统中删除所有已记录的包信息。", - "repeater_cliHelpNeighbors": "显示了通过零跳广告收到的其他复用节点列表。 每行包含:id-前缀-十六进制:时间戳:信噪比(4次)", - "repeater_cliHelpNeighborRemove": "从邻居列表中删除第一个匹配项(通过十六进制的 pubkey 前缀)。", - "repeater_cliHelpRegion": "(仅限序列)列出所有已定义的区域以及当前的防洪许可。", - "repeater_cliHelpRegionLoad": "请注意:这是一个特殊的、包含多个命令的调用方式。 之后的每个命令都是一个区域名称(使用空格进行缩进,以表示父级关系,至少需要一个空格)。 结束方式是通过发送一个空行/命令。", - "repeater_cliHelpRegionGet": "搜索具有指定名称前缀的区域(或使用“*”表示全局范围)。 返回结果为“-> region-name (parent-name) 'F'”", - "repeater_cliHelpRegionPut": "添加或更新一个区域定义,并指定其名称。", - "repeater_cliHelpRegionRemove": "删除具有指定名称的区域定义。 (必须与指定名称完全匹配,且不能有子区域)", - "repeater_cliHelpRegionAllowf": "为指定区域设置“洪水”权限。(“*”表示全局/旧版本范围)", - "repeater_cliHelpRegionDenyf": "移除指定区域的“洪水”权限。(请注意:目前不建议在全局/旧版本中使用此功能!!)", - "repeater_cliHelpRegionHome": "回复当前“主区域”。(此功能尚未应用,仅供未来使用)", - "repeater_cliHelpRegionHomeSet": "设置“主”区域。", - "repeater_cliHelpRegionSave": "将区域列表/地图保存到存储中。", - "repeater_cliHelpGps": "显示 GPS 状态。当 GPS 处于关闭状态时,它只会显示“关闭”;当 GPS 处于开启状态时,它会显示“开启”、“状态”、“定位”、“卫星数量”等信息。", - "repeater_cliHelpGpsOnOff": "切换 GPS 设备的电源状态。", - "repeater_cliHelpGpsSync": "将节点时间与 GPS 钟同步。", - "repeater_cliHelpGpsSetLoc": "将节点的坐标设置为 GPS 坐标,并保存设置。", - "repeater_cliHelpGpsAdvert": "设置节点的位置广告配置:\n- none:不将位置信息包含在广告中\n- share:共享 GPS 位置(从 SensorManager 获取)\n- prefs:在偏好设置中展示的位置", - "repeater_cliHelpGpsAdvertSet": "设置广告的位置配置。", + "repeater_cliHelpAdvert": "发送广播包", + "repeater_cliHelpReboot": "重启设备。(注意:可能会收到超时错误,属于正常现象)", + "repeater_cliHelpClock": "显示设备当前时间", + "repeater_cliHelpPassword": "设置新的管理员密码", + "repeater_cliHelpVersion": "显示设备版本和固件构建日期", + "repeater_cliHelpClearStats": "重置各种统计数据", + "repeater_cliHelpSetAf": "设置时间因子", + "repeater_cliHelpSetTx": "设置 LoRa 发射功率 (dBm)(重启生效)", + "repeater_cliHelpSetRepeat": "启用或禁用此节点的转发功能", + "repeater_cliHelpSetAllowReadOnly": "(房间服务器)设为“开”则允许空密码登录,但只能读(不能发送)", + "repeater_cliHelpSetFloodMax": "设置最大传入数据包跳数(≥该值则不转发)", + "repeater_cliHelpSetIntThresh": "设置干扰阈值 (dB),默认14,设为0禁用", + "repeater_cliHelpSetAgcResetInterval": "设置 AGC 重置间隔(秒),设为0禁用", + "repeater_cliHelpSetMultiAcks": "启用或禁用“多重确认”功能", + "repeater_cliHelpSetAdvertInterval": "设置本地广播间隔(分钟),设为0禁用", + "repeater_cliHelpSetFloodAdvertInterval": "设置泛洪广播间隔(小时),设为0禁用", + "repeater_cliHelpSetGuestPassword": "设置/更新访客密码", + "repeater_cliHelpSetName": "设置广播名称", + "repeater_cliHelpSetLat": "设置广播纬度(十进制)", + "repeater_cliHelpSetLon": "设置广播经度(十进制)", + "repeater_cliHelpSetRadio": "完全重设无线电参数并保存,需重启生效", + "repeater_cliHelpSetRxDelay": "(实验性)设置接收延迟基数,设为0禁用", + "repeater_cliHelpSetTxDelay": "通过因子和随机时隙延迟泛洪数据包转发,降低冲突", + "repeater_cliHelpSetDirectTxDelay": "同 txdelay,用于直连模式数据包", + "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接", + "repeater_cliHelpSetBridgeDelay": "设置桥接转发延迟", + "repeater_cliHelpSetBridgeSource": "选择桥接器转发接收或发送的数据包", + "repeater_cliHelpSetBridgeBaud": "设置 RS232 桥接串口波特率", + "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥接密钥", + "repeater_cliHelpSetAdcMultiplier": "设置电池电压校正系数(特定板支持)", + "repeater_cliHelpTempRadio": "临时设置无线电参数指定分钟,之后恢复(不保存)", + "repeater_cliHelpSetPerm": "修改 ACL,权限位:0访客、1只读、2读写、3管理员", + "repeater_cliHelpGetBridgeType": "支持桥接模式:RS232、ESPNOW", + "repeater_cliHelpLogStart": "开始记录数据包到文件系统", + "repeater_cliHelpLogStop": "停止记录数据包", + "repeater_cliHelpLogErase": "删除所有记录的数据包", + "repeater_cliHelpNeighbors": "显示零跳广播收到的其他转发节点列表", + "repeater_cliHelpNeighborRemove": "从邻居列表删除第一个匹配项(通过公钥前缀)", + "repeater_cliHelpRegion": "(仅串口)列出所有定义区域及当前泛洪权限", + "repeater_cliHelpRegionLoad": "特殊多命令调用,以空行结束", + "repeater_cliHelpRegionGet": "搜索指定前缀的区域", + "repeater_cliHelpRegionPut": "添加或更新区域定义", + "repeater_cliHelpRegionRemove": "删除指定区域定义", + "repeater_cliHelpRegionAllowf": "为区域设置“泛洪”权限", + "repeater_cliHelpRegionDenyf": "移除区域的“泛洪”权限", + "repeater_cliHelpRegionHome": "返回当前“主区域”(预留)", + "repeater_cliHelpRegionHomeSet": "设置“主”区域", + "repeater_cliHelpRegionSave": "保存区域列表到存储", + "repeater_cliHelpGps": "显示 GPS 状态", + "repeater_cliHelpGpsOnOff": "切换 GPS 电源", + "repeater_cliHelpGpsSync": "将节点时间与 GPS 同步", + "repeater_cliHelpGpsSetLoc": "将节点坐标设为 GPS 坐标并保存", + "repeater_cliHelpGpsAdvert": "设置位置广播配置:none/share/prefs", + "repeater_cliHelpGpsAdvertSet": "设置广播位置配置", "repeater_commandsListTitle": "命令列表", - "repeater_commandsListNote": "请注意:对于各种“set ...”命令,也存在“get ...”命令。", + "repeater_commandsListNote": "注意:多数 set 命令也有对应的 get 命令", "repeater_general": "通用", "repeater_settingsCategory": "设置", - "repeater_bridge": "桥", - "repeater_logging": "记录", - "repeater_neighborsRepeaterOnly": "邻居(仅限重复功能)", - "repeater_regionManagementRepeaterOnly": "区域管理(仅限重复站点)", - "repeater_regionNote": "区域命令已引入,用于管理区域定义和权限。", + "repeater_bridge": "桥接", + "repeater_logging": "日志", + "repeater_neighborsRepeaterOnly": "邻居(仅转发节点)", + "repeater_regionManagementRepeaterOnly": "区域管理(仅转发节点)", + "repeater_regionNote": "区域命令用于管理区域定义和权限", "repeater_gpsManagement": "GPS 管理", - "repeater_gpsNote": "已引入 GPS 命令,用于管理与位置相关的任务。", + "repeater_gpsNote": "GPS 命令用于位置相关任务", "telemetry_receivedData": "接收到的遥测数据", - "telemetry_requestTimeout": "遥测请求超时。", - "telemetry_errorLoading": "Error loading telemetry: {error}", + "telemetry_requestTimeout": "遥测请求超时", + "telemetry_errorLoading": "加载遥测数据时出错:{error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1209,7 +1209,7 @@ } } }, - "telemetry_noData": "没有可用的 telemetry 数据。", + "telemetry_noData": "暂无遥测数据", "telemetry_channelTitle": "频道 {channel}", "@telemetry_channelTitle": { "placeholders": { @@ -1220,9 +1220,9 @@ }, "telemetry_batteryLabel": "电池", "telemetry_voltageLabel": "电压", - "telemetry_mcuTemperatureLabel": "MCU 的温度", + "telemetry_mcuTemperatureLabel": "MCU 温度", "telemetry_temperatureLabel": "温度", - "telemetry_currentLabel": "当前", + "telemetry_currentLabel": "电流", "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { @@ -1261,9 +1261,9 @@ } } }, - "neighbors_receivedData": "已收到邻居信息", - "neighbors_requestTimedOut": "邻居要求停止干扰。", - "neighbors_errorLoading": "Error loading neighbors: {error}", + "neighbors_receivedData": "已接收邻居信息", + "neighbors_requestTimedOut": "邻居请求超时", + "neighbors_errorLoading": "加载邻居时出错:{error}", "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1271,9 +1271,9 @@ } } }, - "neighbors_repeatersNeighbours": "重复使用的邻居", - "neighbors_noData": "没有可用的邻居信息。", - "neighbors_unknownContact": "Unknown {pubkey}", + "neighbors_repeatersNeighbours": "转发节点的邻居", + "neighbors_noData": "暂无邻居信息", + "neighbors_unknownContact": "未知 {pubkey}", "@neighbors_unknownContact": { "placeholders": { "pubkey": { @@ -1281,7 +1281,7 @@ } } }, - "neighbors_heardAgo": "Heard: {time} ago", + "neighbors_heardAgo": "听到:{time}前", "@neighbors_heardAgo": { "placeholders": { "time": { @@ -1292,15 +1292,15 @@ "channelPath_title": "数据包路径", "channelPath_viewMap": "查看地图", "channelPath_otherObservedPaths": "其他观察到的路径", - "channelPath_repeaterHops": "复用跳跃", - "channelPath_noHopDetails": "对于此包,未提供详细信息。", + "channelPath_repeaterHops": "转发节点跳数", + "channelPath_noHopDetails": "此数据包未提供详细信息", "channelPath_messageDetails": "消息详情", - "channelPath_senderLabel": "发件人", + "channelPath_senderLabel": "发送者", "channelPath_timeLabel": "时间", "channelPath_repeatsLabel": "重复", "channelPath_pathLabel": "路径 {index}", "channelPath_observedLabel": "观察到的", - "channelPath_observedPathTitle": "Observed path {index} • {hops}", + "channelPath_observedPathTitle": "观察到的路径 {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1311,7 +1311,7 @@ } } }, - "channelPath_noLocationData": "没有位置信息", + "channelPath_noLocationData": "无位置信息", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1335,9 +1335,9 @@ } }, "channelPath_unknownPath": "未知", - "channelPath_floodPath": "洪水", - "channelPath_directPath": "直接", - "channelPath_observedZeroOf": "0 of {total} hops", + "channelPath_floodPath": "泛洪", + "channelPath_directPath": "直连", + "channelPath_observedZeroOf": "0 / {total} 跳", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1345,7 +1345,7 @@ } } }, - "channelPath_observedSomeOf": "{observed} of {total} hops", + "channelPath_observedSomeOf": "{observed} / {total} 跳", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1356,9 +1356,9 @@ } } }, - "channelPath_mapTitle": "路线图", - "channelPath_noRepeaterLocations": "这条路径上没有可用的中继器位置。", - "channelPath_primaryPath": "路径 {index} (主要路径)", + "channelPath_mapTitle": "路径地图", + "channelPath_noRepeaterLocations": "此路径上没有可用的转发节点位置信息", + "channelPath_primaryPath": "路径 {index}(主要)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1374,7 +1374,7 @@ } }, "channelPath_pathLabelTitle": "路径", - "channelPath_observedPathHeader": "观察路径", + "channelPath_observedPathHeader": "观察到的路径", "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { @@ -1386,14 +1386,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "对于此包裹,尚无详细信息。", - "channelPath_unknownRepeater": "未知的重复设备", + "channelPath_noHopDetailsAvailable": "此数据包暂无详细信息", + "channelPath_unknownRepeater": "未知转发节点", "community_title": "社区", - "community_create": "建立社区", - "community_createDesc": "创建一个新的社群,并通过二维码进行分享。", + "community_create": "创建社区", + "community_createDesc": "创建新社区并通过二维码分享。", "community_join": "加入", "community_joinTitle": "加入社区", - "community_joinConfirmation": "Do you want to join the community \"{name}\"?", + "community_joinConfirmation": "是否加入社区 \"{name}\"?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1402,13 +1402,13 @@ } }, "community_scanQr": "扫描社区二维码", - "community_scanInstructions": "将相机对准社区的二维码。", + "community_scanInstructions": "将摄像头对准社区的二维码", "community_showQr": "显示二维码", - "community_publicChannel": "社区公共", - "community_hashtagChannel": "社区标签", + "community_publicChannel": "社区公共频道", + "community_hashtagChannel": "社区标签频道", "community_name": "社区名称", "community_enterName": "请输入社区名称", - "community_created": "Community \"{name}\" created", + "community_created": "社区 \"{name}\" 已创建", "@community_created": { "placeholders": { "name": { @@ -1416,7 +1416,7 @@ } } }, - "community_joined": "Joined community \"{name}\"", + "community_joined": "已加入社区 \"{name}\"", "@community_joined": { "placeholders": { "name": { @@ -1425,7 +1425,7 @@ } }, "community_qrTitle": "分享社区", - "community_qrInstructions": "Scan this QR code to join \"{name}\"", + "community_qrInstructions": "扫描此二维码加入 \"{name}\"", "@community_qrInstructions": { "placeholders": { "name": { @@ -1433,10 +1433,10 @@ } } }, - "community_hashtagPrivacyHint": "仅社区成员才能加入社区话题标签的频道。", + "community_hashtagPrivacyHint": "仅社区成员可加入社区标签频道。", "community_invalidQrCode": "无效的社区二维码", - "community_alreadyMember": "已经是会员", - "community_alreadyMemberMessage": "You are already a member of \"{name}\".", + "community_alreadyMember": "已是成员", + "community_alreadyMemberMessage": "您已是 \"{name}\" 的成员。", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1445,12 +1445,12 @@ } }, "community_addPublicChannel": "添加公共频道", - "community_addPublicChannelHint": "自动添加该社区的公共频道", - "community_noCommunities": "目前还没有任何社区加入。", - "community_scanOrCreate": "扫描二维码或创建社群,即可开始。", + "community_addPublicChannelHint": "自动添加此社区的公共频道", + "community_noCommunities": "尚未加入任何社区。", + "community_scanOrCreate": "扫描二维码或创建社区以开始。", "community_manageCommunities": "管理社区", "community_delete": "退出社区", - "community_deleteConfirm": "是否要删除\"{name}\"?", + "community_deleteConfirm": "是否退出 \"{name}\"?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1466,7 +1466,7 @@ } } }, - "community_deleted": "Left community \"{name}\"", + "community_deleted": "已退出社区 \"{name}\"", "@community_deleted": { "placeholders": { "name": { @@ -1474,8 +1474,8 @@ } } }, - "community_regenerateSecret": "恢复秘密", - "community_regenerateSecretConfirm": "[保存:{name}]\n是否需要重新生成\"{name}\"的密钥?所有成员都需要扫描新的二维码才能继续进行通信。", + "community_regenerateSecret": "重新生成密钥", + "community_regenerateSecretConfirm": "是否为 \"{name}\" 重新生成密钥?所有成员需扫描新的二维码才能继续通信。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1483,8 +1483,8 @@ } } }, - "community_regenerate": "再生", - "community_secretRegenerated": "[保护对象:{name}]\n秘密已恢复至\"{name}\"", + "community_regenerate": "重新生成", + "community_secretRegenerated": "已为 \"{name}\" 重新生成密钥", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1492,8 +1492,8 @@ } } }, - "community_updateSecret": "更新秘密", - "community_secretUpdated": "“{name}”的秘密已更新", + "community_updateSecret": "更新密钥", + "community_secretUpdated": "“{name}”的密钥已更新", "@community_secretUpdated": { "placeholders": { "name": { @@ -1501,7 +1501,7 @@ } } }, - "community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"", + "community_scanToUpdateSecret": "扫描新二维码以更新 \"{name}\" 的密钥", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1509,14 +1509,14 @@ } } }, - "community_addHashtagChannel": "添加社区标签", - "community_addHashtagChannelDesc": "为这个社区创建一个带有话题标签的频道", + "community_addHashtagChannel": "添加标签频道", + "community_addHashtagChannelDesc": "为此社区创建标签频道", "community_selectCommunity": "选择社区", - "community_regularHashtag": "常用标签", - "community_regularHashtagDesc": "公共话题标签(任何人都可以参与)", + "community_regularHashtag": "普通标签", + "community_regularHashtagDesc": "公共标签频道(任何人都可参与)", "community_communityHashtag": "社区标签", "community_communityHashtagDesc": "仅限社区成员", - "community_forCommunity": "For {name}", + "community_forCommunity": "为 {name}", "@community_forCommunity": { "placeholders": { "name": { @@ -1524,30 +1524,30 @@ } } }, - "listFilter_tooltip": "筛选和排序", - "listFilter_sortBy": "按排序", + "listFilter_tooltip": "筛选与排序", + "listFilter_sortBy": "排序方式", "listFilter_latestMessages": "最新消息", - "listFilter_heardRecently": "最近听到的", - "listFilter_az": "A 到 Z", - "listFilter_filters": "过滤器", + "listFilter_heardRecently": "最近听到", + "listFilter_az": "A-Z", + "listFilter_filters": "筛选", "listFilter_all": "全部", "listFilter_users": "用户", - "listFilter_repeaters": "重复器", + "listFilter_repeaters": "转发节点", "listFilter_roomServers": "房间服务器", - "listFilter_unreadOnly": "仅显示未读消息", - "listFilter_newGroup": "新的团体", - "pathTrace_you": "您", + "listFilter_unreadOnly": "仅显示未读", + "listFilter_newGroup": "新建群聊", + "pathTrace_you": "我自己", "pathTrace_failed": "路径追踪失败。", "pathTrace_notAvailable": "无法获取路径信息。", - "pathTrace_refreshTooltip": "重新绘制路径。", + "pathTrace_refreshTooltip": "刷新路径追踪", "contacts_pathTrace": "路径追踪", - "contacts_ping": "乒", - "contacts_repeaterPathTrace": "追踪路径至中继器", - "contacts_repeaterPing": "中继器", - "contacts_roomPathTrace": "追踪到房间服务器", - "contacts_roomPing": "会议室服务器", - "contacts_chatTraceRoute": "路径跟踪路线", - "contacts_pathTraceTo": "追踪路径至 {name}", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Trace 转发节点", + "contacts_repeaterPing": "Ping 转发节点", + "contacts_roomPathTrace": "Trace 房间服务器", + "contacts_roomPing": "Ping 房间服务器", + "contacts_chatTraceRoute": "路由追踪", + "contacts_pathTraceTo": "追踪至 {name} 的路径", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1555,48 +1555,48 @@ } } }, - "contacts_clipboardEmpty": "剪贴板为空。", - "contacts_invalidAdvertFormat": "无效的联系信息", - "contacts_contactImported": "已建立联系。", - "contacts_contactImportFailed": "未能导入联系人。", - "contacts_zeroHopAdvert": "零跳广告", - "contacts_floodAdvert": "防洪广告", - "contacts_copyAdvertToClipboard": "复制广告到剪贴板", + "contacts_clipboardEmpty": "剪贴板为空", + "contacts_invalidAdvertFormat": "无效的联系人信息格式", + "contacts_contactImported": "联系人已导入", + "contacts_contactImportFailed": "导入联系人失败。", + "contacts_zeroHopAdvert": "发送零跳广播", + "contacts_floodAdvert": "发送泛洪广播", + "contacts_copyAdvertToClipboard": "复制广播到剪贴板", "contacts_addContactFromClipboard": "从剪贴板添加联系人", - "contacts_ShareContact": "复制联系方式到剪贴板", - "contacts_ShareContactZeroHop": "通过广告分享联系方式", - "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", - "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", - "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。", + "contacts_ShareContact": "复制联系人信息到剪贴板", + "contacts_ShareContactZeroHop": "通过广播分享联系人", + "contacts_zeroHopContactAdvertSent": "零跳广播已发送", + "contacts_zeroHopContactAdvertFailed": "发送联系人广播失败。", + "contacts_contactAdvertCopied": "广播已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "复制广播到剪贴板失败。", "notification_activityTitle": "MeshCore 活动", "notification_messagesCount": "{count} 条消息", "notification_channelMessagesCount": "{count} 条频道消息", "notification_newNodesCount": "{count} 个新节点", "notification_newTypeDiscovered": "发现新 {contactType}", "notification_receivedNewMessage": "收到新消息", - "settings_gpxExportRepeaters": "导出重复器/房间服务器到GPX", - "settings_gpxExportRepeatersSubtitle": "导出带有位置的重复器/房间服务器到GPX文件。", - "settings_gpxExportContactsSubtitle": "导出带有位置的伙伴到GPX文件。", + "settings_gpxExportRepeaters": "导出转发节点/房间服务器到 GPX", + "settings_gpxExportRepeatersSubtitle": "导出带位置的转发节点/房间服务器到 GPX 文件", + "settings_gpxExportContactsSubtitle": "导出带位置的伙伴到 GPX 文件", "settings_gpxExportNotAvailable": "您的设备/操作系统不支持", - "settings_gpxExportSuccess": "成功导出GPX文件", - "settings_gpxExportError": "导出时发生错误", - "settings_gpxExportRepeatersRoom": "重复器和房间服务器位置", - "settings_gpxExportChat": "伴侣位置", - "settings_gpxExportAll": "导出所有联系人到GPX", - "settings_gpxExportContacts": "导出伴侣到GPX", - "settings_gpxExportAllSubtitle": "导出所有带有位置的联系人到GPX文件。", + "settings_gpxExportSuccess": "GPX 文件导出成功", + "settings_gpxExportError": "导出时出错", + "settings_gpxExportRepeatersRoom": "转发节点与房间服务器位置", + "settings_gpxExportChat": "伙伴位置", + "settings_gpxExportAll": "导出所有联系人到 GPX", + "settings_gpxExportContacts": "导出伙伴到 GPX", + "settings_gpxExportAllSubtitle": "导出所有带位置的联系人到 GPX 文件", "settings_gpxExportAllContacts": "所有联系人位置", - "settings_gpxExportNoContacts": "没有联系人可导出", - "settings_gpxExportShareText": "来自meshcore-open的导出地图数据", - "settings_gpxExportShareSubject": "meshcore-open GPX 地图数据导出", - "pathTrace_someHopsNoLocation": "其中一个或多个啤酒花缺少位置!", - "map_tapToAdd": "点击节点将其添加到路径中", + "settings_gpxExportNoContacts": "没有可导出的联系人", + "settings_gpxExportShareText": "来自 MeshCore Open 的地图数据导出", + "settings_gpxExportShareSubject": "MeshCore Open GPX 地图数据导出", + "pathTrace_someHopsNoLocation": "某些跳缺少位置信息!", + "map_tapToAdd": "点击节点以添加到路径", "pathTrace_clearTooltip": "清除路径", - "map_pathTraceCancelled": "路径跟踪已取消", - "map_removeLast": "删除最后一个", - "map_runTrace": "运行路径跟踪", - "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", + "map_pathTraceCancelled": "路径追踪已取消", + "map_removeLast": "移除最后一个", + "map_runTrace": "运行路径追踪", + "scanner_bluetoothOffMessage": "请开启蓝牙以搜索设备", "scanner_bluetoothOff": "蓝牙已关闭", "scanner_enableBluetooth": "启用蓝牙" } From 947fafbbb759fb5b21b95b47fa86face97635ebd Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 17 Feb 2026 23:42:04 -0700 Subject: [PATCH 26/99] Refactor radio settings and localization updates fixes #72 - Removed preset configurations for 915 MHz, 868 MHz, and 433 MHz from the RadioSettings model. - Introduced a new list of regional preset configurations for various countries. - Updated the settings screen to use a dropdown for selecting presets instead of chips. - Added a switch for enabling client repeat functionality with appropriate warnings for frequency usage. - Updated localization files for multiple languages to reflect changes in settings related to client repeat functionality. --- lib/connector/meshcore_connector.dart | 15 +- lib/connector/meshcore_protocol.dart | 10 +- lib/l10n/app_bg.arb | 10 +- lib/l10n/app_de.arb | 10 +- lib/l10n/app_en.arb | 567 ++++++++++++++++++-------- lib/l10n/app_es.arb | 10 +- lib/l10n/app_fr.arb | 10 +- lib/l10n/app_it.arb | 10 +- lib/l10n/app_localizations.dart | 36 +- lib/l10n/app_localizations_bg.dart | 18 +- lib/l10n/app_localizations_de.dart | 18 +- lib/l10n/app_localizations_en.dart | 18 +- lib/l10n/app_localizations_es.dart | 18 +- lib/l10n/app_localizations_fr.dart | 18 +- lib/l10n/app_localizations_it.dart | 18 +- lib/l10n/app_localizations_nl.dart | 18 +- lib/l10n/app_localizations_pl.dart | 18 +- lib/l10n/app_localizations_pt.dart | 18 +- lib/l10n/app_localizations_ru.dart | 18 +- lib/l10n/app_localizations_sk.dart | 18 +- lib/l10n/app_localizations_sl.dart | 18 +- lib/l10n/app_localizations_sv.dart | 18 +- lib/l10n/app_localizations_uk.dart | 18 +- lib/l10n/app_localizations_zh.dart | 17 +- lib/l10n/app_nl.arb | 10 +- lib/l10n/app_pl.arb | 10 +- lib/l10n/app_pt.arb | 10 +- lib/l10n/app_ru.arb | 10 +- lib/l10n/app_sk.arb | 10 +- lib/l10n/app_sl.arb | 10 +- lib/l10n/app_sv.arb | 10 +- lib/l10n/app_uk.arb | 10 +- lib/l10n/app_zh.arb | 10 +- lib/models/radio_settings.dart | 63 ++- lib/screens/settings_screen.dart | 86 ++-- 35 files changed, 660 insertions(+), 526 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 5d6c7e6f..5cdab789 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -90,6 +90,8 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentBwHz; int? _currentSf; int? _currentCr; + bool? _clientRepeat; + int? _firmwareVerCode; int? _batteryMillivolts; double? _selfLatitude; double? _selfLongitude; @@ -200,6 +202,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentBwHz => _currentBwHz; int? get currentSf => _currentSf; int? get currentCr => _currentCr; + bool? get clientRepeat => _clientRepeat; + int? get firmwareVerCode => _firmwareVerCode; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; @@ -916,6 +920,8 @@ class MeshCoreConnector extends ChangeNotifier { _selfName = null; _selfLatitude = null; _selfLongitude = null; + _clientRepeat = null; + _firmwareVerCode = null; _batteryMillivolts = null; _batteryRequested = false; _awaitingSelfInfo = false; @@ -1820,6 +1826,13 @@ class MeshCoreConnector extends ChangeNotifier { void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; + _firmwareVerCode = frame[1]; + + // Parse client_repeat from firmware v9+ (byte 80) + if (frame.length >= 81) { + _clientRepeat = frame[80] != 0; + } + // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; @@ -1840,8 +1853,8 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(getChannels(maxChannels: nextMaxChannels)); } } - notifyListeners(); } + notifyListeners(); } void _handleNoMoreMessages() { diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 25359a8d..0b78c65f 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -550,18 +550,24 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) { } // Build CMD_SET_RADIO_PARAMS frame -// Format: [cmd][freq x4][bw x4][sf][cr] +// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9) +// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+) // freq: frequency in Hz (300000-2500000) // bw: bandwidth in Hz (7000-500000) // sf: spreading factor (5-12) // cr: coding rate (5-8) -Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) { +// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older) +Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr, + {bool? clientRepeat}) { final writer = BufferWriter(); writer.writeByte(cmdSetRadioParams); writer.writeUInt32LE(freqHz); writer.writeUInt32LE(bwHz); writer.writeByte(sf); writer.writeByte(cr); + if (clientRepeat != null) { + writer.writeByte(clientRepeat ? 1 : 0); + } return writer.toBytes(); } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index a1cfb3ed..567c74c6 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Брой контакти", "settings_infoChannelCount": "Брой канали", "settings_presets": "Предварителни настройки", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Честота (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Мощност (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)", - "settings_longRange": "Дълъг обхват", - "settings_fastSpeed": "Бърза скорост", "settings_error": "Грешка: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.", "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", - "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства." + "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", + "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", + "settings_clientRepeat": "Без електричество – повторение" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2e662227..7aba89b1 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Anzahl Kontakte", "settings_infoChannelCount": "Anzahl Kanäle", "settings_presets": "Voreinstellungen", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequenz (MHz)", "settings_frequencyHelper": "300,00 - 2.500,00", "settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX-Leistung (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)", - "settings_longRange": "Grosse Reichweite", - "settings_fastSpeed": "Schnelle Geschwindigkeit", "settings_error": "Fehler: {message}", "@settings_error": { "placeholders": { @@ -1626,5 +1621,8 @@ "map_pathTraceCancelled": "Pfadverfolgung abgebrochen.", "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", - "scanner_enableBluetooth": "Bluetooth aktivieren" + "scanner_enableBluetooth": "Bluetooth aktivieren", + "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", + "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", + "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 890b992e..cfd63300 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,12 +1,9 @@ { "@@locale": "en", - "appTitle": "MeshCore Open", - "nav_contacts": "Contacts", "nav_channels": "Channels", "nav_map": "Map", - "common_cancel": "Cancel", "common_ok": "OK", "common_connect": "Connect", @@ -35,16 +32,19 @@ "common_voltageValue": "{volts} V", "@common_voltageValue": { "placeholders": { - "volts": {"type": "String"} + "volts": { + "type": "String" + } } }, "common_percentValue": "{percent}%", "@common_percentValue": { "placeholders": { - "percent": {"type": "int"} + "percent": { + "type": "int" + } } }, - "scanner_title": "MeshCore Open", "scanner_scanning": "Scanning for devices...", "scanner_connecting": "Connecting...", @@ -53,7 +53,9 @@ "scanner_connectedTo": "Connected to {deviceName}", "@scanner_connectedTo": { "placeholders": { - "deviceName": {"type": "String"} + "deviceName": { + "type": "String" + } } }, "scanner_searchingDevices": "Searching for MeshCore devices...", @@ -61,7 +63,9 @@ "scanner_connectionFailed": "Connection failed: {error}", "@scanner_connectionFailed": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "scanner_stop": "Stop", @@ -69,10 +73,8 @@ "scanner_bluetoothOff": "Bluetooth is off", "scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices", "scanner_enableBluetooth": "Enable Bluetooth", - "device_quickSwitch": "Quick switch", "device_meshcore": "MeshCore", - "settings_title": "Settings", "settings_deviceInfo": "Device Info", "settings_appSettings": "App Settings", @@ -122,7 +124,9 @@ "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { "placeholders": { - "version": {"type": "String"} + "version": { + "type": "String" + } } }, "settings_aboutLegalese": "2026 MeshCore Open Source Project", @@ -135,9 +139,6 @@ "settings_infoContactsCount": "Contacts Count", "settings_infoChannelCount": "Channel Count", "settings_presets": "Presets", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequency (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "Invalid frequency (300-2500 MHz)", @@ -147,15 +148,17 @@ "settings_txPower": "TX Power (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Invalid TX power (0-22 dBm)", - "settings_longRange": "Long Range", - "settings_fastSpeed": "Fast Speed", + "settings_clientRepeat": "Off-Grid Repeat", + "settings_clientRepeatSubtitle": "Allow this device to repeat mesh packets for others", + "settings_clientRepeatFreqWarning": "Off-grid repeat requires 433, 869, or 918 MHz frequency", "settings_error": "Error: {message}", "@settings_error": { "placeholders": { - "message": {"type": "String"} + "message": { + "type": "String" + } } }, - "appSettings_title": "App Settings", "appSettings_appearance": "Appearance", "appSettings_theme": "Theme", @@ -205,7 +208,9 @@ "appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { - "deviceName": {"type": "String"} + "deviceName": { + "type": "String" + } } }, "appSettings_batteryChemistryConnectFirst": "Connect to a device to choose", @@ -224,7 +229,9 @@ "appSettings_timeFilterShowLast": "Show nodes from last {hours} hours", "@appSettings_timeFilterShowLast": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "appSettings_mapTimeFilter": "Map Time Filter", @@ -239,8 +246,12 @@ "appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { - "minZoom": {"type": "int"}, - "maxZoom": {"type": "int"} + "minZoom": { + "type": "int" + }, + "maxZoom": { + "type": "int" + } } }, "appSettings_debugCard": "Debug", @@ -248,7 +259,6 @@ "appSettings_appDebugLoggingSubtitle": "Log app debug messages for troubleshooting", "appSettings_appDebugLoggingEnabled": "App debug logging enabled", "appSettings_appDebugLoggingDisabled": "App debug logging disabled", - "contacts_title": "Contacts", "contacts_noContacts": "No contacts yet", "contacts_contactsWillAppear": "Contacts will appear when devices advertise", @@ -259,7 +269,9 @@ "contacts_removeConfirm": "Remove {contactName} from contacts?", "@contacts_removeConfirm": { "placeholders": { - "contactName": {"type": "String"} + "contactName": { + "type": "String" + } } }, "contacts_manageRepeater": "Manage Repeater", @@ -271,7 +283,9 @@ "contacts_deleteGroupConfirm": "Remove \"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { - "groupName": {"type": "String"} + "groupName": { + "type": "String" + } } }, "contacts_newGroup": "New Group", @@ -280,7 +294,9 @@ "contacts_groupAlreadyExists": "Group \"{name}\" already exists", "@contacts_groupAlreadyExists": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "contacts_filterContacts": "Filter contacts...", @@ -290,24 +306,29 @@ "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", "@contacts_lastSeenMinsAgo": { "placeholders": { - "minutes": {"type": "int"} + "minutes": { + "type": "int" + } } }, "contacts_lastSeenHourAgo": "Last seen 1 hour ago", "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", "@contacts_lastSeenHoursAgo": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "contacts_lastSeenDayAgo": "Last seen 1 day ago", "contacts_lastSeenDaysAgo": "Last seen {days} days ago", "@contacts_lastSeenDaysAgo": { "placeholders": { - "days": {"type": "int"} + "days": { + "type": "int" + } } }, - "channels_title": "Channels", "channels_noChannelsConfigured": "No channels configured", "channels_addPublicChannel": "Add Public Channel", @@ -316,7 +337,9 @@ "channels_channelIndex": "Channel {index}", "@channels_channelIndex": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, "channels_hashtagChannel": "Hashtag channel", @@ -329,13 +352,17 @@ "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_channelDeleted": "Channel \"{name}\" deleted", "@channels_channelDeleted": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_addChannel": "Add Channel", @@ -350,20 +377,26 @@ "channels_channelAdded": "Channel \"{name}\" added", "@channels_channelAdded": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_editChannelTitle": "Edit Channel {index}", "@channels_editChannelTitle": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, "channels_smazCompression": "SMAZ compression", "channels_channelUpdated": "Channel \"{name}\" updated", "@channels_channelUpdated": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "channels_publicChannelAdded": "Public channel added", @@ -384,34 +417,41 @@ "channels_scanQrCodeComingSoon": "Coming soon", "channels_enterHashtag": "Enter hashtag", "channels_hashtagHint": "e.g. #team", - "chat_noMessages": "No messages yet", "chat_sendMessageToStart": "Send a message to get started", "chat_originalMessageNotFound": "Original message not found", "chat_replyingTo": "Replying to {name}", "@chat_replyingTo": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "chat_replyTo": "Reply to {name}", "@chat_replyTo": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "chat_location": "Location", "chat_sendMessageTo": "Send a message to {contactName}", "@chat_sendMessageTo": { "placeholders": { - "contactName": {"type": "String"} + "contactName": { + "type": "String" + } } }, "chat_typeMessage": "Type a message...", "chat_messageTooLong": "Message too long (max {maxBytes} bytes).", "@chat_messageTooLong": { "placeholders": { - "maxBytes": {"type": "int"} + "maxBytes": { + "type": "int" + } } }, "chat_messageCopied": "Message copied", @@ -420,8 +460,12 @@ "chat_retryCount": "Retry {current}/{max}", "@chat_retryCount": { "placeholders": { - "current": {"type": "int"}, - "max": {"type": "int"} + "current": { + "type": "int" + }, + "max": { + "type": "int" + } } }, "chat_sendGif": "Send GIF", @@ -453,39 +497,53 @@ "debugFrame_length": "Frame Length: {count} bytes", "@debugFrame_length": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "debugFrame_command": "Command: 0x{value}", "@debugFrame_command": { "placeholders": { - "value": {"type": "String"} + "value": { + "type": "String" + } } }, "debugFrame_textMessageHeader": "Text Message Frame:", "debugFrame_destinationPubKey": "- Destination PubKey: {pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { - "pubKey": {"type": "String"} + "pubKey": { + "type": "String" + } } }, "debugFrame_timestamp": "- Timestamp: {timestamp}", "@debugFrame_timestamp": { "placeholders": { - "timestamp": {"type": "int"} + "timestamp": { + "type": "int" + } } }, "debugFrame_flags": "- Flags: 0x{value}", "@debugFrame_flags": { "placeholders": { - "value": {"type": "String"} + "value": { + "type": "String" + } } }, "debugFrame_textType": "- Text Type: {type} ({label})", "@debugFrame_textType": { "placeholders": { - "type": {"type": "int"}, - "label": {"type": "String"} + "type": { + "type": "int" + }, + "label": { + "type": "String" + } } }, "debugFrame_textTypeCli": "CLI", @@ -493,7 +551,9 @@ "debugFrame_text": "- Text: \"{text}\"", "@debugFrame_text": { "placeholders": { - "text": {"type": "String"} + "text": { + "type": "String" + } } }, "debugFrame_hexDump": "Hex Dump:", @@ -508,7 +568,9 @@ "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "chat_successes": "successes", @@ -527,8 +589,12 @@ "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { - "hopCount": {"type": "int"}, - "status": {"type": "String"} + "hopCount": { + "type": "int" + }, + "status": { + "type": "String" + } } }, "chat_pathSavedLocally": "Saved locally. Connect to sync.", @@ -543,7 +609,9 @@ "chat_hopsForced": "{count} hops (forced)", "@chat_hopsForced": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "chat_floodAuto": "Flood (auto)", @@ -552,7 +620,9 @@ "chat_unread": "Unread: {count}", "@chat_unread": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "chat_openLink": "Open Link?", @@ -561,24 +631,29 @@ "chat_couldNotOpenLink": "Could not open link: {url}", "@chat_couldNotOpenLink": { "placeholders": { - "url": {"type": "String"} + "url": { + "type": "String" + } } }, "chat_invalidLink": "Invalid link format", - "map_title": "Node Map", "map_noNodesWithLocation": "No nodes with location data", "map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map", "map_nodesCount": "Nodes: {count}", "@map_nodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "map_pinsCount": "Pins: {count}", "@map_pinsCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "map_chat": "Chat", @@ -604,7 +679,9 @@ "map_publicLocationShareConfirm": "You are about to share a location in {channelLabel}. This channel is public and anyone with the PSK can see it.", "@map_publicLocationShareConfirm": { "placeholders": { - "channelLabel": {"type": "String"} + "channelLabel": { + "type": "String" + } } }, "map_connectToShareMarkers": "Connect to a device to share markers", @@ -633,21 +710,29 @@ "mapCache_downloadTilesPrompt": "Download {count} tiles for offline use?", "@mapCache_downloadTilesPrompt": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_downloadAction": "Download", "mapCache_cachedTiles": "Cached {count} tiles", "@mapCache_cachedTiles": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", "@mapCache_cachedTilesWithFailed": { "placeholders": { - "downloaded": {"type": "int"}, - "failed": {"type": "int"} + "downloaded": { + "type": "int" + }, + "failed": { + "type": "int" + } } }, "mapCache_clearOfflineCacheTitle": "Clear offline cache", @@ -660,14 +745,20 @@ "mapCache_estimatedTiles": "Estimated tiles: {count}", "@mapCache_estimatedTiles": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_downloadedTiles": "Downloaded {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { - "completed": {"type": "int"}, - "total": {"type": "int"} + "completed": { + "type": "int" + }, + "total": { + "type": "int" + } } }, "mapCache_downloadTilesButton": "Download Tiles", @@ -675,36 +766,51 @@ "mapCache_failedDownloads": "Failed downloads: {count}", "@mapCache_failedDownloads": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { - "north": {"type": "String"}, - "south": {"type": "String"}, - "east": {"type": "String"}, - "west": {"type": "String"} + "north": { + "type": "String" + }, + "south": { + "type": "String" + }, + "east": { + "type": "String" + }, + "west": { + "type": "String" + } } }, - "time_justNow": "Just now", "time_minutesAgo": "{minutes}m ago", "@time_minutesAgo": { "placeholders": { - "minutes": {"type": "int"} + "minutes": { + "type": "int" + } } }, "time_hoursAgo": "{hours}h ago", "@time_hoursAgo": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "time_daysAgo": "{days}d ago", "@time_daysAgo": { "placeholders": { - "days": {"type": "int"} + "days": { + "type": "int" + } } }, "time_hour": "hour", @@ -717,10 +823,8 @@ "time_months": "months", "time_minutes": "minutes", "time_allTime": "All Time", - "dialog_disconnect": "Disconnect", "dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?", - "login_repeaterLogin": "Repeater Login", "login_roomLogin": "Room Server Login", "login_password": "Password", @@ -738,32 +842,39 @@ "login_attempt": "Attempt {current}/{max}", "@login_attempt": { "placeholders": { - "current": {"type": "int"}, - "max": {"type": "int"} + "current": { + "type": "int" + }, + "max": { + "type": "int" + } } }, "login_failed": "Login failed: {error}", "@login_failed": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.", - - "common_reload": "Reload", "common_clear": "Clear", - "path_currentPath": "Current path: {path}", "@path_currentPath": { "placeholders": { - "path": {"type": "String"} + "path": { + "type": "String" + } } }, "path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path", "@path_usingHopsPath": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "path_enterCustomPath": "Enter Custom Path", @@ -778,12 +889,13 @@ "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { - "prefixes": {"type": "String"} + "prefixes": { + "type": "String" + } } }, "path_tooLong": "Path too long. Maximum 64 hops allowed.", "path_setPath": "Set Path", - "repeater_management": "Repeater Management", "room_management": "Room Server Management", "repeater_managementTools": "Management Tools", @@ -797,7 +909,6 @@ "repeater_neighboursSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", - "repeater_statusTitle": "Repeater Status", "repeater_routingMode": "Routing mode", "repeater_autoUseSavedPath": "Auto (use saved path)", @@ -808,7 +919,9 @@ "repeater_errorLoadingStatus": "Error loading status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_systemInformation": "System Information", @@ -830,42 +943,67 @@ "repeater_daysHoursMinsSecs": "{days} days {hours}h {minutes}m {seconds}s", "@repeater_daysHoursMinsSecs": { "placeholders": { - "days": {"type": "int"}, - "hours": {"type": "int"}, - "minutes": {"type": "int"}, - "seconds": {"type": "int"} + "days": { + "type": "int" + }, + "hours": { + "type": "int" + }, + "minutes": { + "type": "int" + }, + "seconds": { + "type": "int" + } } }, "repeater_packetTxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetTxTotal": { "placeholders": { - "total": {"type": "int"}, - "flood": {"type": "String"}, - "direct": {"type": "String"} + "total": { + "type": "int" + }, + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } } }, "repeater_packetRxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetRxTotal": { "placeholders": { - "total": {"type": "int"}, - "flood": {"type": "String"}, - "direct": {"type": "String"} + "total": { + "type": "int" + }, + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } } }, "repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { - "flood": {"type": "String"}, - "direct": {"type": "String"} + "flood": { + "type": "String" + }, + "direct": { + "type": "String" + } } }, "repeater_duplicatesTotal": "Total: {total}", "@repeater_duplicatesTotal": { "placeholders": { - "total": {"type": "int"} + "total": { + "type": "int" + } } }, - "repeater_settingsTitle": "Repeater Settings", "repeater_basicSettings": "Basic Settings", "repeater_repeaterName": "Repeater Name", @@ -899,14 +1037,18 @@ "repeater_localAdvertIntervalMinutes": "{minutes} minutes", "@repeater_localAdvertIntervalMinutes": { "placeholders": { - "minutes": {"type": "int"} + "minutes": { + "type": "int" + } } }, "repeater_floodAdvertInterval": "Flood Advertisement Interval", "repeater_floodAdvertIntervalHours": "{hours} hours", "@repeater_floodAdvertIntervalHours": { "placeholders": { - "hours": {"type": "int"} + "hours": { + "type": "int" + } } }, "repeater_encryptedAdvertInterval": "Encrypted Advertisement Interval", @@ -924,13 +1066,17 @@ "repeater_commandSent": "Command sent: {command}", "@repeater_commandSent": { "placeholders": { - "command": {"type": "String"} + "command": { + "type": "String" + } } }, "repeater_errorSendingCommand": "Error sending command: {error}", "@repeater_errorSendingCommand": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_confirm": "Confirm", @@ -938,7 +1084,9 @@ "repeater_errorSavingSettings": "Error saving settings: {error}", "@repeater_errorSavingSettings": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_refreshBasicSettings": "Refresh Basic Settings", @@ -952,16 +1100,19 @@ "repeater_refreshed": "{label} refreshed", "@repeater_refreshed": { "placeholders": { - "label": {"type": "String"} + "label": { + "type": "String" + } } }, "repeater_errorRefreshing": "Error refreshing {label}", "@repeater_errorRefreshing": { "placeholders": { - "label": {"type": "String"} + "label": { + "type": "String" + } } }, - "repeater_cliTitle": "Repeater CLI", "repeater_debugNextCommand": "Debug Next Command", "repeater_commandHelp": "Command Help", @@ -976,7 +1127,9 @@ "repeater_cliCommandError": "Error: {error}", "@repeater_cliCommandError": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "repeater_cliQuickGetName": "Get Name", @@ -1056,14 +1209,18 @@ "telemetry_errorLoading": "Error loading telemetry: {error}", "@telemetry_errorLoading": { "placeholders": { - "error": {"type": "String"} + "error": { + "type": "String" + } } }, "telemetry_noData": "No telemetry data available.", "telemetry_channelTitle": "Channel {channel}", "@telemetry_channelTitle": { "placeholders": { - "channel": {"type": "int"} + "channel": { + "type": "int" + } } }, "telemetry_batteryLabel": "Battery", @@ -1074,36 +1231,49 @@ "telemetry_batteryValue": "{percent}% / {volts}V", "@telemetry_batteryValue": { "placeholders": { - "percent": {"type": "int"}, - "volts": {"type": "String"} + "percent": { + "type": "int" + }, + "volts": { + "type": "String" + } } }, "telemetry_voltageValue": "{volts}V", "@telemetry_voltageValue": { "placeholders": { - "volts": {"type": "String"} + "volts": { + "type": "String" + } } }, "telemetry_currentValue": "{amps}A", "@telemetry_currentValue": { "placeholders": { - "amps": {"type": "String"} + "amps": { + "type": "String" + } } }, "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F", "@telemetry_temperatureValue": { "placeholders": { - "celsius": {"type": "String"}, - "fahrenheit": {"type": "String"} + "celsius": { + "type": "String" + }, + "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"} + "error": { + "type": "String" + } } }, "neighbors_repeatersNeighbours": "Repeaters Neighbours", @@ -1111,13 +1281,17 @@ "neighbors_unknownContact": "Unknown {pubkey}", "@neighbors_unknownContact": { "placeholders": { - "pubkey": {"type": "String"} + "pubkey": { + "type": "String" + } } }, "neighbors_heardAgo": "Heard: {time} ago", "@neighbors_heardAgo": { "placeholders": { - "time": {"type": "String"} + "time": { + "type": "String" + } } }, "channelPath_title": "Packet Path", @@ -1129,28 +1303,40 @@ "channelPath_senderLabel": "Sender", "channelPath_timeLabel": "Time", "channelPath_repeatsLabel": "Repeats", - "channelPath_pathLabel": "Path", + "channelPath_pathLabel": "Path {index}", "channelPath_observedLabel": "Observed", "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { - "index": {"type": "int"}, - "hops": {"type": "String"} + "index": { + "type": "int" + }, + "hops": { + "type": "String" + } } }, "channelPath_noLocationData": "No location data", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { - "day": {"type": "int"}, - "month": {"type": "int"}, - "time": {"type": "String"} + "day": { + "type": "int" + }, + "month": { + "type": "int" + }, + "time": { + "type": "String" + } } }, "channelPath_timeOnly": "{time}", "@channelPath_timeOnly": { "placeholders": { - "time": {"type": "String"} + "time": { + "type": "String" + } } }, "channelPath_unknownPath": "Unknown", @@ -1159,14 +1345,20 @@ "channelPath_observedZeroOf": "0 of {total} hops", "@channelPath_observedZeroOf": { "placeholders": { - "total": {"type": "int"} + "total": { + "type": "int" + } } }, "channelPath_observedSomeOf": "{observed} of {total} hops", "@channelPath_observedSomeOf": { "placeholders": { - "observed": {"type": "int"}, - "total": {"type": "int"} + "observed": { + "type": "int" + }, + "total": { + "type": "int" + } } }, "channelPath_mapTitle": "Path Map", @@ -1174,13 +1366,16 @@ "channelPath_primaryPath": "Path {index} (Primary)", "@channelPath_primaryPath": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, - "channelPath_pathLabel": "Path {index}", "@channelPath_pathLabel": { "placeholders": { - "index": {"type": "int"} + "index": { + "type": "int" + } } }, "channelPath_pathLabelTitle": "Path", @@ -1188,13 +1383,16 @@ "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { - "label": {"type": "String"}, - "prefixes": {"type": "String"} + "label": { + "type": "String" + }, + "prefixes": { + "type": "String" + } } }, "channelPath_noHopDetailsAvailable": "No hop details available for this packet.", "channelPath_unknownRepeater": "Unknown Repeater", - "community_title": "Community", "community_create": "Create Community", "community_createDesc": "Create a new community and share via QR code.", @@ -1203,7 +1401,9 @@ "community_joinConfirmation": "Do you want to join the community \"{name}\"?", "@community_joinConfirmation": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_scanQr": "Scan Community QR", @@ -1216,20 +1416,26 @@ "community_created": "Community \"{name}\" created", "@community_created": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_joined": "Joined community \"{name}\"", "@community_joined": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_qrTitle": "Share Community", "community_qrInstructions": "Scan this QR code to join \"{name}\"", "@community_qrInstructions": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_hashtagPrivacyHint": "Community hashtag channels are only joinable by members of the community", @@ -1238,7 +1444,9 @@ "community_alreadyMemberMessage": "You are already a member of \"{name}\".", "@community_alreadyMemberMessage": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_addPublicChannel": "Add Community Public Channel", @@ -1250,46 +1458,60 @@ "community_deleteConfirm": "Leave \"{name}\"?", "@community_deleteConfirm": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_deleteChannelsWarning": "This will also delete {count} channel(s) and their messages.", "@community_deleteChannelsWarning": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "community_deleted": "Left community \"{name}\"", "@community_deleted": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_regenerateSecret": "Regenerate Secret", "community_regenerateSecretConfirm": "Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.", "@community_regenerateSecretConfirm": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_regenerate": "Regenerate", "community_secretRegenerated": "Secret regenerated for \"{name}\"", "@community_secretRegenerated": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_updateSecret": "Update Secret", "community_secretUpdated": "Secret updated for \"{name}\"", "@community_secretUpdated": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"", "@community_scanToUpdateSecret": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, "community_addHashtagChannel": "Add Community Hashtag", @@ -1302,10 +1524,11 @@ "community_forCommunity": "For {name}", "@community_forCommunity": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, - "listFilter_tooltip": "Filter and sort", "listFilter_sortBy": "Sort by", "listFilter_latestMessages": "Latest messages", @@ -1318,7 +1541,6 @@ "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", "listFilter_newGroup": "New group", - "pathTrace_you": "You", "pathTrace_failed": "Path trace failed.", "pathTrace_notAvailable": "Path trace not available.", @@ -1335,52 +1557,59 @@ "contacts_pathTraceTo": "Trace route to {name}", "@contacts_pathTraceTo": { "placeholders": { - "name": {"type": "String"} + "name": { + "type": "String" + } } }, - "contacts_clipboardEmpty": "Clipboard is empty.", "contacts_invalidAdvertFormat": "Invalid contact data", "contacts_contactImported": "Contact has been imported.", "contacts_contactImportFailed": "Failed to import contact.", - "contacts_zeroHopAdvert":"Zero Hop Advert", - "contacts_floodAdvert":"Flood Advert", - "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", - "contacts_addContactFromClipboard":"Add Contact from Clipboard", + "contacts_zeroHopAdvert": "Zero Hop Advert", + "contacts_floodAdvert": "Flood Advert", + "contacts_copyAdvertToClipboard": "Copy Advert to Clipboard", + "contacts_addContactFromClipboard": "Add Contact from Clipboard", "contacts_ShareContact": "Copy contact to Clipboard", "contacts_ShareContactZeroHop": "Share contact by advert", "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", "contacts_zeroHopContactAdvertFailed": "Failed to send contact.", "contacts_contactAdvertCopied": "Advert copied to Clipboard.", "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed.", - "notification_activityTitle": "MeshCore Activity", "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}", "@notification_messagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_channelMessagesCount": "{count} {count, plural, =1{channel message} other{channel messages}}", "@notification_channelMessagesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newNodesCount": "{count} {count, plural, =1{new node} other{new nodes}}", "@notification_newNodesCount": { "placeholders": { - "count": {"type": "int"} + "count": { + "type": "int" + } } }, "notification_newTypeDiscovered": "New {contactType} discovered", "@notification_newTypeDiscovered": { "placeholders": { - "contactType": {"type": "String"} + "contactType": { + "type": "String" + } } }, "notification_receivedNewMessage": "Received new message", - "settings_gpxExportRepeaters": "Export repeaters / room server to GPX", "settings_gpxExportRepeatersSubtitle": "Exports repeaters / roomserver with a location to GPX file.", "settings_gpxExportContacts": "Export companions to GPX", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e6b22fad..4250a6f6 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Número de contactos", "settings_infoChannelCount": "Número de canales", "settings_presets": "Preajustes", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frecuencia (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Potencia (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)", - "settings_longRange": "Largo Alcance", - "settings_fastSpeed": "Velocidad Rápida", "settings_error": "Error: {message}", "@settings_error": { "placeholders": { @@ -1626,5 +1621,8 @@ "map_pathTraceCancelled": "Rastreo de ruta cancelado.", "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_bluetoothOff": "Bluetooth está desactivado.", - "scanner_enableBluetooth": "Habilitar Bluetooth" + "scanner_enableBluetooth": "Habilitar Bluetooth", + "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", + "settings_clientRepeat": "Repetir sin conexión", + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f11238bf..56d4a410 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Nombre de contacts", "settings_infoChannelCount": "Nombre de canaux", "settings_presets": "Préréglages", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Fréquence (MHz)", "settings_frequencyHelper": "300,0 - 2 500,0", "settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Puissance (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)", - "settings_longRange": "Portée Longue", - "settings_fastSpeed": "Vitesse Rapide", "settings_error": "Erreur : {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_runTrace": "Exécuter la traçage de chemin", "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", - "scanner_enableBluetooth": "Activer le Bluetooth" + "scanner_enableBluetooth": "Activer le Bluetooth", + "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", + "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", + "settings_clientRepeat": "Répétition hors réseau" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 601f1af0..239c7659 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Numero contatti", "settings_infoChannelCount": "Numero Canale", "settings_presets": "Preset", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequenza (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Potenza (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)", - "settings_longRange": "Lungo Raggio", - "settings_fastSpeed": "Velocità Rapida", "settings_error": "Errore: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.", "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", - "scanner_enableBluetooth": "Abilita il Bluetooth" + "scanner_enableBluetooth": "Abilita il Bluetooth", + "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", + "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", + "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index bc1cfbd7..7235e90b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -748,24 +748,6 @@ abstract class AppLocalizations { /// **'Presets'** String get settings_presets; - /// No description provided for @settings_preset915Mhz. - /// - /// In en, this message translates to: - /// **'915 MHz'** - String get settings_preset915Mhz; - - /// No description provided for @settings_preset868Mhz. - /// - /// In en, this message translates to: - /// **'868 MHz'** - String get settings_preset868Mhz; - - /// No description provided for @settings_preset433Mhz. - /// - /// In en, this message translates to: - /// **'433 MHz'** - String get settings_preset433Mhz; - /// No description provided for @settings_frequency. /// /// In en, this message translates to: @@ -820,17 +802,23 @@ abstract class AppLocalizations { /// **'Invalid TX power (0-22 dBm)'** String get settings_txPowerInvalid; - /// No description provided for @settings_longRange. + /// No description provided for @settings_clientRepeat. /// /// In en, this message translates to: - /// **'Long Range'** - String get settings_longRange; + /// **'Off-Grid Repeat'** + String get settings_clientRepeat; - /// No description provided for @settings_fastSpeed. + /// No description provided for @settings_clientRepeatSubtitle. /// /// In en, this message translates to: - /// **'Fast Speed'** - String get settings_fastSpeed; + /// **'Allow this device to repeat mesh packets for others'** + String get settings_clientRepeatSubtitle; + + /// No description provided for @settings_clientRepeatFreqWarning. + /// + /// In en, this message translates to: + /// **'Off-grid repeat requires 433, 869, or 918 MHz frequency'** + String get settings_clientRepeatFreqWarning; /// No description provided for @settings_error. /// diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 695bde29..fdf9ec7e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -350,15 +350,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_presets => 'Предварителни настройки'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Честота (MHz)'; @@ -387,10 +378,15 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)'; @override - String get settings_longRange => 'Дълъг обхват'; + String get settings_clientRepeat => 'Без електричество – повторение'; @override - String get settings_fastSpeed => 'Бърза скорост'; + String get settings_clientRepeatSubtitle => + 'Позволете на това устройство да предава пакети към мрежата за други устройства.'; + + @override + String get settings_clientRepeatFreqWarning => + 'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 6e04655b..c0fc4c86 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -344,15 +344,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_presets => 'Voreinstellungen'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequenz (MHz)'; @@ -381,10 +372,15 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_txPowerInvalid => 'Ungültige TX-Leistung (0-22 dBm)'; @override - String get settings_longRange => 'Grosse Reichweite'; + String get settings_clientRepeat => 'Wiederholung, ohne Stromanschluss'; @override - String get settings_fastSpeed => 'Schnelle Geschwindigkeit'; + String get settings_clientRepeatSubtitle => + 'Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5ed81627..4f0bed12 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -342,15 +342,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_presets => 'Presets'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequency (MHz)'; @@ -379,10 +370,15 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)'; @override - String get settings_longRange => 'Long Range'; + String get settings_clientRepeat => 'Off-Grid Repeat'; @override - String get settings_fastSpeed => 'Fast Speed'; + String get settings_clientRepeatSubtitle => + 'Allow this device to repeat mesh packets for others'; + + @override + String get settings_clientRepeatFreqWarning => + 'Off-grid repeat requires 433, 869, or 918 MHz frequency'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ff4e8f36..f56e4e4a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -347,15 +347,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_presets => 'Preajustes'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frecuencia (MHz)'; @@ -384,10 +375,15 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)'; @override - String get settings_longRange => 'Largo Alcance'; + String get settings_clientRepeat => 'Repetir sin conexión'; @override - String get settings_fastSpeed => 'Velocidad Rápida'; + String get settings_clientRepeatSubtitle => + 'Permita que este dispositivo repita los paquetes de red para otros usuarios.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f8b77757..e1325dae 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -348,15 +348,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_presets => 'Préréglages'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Fréquence (MHz)'; @@ -385,10 +376,15 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_txPowerInvalid => 'Puissance TX invalide (0-22 dBm)'; @override - String get settings_longRange => 'Portée Longue'; + String get settings_clientRepeat => 'Répétition hors réseau'; @override - String get settings_fastSpeed => 'Vitesse Rapide'; + String get settings_clientRepeatSubtitle => + 'Permettez à cet appareil de répéter les paquets de données pour les autres.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Pour les transmissions hors réseau, il est nécessaire d\'utiliser les fréquences de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8fd6128..15d5354d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -346,15 +346,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_presets => 'Preset'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequenza (MHz)'; @@ -383,10 +374,15 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)'; @override - String get settings_longRange => 'Lungo Raggio'; + String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"'; @override - String get settings_fastSpeed => 'Velocità Rapida'; + String get settings_clientRepeatSubtitle => + 'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index de6c9099..17b3bce1 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -344,15 +344,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_presets => 'Presets'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequentie (MHz)'; @@ -381,10 +372,15 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)'; @override - String get settings_longRange => 'Lange Afstand'; + String get settings_clientRepeat => 'Herhalen: Afgekoppeld'; @override - String get settings_fastSpeed => 'Hoge Snelheid'; + String get settings_clientRepeatSubtitle => + 'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c5d2bd9e..147e1606 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -347,15 +347,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_presets => 'Preset'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Częstotliwość (MHz)'; @@ -385,10 +376,15 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)'; @override - String get settings_longRange => 'Długi zasięg'; + String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci'; @override - String get settings_fastSpeed => 'Szybka prędkość'; + String get settings_clientRepeatSubtitle => + 'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b5ffdd6f..b4817757 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -348,15 +348,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_presets => 'Presets'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frequência (MHz)'; @@ -385,10 +376,15 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)'; @override - String get settings_longRange => 'Alcance Longo'; + String get settings_clientRepeat => 'Repetição sem rede'; @override - String get settings_fastSpeed => 'Velocidade Rápida'; + String get settings_clientRepeatSubtitle => + 'Permita que este dispositivo repita pacotes de rede para outros dispositivos.'; + + @override + String get settings_clientRepeatFreqWarning => + 'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c41bf20c..e5875b13 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -345,15 +345,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_presets => 'Пресеты'; - @override - String get settings_preset915Mhz => '915 МГц'; - - @override - String get settings_preset868Mhz => '868 МГц'; - - @override - String get settings_preset433Mhz => '433 МГц'; - @override String get settings_frequency => 'Частота (МГц)'; @@ -383,10 +374,15 @@ class AppLocalizationsRu extends AppLocalizations { 'Недопустимая мощность передачи (0–22 дБм)'; @override - String get settings_longRange => 'Дальний радиус'; + String get settings_clientRepeat => 'Повторение \"вне сети\"'; @override - String get settings_fastSpeed => 'Высокая скорость'; + String get settings_clientRepeatSubtitle => + 'Позвольте этому устройству повторять пакеты данных для других устройств.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index e0ee455e..4e8b4cb7 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -344,15 +344,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_presets => 'Prednastavenia'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frekvencia (MHz)'; @@ -381,10 +372,15 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)'; @override - String get settings_longRange => 'Dlhý dosah'; + String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete'; @override - String get settings_fastSpeed => 'Rýchla rýchlosť'; + String get settings_clientRepeatSubtitle => + 'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 36445f77..e01151e3 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -343,15 +343,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_presets => 'Prednastavitve'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frekvenca (MHz)'; @@ -380,10 +371,15 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_txPowerInvalid => 'Neveljavna TX moč (0-22 dBm)'; @override - String get settings_longRange => 'DDolg doseg'; + String get settings_clientRepeat => 'Neovadno ponavljanje'; @override - String get settings_fastSpeed => 'Visoka hitrost'; + String get settings_clientRepeatSubtitle => + 'Omogočite temu naprave, da ponavlja paketne sporočila za druge.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index cbfa45d8..f0817114 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -341,15 +341,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_presets => 'Fördefinierade inställningar'; - @override - String get settings_preset915Mhz => '915 MHz'; - - @override - String get settings_preset868Mhz => '868 MHz'; - - @override - String get settings_preset433Mhz => '433 MHz'; - @override String get settings_frequency => 'Frekvens (MHz)'; @@ -378,10 +369,15 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)'; @override - String get settings_longRange => 'Lång räckvidd'; + String get settings_clientRepeat => 'Upprepa utan elnät'; @override - String get settings_fastSpeed => 'Snabb hastighet'; + String get settings_clientRepeatSubtitle => + 'Låt enheten repetera nätpaket för andra användare.'; + + @override + String get settings_clientRepeatFreqWarning => + 'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4dfa2606..847c3e5d 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -346,15 +346,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_presets => 'Попередні налаштування'; - @override - String get settings_preset915Mhz => '915 МГц'; - - @override - String get settings_preset868Mhz => '868 МГц'; - - @override - String get settings_preset433Mhz => '433 МГц'; - @override String get settings_frequency => 'Частота (МГц)'; @@ -383,10 +374,15 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_txPowerInvalid => 'Некоректна потужність TX (0-22 дБм)'; @override - String get settings_longRange => 'Дальній діапазон'; + String get settings_clientRepeat => 'Автономна система'; @override - String get settings_fastSpeed => 'Висока швидкість'; + String get settings_clientRepeatSubtitle => + 'Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.'; + + @override + String get settings_clientRepeatFreqWarning => + 'Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.'; @override String settings_error(String message) { diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4441b222..fdc45312 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -331,15 +331,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_presets => '预设'; - @override - String get settings_preset915Mhz => '915 兆赫'; - - @override - String get settings_preset868Mhz => '868 兆赫'; - - @override - String get settings_preset433Mhz => '433 兆赫'; - @override String get settings_frequency => '频率 (MHz)'; @@ -368,10 +359,14 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; @override - String get settings_longRange => '远距离'; + String get settings_clientRepeat => '离网重复'; @override - String get settings_fastSpeed => '高速'; + String get settings_clientRepeatSubtitle => '允许此设备重复发送网状数据包给其他设备'; + + @override + String get settings_clientRepeatFreqWarning => + '离网重复通信需要使用 433、869 或 918 兆赫兹的频率。'; @override String settings_error(String message) { diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b150d62b..7c397b46 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Aantal Contacten", "settings_infoChannelCount": "Aantal Kanalen", "settings_presets": "Presets", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequentie (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Vermogen (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)", - "settings_longRange": "Lange Afstand", - "settings_fastSpeed": "Hoge Snelheid", "settings_error": "Fout: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_runTrace": "Padeshulp traceren", "scanner_enableBluetooth": "Activeer Bluetooth", "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", - "scanner_bluetoothOff": "Bluetooth is uitgeschakeld" + "scanner_bluetoothOff": "Bluetooth is uitgeschakeld", + "settings_clientRepeat": "Herhalen: Afgekoppeld", + "settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.", + "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d576fca8..5ebeebff 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Liczba kontaktów", "settings_infoChannelCount": "Liczba kanałów", "settings_presets": "Preset", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Częstotliwość (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Moc (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)", - "settings_longRange": "Długi zasięg", - "settings_fastSpeed": "Szybka prędkość", "settings_error": "Błąd: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.", "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", "scanner_bluetoothOff": "Bluetooth jest wyłączony", - "scanner_enableBluetooth": "Włącz Bluetooth" + "scanner_enableBluetooth": "Włącz Bluetooth", + "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", + "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", + "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 53c43fe7..a88e0388 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Número de Contatos", "settings_infoChannelCount": "Número do Canal", "settings_presets": "Presets", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frequência (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Potência (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)", - "settings_longRange": "Alcance Longo", - "settings_fastSpeed": "Velocidade Rápida", "settings_error": "Erro: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.", "scanner_enableBluetooth": "Ative o Bluetooth", "scanner_bluetoothOff": "Bluetooth está desativado", - "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos." + "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", + "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", + "settings_clientRepeat": "Repetição sem rede", + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 19b49907..fc17eee1 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -101,9 +101,6 @@ "settings_infoContactsCount": "Количество контактов", "settings_infoChannelCount": "Количество каналов", "settings_presets": "Пресеты", - "settings_preset915Mhz": "915 МГц", - "settings_preset868Mhz": "868 МГц", - "settings_preset433Mhz": "433 МГц", "settings_frequency": "Частота (МГц)", "settings_frequencyHelper": "300.0 – 2500.0", "settings_frequencyInvalid": "Недопустимая частота (300–2500 МГц)", @@ -113,8 +110,6 @@ "settings_txPower": "Мощность передачи (дБм)", "settings_txPowerHelper": "0 – 22", "settings_txPowerInvalid": "Недопустимая мощность передачи (0–22 дБм)", - "settings_longRange": "Дальний радиус", - "settings_fastSpeed": "Высокая скорость", "settings_error": "Ошибка: {message}", "appSettings_title": "Настройки приложения", "appSettings_appearance": "Внешний вид", @@ -838,5 +833,8 @@ "map_runTrace": "Запустить трассировку пути", "scanner_enableBluetooth": "Включите Bluetooth", "scanner_bluetoothOff": "Bluetooth выключен", - "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства." + "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", + "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", + "settings_clientRepeat": "Повторение \"вне сети\"" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 3f612922..14cd3eca 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Počet kontaktov", "settings_infoChannelCount": "Počet kanálov", "settings_presets": "Prednastavenia", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvencia (MHz)", "settings_frequencyHelper": "300,0 – 2500,0", "settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Výkon (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)", - "settings_longRange": "Dlhý dosah", - "settings_fastSpeed": "Rýchla rýchlosť", "settings_error": "Chyba: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.", "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", "scanner_bluetoothOff": "Bluetooth je vypnutý", - "scanner_enableBluetooth": "Povolte Bluetooth" + "scanner_enableBluetooth": "Povolte Bluetooth", + "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", + "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", + "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index d94695e3..e633965e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Število stikov", "settings_infoChannelCount": "Število kanalov", "settings_presets": "Prednastavitve", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvenca (MHz)", "settings_frequencyHelper": "300,00 - 2500,00", "settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX Moč (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)", - "settings_longRange": "DDolg doseg", - "settings_fastSpeed": "Visoka hitrost", "settings_error": "Napaka: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_pathTraceCancelled": "Spremljanje poti je prekinjeno.", "scanner_enableBluetooth": "Omogočite Bluetooth", "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", - "scanner_bluetoothOff": "Bluetooth je izklopljen" + "scanner_bluetoothOff": "Bluetooth je izklopljen", + "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", + "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", + "settings_clientRepeat": "Neovadno ponavljanje" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 59b3fca7..4e50409a 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Kontakterantal", "settings_infoChannelCount": "Kanalantal", "settings_presets": "Fördefinierade inställningar", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", "settings_frequency": "Frekvens (MHz)", "settings_frequencyHelper": "300,0 - 2500,0", "settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)", @@ -143,8 +140,6 @@ "settings_txPower": "TX-effekt (dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)", - "settings_longRange": "Lång räckvidd", - "settings_fastSpeed": "Snabb hastighet", "settings_error": "Fel: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_removeLast": "Ta bort sista", "scanner_enableBluetooth": "Aktivera Bluetooth", "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", - "scanner_bluetoothOff": "Bluetooth är avstängt" + "scanner_bluetoothOff": "Bluetooth är avstängt", + "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", + "settings_clientRepeat": "Upprepa utan elnät", + "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 26f3984e..afa11793 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -131,9 +131,6 @@ "settings_infoContactsCount": "Кількість контактів", "settings_infoChannelCount": "Кількість каналів", "settings_presets": "Попередні налаштування", - "settings_preset915Mhz": "915 МГц", - "settings_preset868Mhz": "868 МГц", - "settings_preset433Mhz": "433 МГц", "settings_frequency": "Частота (МГц)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "Некоректна частота (300-2500 МГц)", @@ -143,8 +140,6 @@ "settings_txPower": "Потужність TX (дБм)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "Некоректна потужність TX (0-22 дБм)", - "settings_longRange": "Дальній діапазон", - "settings_fastSpeed": "Висока швидкість", "settings_error": "Помилка: {message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_pathTraceCancelled": "Відмінується трасування шляху", "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", - "scanner_bluetoothOff": "Bluetooth вимкнено" + "scanner_bluetoothOff": "Bluetooth вимкнено", + "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", + "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", + "settings_clientRepeat": "Автономна система" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 7b4b3ab5..312ed1a1 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -136,9 +136,6 @@ "settings_infoContactsCount": "联系人数量", "settings_infoChannelCount": "通道数量", "settings_presets": "预设", - "settings_preset915Mhz": "915 兆赫", - "settings_preset868Mhz": "868 兆赫", - "settings_preset433Mhz": "433 兆赫", "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyInvalid": "无效频率(300-2500 MHz)", @@ -148,8 +145,6 @@ "settings_txPower": "TX 功率(dBm)", "settings_txPowerHelper": "0 - 22", "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", - "settings_longRange": "远距离", - "settings_fastSpeed": "高速", "settings_error": "[保存:{message}]\n错误:{message}", "@settings_error": { "placeholders": { @@ -1598,5 +1593,8 @@ "map_runTrace": "运行路径跟踪", "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", "scanner_bluetoothOff": "蓝牙已关闭", - "scanner_enableBluetooth": "启用蓝牙" + "scanner_enableBluetooth": "启用蓝牙", + "settings_clientRepeat": "离网重复", + "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", + "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。" } diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 20b7771f..51256982 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -59,46 +59,29 @@ class RadioSettings { required this.txPowerDbm, }); - // Preset configurations - static RadioSettings get preset915MHz => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); - - static RadioSettings get preset868MHz => RadioSettings( - frequencyMHz: 868.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 14, - ); - - static RadioSettings get preset433MHz => RadioSettings( - frequencyMHz: 433.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); - - static RadioSettings get presetLongRange => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw125, - spreadingFactor: LoRaSpreadingFactor.sf12, - codingRate: LoRaCodingRate.cr4_8, - txPowerDbm: 20, - ); - - static RadioSettings get presetFastSpeed => RadioSettings( - frequencyMHz: 915.0, - bandwidth: LoRaBandwidth.bw500, - spreadingFactor: LoRaSpreadingFactor.sf7, - codingRate: LoRaCodingRate.cr4_5, - txPowerDbm: 20, - ); + // Regional preset configurations + static final List<(String, RadioSettings)> presets = [ + ('Australia', RadioSettings(frequencyMHz: 915.8, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Australia (Narrow)', RadioSettings(frequencyMHz: 916.575, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Australia SA, WA, QLD', RadioSettings(frequencyMHz: 923.125, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Czech Republic', RadioSettings(frequencyMHz: 869.432, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('EU 433MHz', RadioSettings(frequencyMHz: 433.650, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('EU/UK (Long Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('EU/UK (Medium Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('EU/UK (Narrow)', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('New Zealand', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('New Zealand (Narrow)', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Portugal 433', RadioSettings(frequencyMHz: 433.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf9, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Portugal 869', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('Switzerland', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('USA Arizona', RadioSettings(frequencyMHz: 908.205, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('USA/Canada', RadioSettings(frequencyMHz: 910.525, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Vietnam', RadioSettings(frequencyMHz: 920.250, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + // Off-grid repeat presets (valid client_repeat frequencies) + ('Off-Grid 433', RadioSettings(frequencyMHz: 433.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ('Off-Grid 869', RadioSettings(frequencyMHz: 869.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), + ('Off-Grid 918', RadioSettings(frequencyMHz: 918.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ]; int get frequencyHz => (frequencyMHz * 1000).round(); int get bandwidthHz => bandwidth.hz; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 73376f05..f131ecbb 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -862,6 +862,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7; LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5; final _txPowerController = TextEditingController(text: '20'); + bool _clientRepeat = false; @override void initState() { @@ -911,6 +912,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { if (widget.connector.currentTxPower != null) { _txPowerController.text = widget.connector.currentTxPower.toString(); } + + _clientRepeat = widget.connector.clientRepeat ?? false; } @override @@ -960,9 +963,23 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { widget.connector.currentCr, ); + final supportsRepeat = + (widget.connector.firmwareVerCode ?? 0) >= 9; + + if (supportsRepeat) { + const validRepeatFreqsKHz = {433000, 869000, 918000}; + if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)), + ); + return; + } + } + try { await widget.connector.sendFrame( - buildSetRadioParamsFrame(freqHz, bwHz, sf, cr), + buildSetRadioParamsFrame(freqHz, bwHz, sf, cr, + clientRepeat: supportsRepeat ? _clientRepeat : null), ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.refreshDeviceInfo(); @@ -1001,37 +1018,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - l10n.settings_presets, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - children: [ - _PresetChip( - label: l10n.settings_preset915Mhz, - onTap: () => _applyPreset(RadioSettings.preset915MHz), - ), - _PresetChip( - label: l10n.settings_preset868Mhz, - onTap: () => _applyPreset(RadioSettings.preset868MHz), - ), - _PresetChip( - label: l10n.settings_preset433Mhz, - onTap: () => _applyPreset(RadioSettings.preset433MHz), - ), - _PresetChip( - label: l10n.settings_longRange, - onTap: () => _applyPreset(RadioSettings.presetLongRange), - ), - _PresetChip( - label: l10n.settings_fastSpeed, - onTap: () => _applyPreset(RadioSettings.presetFastSpeed), - ), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: l10n.settings_presets, + border: const OutlineInputBorder(), + ), + items: [ + for (var i = 0; i < RadioSettings.presets.length; i++) + DropdownMenuItem( + value: i, + child: Text(RadioSettings.presets[i].$1), + ), ], + onChanged: (index) { + if (index != null) { + _applyPreset(RadioSettings.presets[index].$2); + } + }, ), - const SizedBox(height: 24), + const SizedBox(height: 16), TextField( controller: _frequencyController, decoration: InputDecoration( @@ -1103,6 +1108,16 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ), keyboardType: TextInputType.number, ), + if ((widget.connector.firmwareVerCode ?? 0) >= 9) ...[ + const SizedBox(height: 16), + SwitchListTile( + title: Text(l10n.settings_clientRepeat), + subtitle: Text(l10n.settings_clientRepeatSubtitle), + value: _clientRepeat, + onChanged: (value) => setState(() => _clientRepeat = value), + contentPadding: EdgeInsets.zero, + ), + ], ], ), ), @@ -1117,14 +1132,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { } } -class _PresetChip extends StatelessWidget { - final String label; - final VoidCallback onTap; - - const _PresetChip({required this.label, required this.onTap}); - - @override - Widget build(BuildContext context) { - return ActionChip(label: Text(label), onPressed: onTap); - } -} From 5fae2e5f73b14153b126230ed73940fe7c44b42a Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 17 Feb 2026 23:50:11 -0700 Subject: [PATCH 27/99] fix formatting --- lib/connector/meshcore_protocol.dart | 9 +- lib/models/radio_settings.dart | 209 ++++++++++++++++++++++++--- lib/screens/settings_screen.dart | 13 +- 3 files changed, 205 insertions(+), 26 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0b78c65f..ee835781 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -557,8 +557,13 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) { // sf: spreading factor (5-12) // cr: coding rate (5-8) // clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older) -Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr, - {bool? clientRepeat}) { +Uint8List buildSetRadioParamsFrame( + int freqHz, + int bwHz, + int sf, + int cr, { + bool? clientRepeat, +}) { final writer = BufferWriter(); writer.writeByte(cmdSetRadioParams); writer.writeUInt32LE(freqHz); diff --git a/lib/models/radio_settings.dart b/lib/models/radio_settings.dart index 51256982..37ef3ccb 100644 --- a/lib/models/radio_settings.dart +++ b/lib/models/radio_settings.dart @@ -61,26 +61,197 @@ class RadioSettings { // Regional preset configurations static final List<(String, RadioSettings)> presets = [ - ('Australia', RadioSettings(frequencyMHz: 915.8, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Australia (Narrow)', RadioSettings(frequencyMHz: 916.575, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Australia SA, WA, QLD', RadioSettings(frequencyMHz: 923.125, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Czech Republic', RadioSettings(frequencyMHz: 869.432, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('EU 433MHz', RadioSettings(frequencyMHz: 433.650, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('EU/UK (Long Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('EU/UK (Medium Range)', RadioSettings(frequencyMHz: 869.525, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('EU/UK (Narrow)', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('New Zealand', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('New Zealand (Narrow)', RadioSettings(frequencyMHz: 917.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Portugal 433', RadioSettings(frequencyMHz: 433.375, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf9, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Portugal 869', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('Switzerland', RadioSettings(frequencyMHz: 869.618, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf8, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('USA Arizona', RadioSettings(frequencyMHz: 908.205, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf10, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('USA/Canada', RadioSettings(frequencyMHz: 910.525, bandwidth: LoRaBandwidth.bw62_5, spreadingFactor: LoRaSpreadingFactor.sf7, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Vietnam', RadioSettings(frequencyMHz: 920.250, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ( + 'Australia', + RadioSettings( + frequencyMHz: 915.8, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Australia (Narrow)', + RadioSettings( + frequencyMHz: 916.575, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Australia SA, WA, QLD', + RadioSettings( + frequencyMHz: 923.125, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Czech Republic', + RadioSettings( + frequencyMHz: 869.432, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'EU 433MHz', + RadioSettings( + frequencyMHz: 433.650, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'EU/UK (Long Range)', + RadioSettings( + frequencyMHz: 869.525, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'EU/UK (Medium Range)', + RadioSettings( + frequencyMHz: 869.525, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'EU/UK (Narrow)', + RadioSettings( + frequencyMHz: 869.618, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'New Zealand', + RadioSettings( + frequencyMHz: 917.375, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'New Zealand (Narrow)', + RadioSettings( + frequencyMHz: 917.375, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Portugal 433', + RadioSettings( + frequencyMHz: 433.375, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf9, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Portugal 869', + RadioSettings( + frequencyMHz: 869.618, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'Switzerland', + RadioSettings( + frequencyMHz: 869.618, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf8, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'USA Arizona', + RadioSettings( + frequencyMHz: 908.205, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf10, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'USA/Canada', + RadioSettings( + frequencyMHz: 910.525, + bandwidth: LoRaBandwidth.bw62_5, + spreadingFactor: LoRaSpreadingFactor.sf7, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Vietnam', + RadioSettings( + frequencyMHz: 920.250, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), // Off-grid repeat presets (valid client_repeat frequencies) - ('Off-Grid 433', RadioSettings(frequencyMHz: 433.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), - ('Off-Grid 869', RadioSettings(frequencyMHz: 869.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 14)), - ('Off-Grid 918', RadioSettings(frequencyMHz: 918.0, bandwidth: LoRaBandwidth.bw250, spreadingFactor: LoRaSpreadingFactor.sf11, codingRate: LoRaCodingRate.cr4_5, txPowerDbm: 20)), + ( + 'Off-Grid 433', + RadioSettings( + frequencyMHz: 433.0, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), + ( + 'Off-Grid 869', + RadioSettings( + frequencyMHz: 869.0, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 14, + ), + ), + ( + 'Off-Grid 918', + RadioSettings( + frequencyMHz: 918.0, + bandwidth: LoRaBandwidth.bw250, + spreadingFactor: LoRaSpreadingFactor.sf11, + codingRate: LoRaCodingRate.cr4_5, + txPowerDbm: 20, + ), + ), ]; int get frequencyHz => (frequencyMHz * 1000).round(); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index f131ecbb..12b79de9 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -963,8 +963,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { widget.connector.currentCr, ); - final supportsRepeat = - (widget.connector.firmwareVerCode ?? 0) >= 9; + final supportsRepeat = (widget.connector.firmwareVerCode ?? 0) >= 9; if (supportsRepeat) { const validRepeatFreqsKHz = {433000, 869000, 918000}; @@ -978,8 +977,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { try { await widget.connector.sendFrame( - buildSetRadioParamsFrame(freqHz, bwHz, sf, cr, - clientRepeat: supportsRepeat ? _clientRepeat : null), + buildSetRadioParamsFrame( + freqHz, + bwHz, + sf, + cr, + clientRepeat: supportsRepeat ? _clientRepeat : null, + ), ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); await widget.connector.refreshDeviceInfo(); @@ -1131,4 +1135,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ); } } - From 4239fb11edea2d26468be19d5efd99257e19ef7f Mon Sep 17 00:00:00 2001 From: zjs81 Date: Wed, 18 Feb 2026 00:07:08 -0700 Subject: [PATCH 28/99] Fix radio settings to only send repeat byte when the current state is known --- lib/screens/settings_screen.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 12b79de9..94d541b0 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -963,9 +963,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { widget.connector.currentCr, ); - final supportsRepeat = (widget.connector.firmwareVerCode ?? 0) >= 9; + // if the client repeat isnt null then we know its supported + //otherwise we leave it out of the frame to avoid accidentally enabling + final knownRepeat = widget.connector.clientRepeat != null; - if (supportsRepeat) { + if (knownRepeat) { const validRepeatFreqsKHz = {433000, 869000, 918000}; if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) { ScaffoldMessenger.of(context).showSnackBar( @@ -982,7 +984,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { bwHz, sf, cr, - clientRepeat: supportsRepeat ? _clientRepeat : null, + clientRepeat: knownRepeat ? _clientRepeat : null, ), ); await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower)); @@ -1112,7 +1114,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> { ), keyboardType: TextInputType.number, ), - if ((widget.connector.firmwareVerCode ?? 0) >= 9) ...[ + if (widget.connector.clientRepeat != null) ...[ const SizedBox(height: 16), SwitchListTile( title: Text(l10n.settings_clientRepeat), From 1dc90d0e89f40bf297162dbf609b7afbd9a95ce3 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 04:06:45 -0500 Subject: [PATCH 29/99] Add device protocol version tracking and error frame handling Port from meshcore-open: parse protocol version from byte 1 of device info frame, expose supportsFloodScope getter (version >= 8), handle respCodeErr frames with debug logging. Reset on disconnect. Co-authored-by: Cursor (cherry picked from commit a29bb9cdd7a02a85af26d94dd3c787cabd124629) --- lib/connector/meshcore_connector.dart | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 5cdab789..b24e5c28 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -105,6 +105,8 @@ class MeshCoreConnector extends ChangeNotifier { static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; int _maxChannels = _defaultMaxChannels; + int? _deviceProtocolVersion; + String? _activeFloodScopeTag; bool _isSyncingQueuedMessages = false; bool _queuedMessageSyncInFlight = false; bool _didInitialQueueSync = false; @@ -208,6 +210,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; + int? get deviceProtocolVersion => _deviceProtocolVersion; + bool get supportsFloodScope => (_deviceProtocolVersion ?? 0) >= 8; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -927,6 +931,8 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; + _deviceProtocolVersion = null; + _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _didInitialQueueSync = false; @@ -1753,11 +1759,23 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); + break; + case respCodeErr: + _handleErrorFrame(frame); + break; default: debugPrint('Unknown frame code: $code'); } } + void _handleErrorFrame(Uint8List frame) { + final errCode = frame.length > 1 ? frame[1] : -1; + _appDebugLogService?.warn( + 'Firmware responded with error code: $errCode', + tag: 'Protocol', + ); + } + void _handlePathUpdated(Uint8List frame) { // Frame format: [0]=code, [1-32]=pub_key if (frame.length >= 33 && _pathHistoryService != null) { @@ -1827,12 +1845,12 @@ class MeshCoreConnector extends ChangeNotifier { void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; _firmwareVerCode = frame[1]; + _deviceProtocolVersion = frame[1]; // Parse client_repeat from firmware v9+ (byte 80) if (frame.length >= 81) { _clientRepeat = frame[80] != 0; } - // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; @@ -3240,6 +3258,8 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; + _deviceProtocolVersion = null; + _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _isSyncingChannels = false; From 8a804a370607854eff08af35e4012b39e99ea6e7 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 12:30:00 -0500 Subject: [PATCH 30/99] Remove unused protocol placeholder field and unify version source --- lib/connector/meshcore_connector.dart | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index b24e5c28..1f423aae 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -105,8 +105,6 @@ class MeshCoreConnector extends ChangeNotifier { static const int _defaultMaxChannels = 8; int _maxContacts = _defaultMaxContacts; int _maxChannels = _defaultMaxChannels; - int? _deviceProtocolVersion; - String? _activeFloodScopeTag; bool _isSyncingQueuedMessages = false; bool _queuedMessageSyncInFlight = false; bool _didInitialQueueSync = false; @@ -210,8 +208,8 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; - int? get deviceProtocolVersion => _deviceProtocolVersion; - bool get supportsFloodScope => (_deviceProtocolVersion ?? 0) >= 8; + int? get deviceProtocolVersion => _firmwareVerCode; + bool get supportsFloodScope => (_firmwareVerCode ?? 0) >= 8; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -931,8 +929,6 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; - _deviceProtocolVersion = null; - _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _didInitialQueueSync = false; @@ -1845,7 +1841,6 @@ class MeshCoreConnector extends ChangeNotifier { void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; _firmwareVerCode = frame[1]; - _deviceProtocolVersion = frame[1]; // Parse client_repeat from firmware v9+ (byte 80) if (frame.length >= 81) { @@ -3258,8 +3253,6 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; - _deviceProtocolVersion = null; - _activeFloodScopeTag = null; _isSyncingQueuedMessages = false; _queuedMessageSyncInFlight = false; _isSyncingChannels = false; From dc193be8ed80997f9ba200ea1864c28e1164d24a Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 12:45:02 -0500 Subject: [PATCH 31/99] Trim protocol PR to explicit RESP_CODE_ERR handling only --- lib/connector/meshcore_connector.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1f423aae..de59f53a 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -208,8 +208,6 @@ class MeshCoreConnector extends ChangeNotifier { int? get batteryMillivolts => _batteryMillivolts; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; - int? get deviceProtocolVersion => _firmwareVerCode; - bool get supportsFloodScope => (_firmwareVerCode ?? 0) >= 8; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; bool get isSyncingChannels => _isSyncingChannels; int get channelSyncProgress => @@ -1756,6 +1754,7 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeCustomVars: _handleCustomVars(frame); break; + // RESP_CODE_ERR is a defined firmware response (code 1), not an unknown frame. case respCodeErr: _handleErrorFrame(frame); break; From 50ab46ed40e485228ae34be05318d7af53f9be04 Mon Sep 17 00:00:00 2001 From: Specter242 Date: Wed, 18 Feb 2026 12:45:41 -0500 Subject: [PATCH 32/99] Remove incidental whitespace-only diff from protocol PR --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index de59f53a..f74d5241 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1845,6 +1845,7 @@ class MeshCoreConnector extends ChangeNotifier { if (frame.length >= 81) { _clientRepeat = frame[80] != 0; } + // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; From 19edeab9d53c3479dc93d6bf6073725e4be56b9c Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 19 Feb 2026 11:17:58 -0800 Subject: [PATCH 33/99] add rbenv support --- .ruby-version | 1 + 1 file changed, 1 insertion(+) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..fcdb2e10 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0.0 From 4bf2519559d29d9af85fc90afe69e858f488c045 Mon Sep 17 00:00:00 2001 From: 446564 Date: Thu, 19 Feb 2026 11:46:57 -0800 Subject: [PATCH 34/99] clear app db of channel messages on delete we were only deleting channels and messages on device and the in app db would persist this caused weird messages to later show up in other channels as they were deleted and added due to the fact we store messages by channel index(slot #) --- lib/screens/channels_screen.dart | 22 ++++++++++++++++++++-- pubspec.lock | 20 ++++++++++---------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d4..12dc534f 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/storage/channel_message_store.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; @@ -104,6 +105,7 @@ class _ChannelsScreenState extends State @override Widget build(BuildContext context) { final connector = context.watch(); + final channelMessageStore = ChannelMessageStore(); // Auto-navigate back to scanner if disconnected if (!checkConnectionAndNavigate(connector)) { @@ -304,6 +306,7 @@ class _ChannelsScreenState extends State return _buildChannelTile( context, connector, + channelMessageStore, channel, showDragHandle: true, dragIndex: index, @@ -323,6 +326,7 @@ class _ChannelsScreenState extends State return _buildChannelTile( context, connector, + channelMessageStore, channel, ); }, @@ -352,6 +356,7 @@ class _ChannelsScreenState extends State Widget _buildChannelTile( BuildContext context, MeshCoreConnector connector, + ChannelMessageStore channelMessageStore, Channel channel, { bool showDragHandle = false, int? dragIndex, @@ -468,7 +473,12 @@ class _ChannelsScreenState extends State ); } }, - onLongPress: () => _showChannelActions(context, connector, channel), + onLongPress: () => _showChannelActions( + context, + connector, + channelMessageStore, + channel, + ), ), ); } @@ -476,6 +486,7 @@ class _ChannelsScreenState extends State void _showChannelActions( BuildContext context, MeshCoreConnector connector, + ChannelMessageStore channelMessageStore, Channel channel, ) { showModalBottomSheet( @@ -505,7 +516,12 @@ class _ChannelsScreenState extends State Navigator.pop(context); await Future.delayed(const Duration(milliseconds: 100)); if (context.mounted) { - _confirmDeleteChannel(context, connector, channel); + _confirmDeleteChannel( + context, + connector, + channelMessageStore, + channel, + ); } }, ), @@ -1451,6 +1467,7 @@ class _ChannelsScreenState extends State void _confirmDeleteChannel( BuildContext context, MeshCoreConnector connector, + ChannelMessageStore channelMessageStore, Channel channel, ) { showDialog( @@ -1469,6 +1486,7 @@ class _ChannelsScreenState extends State onPressed: () { Navigator.pop(dialogContext); connector.deleteChannel(channel.index); + channelMessageStore.clearChannelMessages(channel.index); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( diff --git a/pubspec.lock b/pubspec.lock index 09e93017..ed84c404 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -497,26 +497,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: From ba2763a3f61109f19e80a46f4760805b01128532 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Fri, 20 Feb 2026 01:28:13 -0500 Subject: [PATCH 35/99] fix(channels): make edit/delete actions use parent context after bottom sheet closes Root cause: edit/delete dialogs were opened from the sheet context after Navigator.pop, so context.mounted was false and follow-up actions never ran. Also keeps async await/error handling for channel edit/delete so failures surface to users instead of silently dropping. --- lib/screens/channels_screen.dart | 70 +++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d4..30c52f1a 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -478,9 +478,10 @@ class _ChannelsScreenState extends State MeshCoreConnector connector, Channel channel, ) { + final parentContext = context; showModalBottomSheet( - context: context, - builder: (context) => SafeArea( + context: parentContext, + builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -488,10 +489,10 @@ class _ChannelsScreenState extends State leading: const Icon(Icons.edit_outlined), title: Text(context.l10n.channels_editChannel), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await Future.delayed(const Duration(milliseconds: 100)); - if (context.mounted) { - _showEditChannelDialog(context, connector, channel); + if (parentContext.mounted) { + _showEditChannelDialog(parentContext, connector, channel); } }, ), @@ -502,10 +503,10 @@ class _ChannelsScreenState extends State style: const TextStyle(color: Colors.red), ), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); await Future.delayed(const Duration(milliseconds: 100)); - if (context.mounted) { - _confirmDeleteChannel(context, connector, channel); + if (parentContext.mounted) { + _confirmDeleteChannel(parentContext, connector, channel); } }, ), @@ -1415,7 +1416,7 @@ class _ChannelsScreenState extends State child: Text(dialogContext.l10n.common_cancel), ), FilledButton( - onPressed: () { + onPressed: () async { final name = nameController.text.trim(); final pskHex = pskController.text.trim(); @@ -1432,13 +1433,25 @@ class _ChannelsScreenState extends State } Navigator.pop(dialogContext); - connector.setChannel(channel.index, name, psk); - connector.setChannelSmazEnabled(channel.index, smazEnabled); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.channels_channelUpdated(name)), - ), - ); + try { + await connector.setChannel(channel.index, name, psk); + await connector.setChannelSmazEnabled( + channel.index, + smazEnabled, + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.channels_channelUpdated(name)), + ), + ); + } catch (e, st) { + debugPrint(st.toString()); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update channel: $e')), + ); + } }, child: Text(dialogContext.l10n.common_save), ), @@ -1466,16 +1479,25 @@ class _ChannelsScreenState extends State child: Text(dialogContext.l10n.common_cancel), ), TextButton( - onPressed: () { + onPressed: () async { Navigator.pop(dialogContext); - connector.deleteChannel(channel.index); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.channels_channelDeleted(channel.name), + try { + await connector.deleteChannel(channel.index); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context.l10n.channels_channelDeleted(channel.name), + ), ), - ), - ); + ); + } catch (e, st) { + debugPrint(st.toString()); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete channel: $e')), + ); + } }, child: Text( dialogContext.l10n.common_delete, From d2b693e5ce7021c27b7061c7408a4278e82064cb Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Fri, 20 Feb 2026 20:27:38 -0800 Subject: [PATCH 36/99] Add a signal readout for the nearest repeater. With improvements to app bar and other UI polish. (#200) * Refactor Cayenne LPP parsing with error handling and logging - Added error handling and logging to the Cayenne LPP parsing methods to manage malformed data gracefully. - Improved the structure of the parsing logic for better readability and maintainability. - Updated the Contact model to include error handling during frame parsing. - Refactored Channels, Contacts, Map, and Neighbours screens to utilize a new AppBarTitle widget for consistent app bar design. - Enhanced the BatteryIndicator widget to display SNR information for direct repeaters. - Introduced SNRUi class for better management of SNR icon and text representation. - Improved error handling in PathTraceMap and Neighbours screens to log errors appropriately. * Fix trace route bytes generation logic in Contact model * Ignore advertisements from self in MeshCoreConnector * Refactor PathTraceData to use List for snrData and adjust data mapping in PathTraceMapScreen * Add SNRIndicator to AppBar and refactor BatteryIndicator layout * Enhance path management dialog to display direct repeaters with color coding based on signal strength * Remove unused import from SNR indicator widget * Update lib/models/contact.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/connector/meshcore_connector.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/path_trace_map.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/widgets/battery_indicator.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/helpers/cayenne_lpp.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor packet handling to skip only the RSSI byte for improved reliability * Add SNR indicator localization and update UI references for nearby repeaters * Handle loading state and error parsing in PathTraceMapScreen; update SNR indicator dialog content layout * Throw an exception for unsupported LPP types in CayenneLpp class * Refactor AppBarTitle widget to remove unused style parameter; update related screens to reflect changes Improve SNR handling by adding validation for spreading factor range in snrUiFromSNR function Update contact handling in MeshCoreConnector to fix variable naming and improve readability Stop parsing unsupported LPP types in CayenneLpp to avoid misalignment * Sort direct repeaters by last updated time and SNR; limit to top three for improved path management dialog * Prevent notifications for chat and sensor adverts without a valid path * Implement ranking system for direct repeaters based on SNR and recency; update related UI components to reflect changes * Refactor localization keys for "neighbors" terminology across multiple languages - Updated localization keys from "neighbours" to "neighbors" in the following files: - app_localizations_bg.dart - app_localizations_de.dart - app_localizations_en.dart - app_localizations_es.dart - app_localizations_fr.dart - app_localizations_it.dart - app_localizations_nl.dart - app_localizations_pl.dart - app_localizations_pt.dart - app_localizations_ru.dart - app_localizations_sk.dart - app_localizations_sl.dart - app_localizations_sv.dart - app_localizations_uk.dart - app_localizations_zh.dart - Updated corresponding ARB files to reflect the changes in keys. - Renamed the NeighboursScreen to NeighborsScreen in the chat and repeater hub screens for consistency. * Adjust ranking calculation for direct repeaters by adding offset to SNR for improved accuracy * Fix typo in variable name for second direct repeater in path management dialog * Refactor ranking calculation for direct repeaters and update path handling in channel message screens * Refactor path handling in ChannelMessagePathScreen to improve logic for outgoing messages and channel messages * Fix AppBarTitle horizontal overflow with long titles (#187) * Initial plan * Wrap title Column in Expanded to prevent horizontal overflow in AppBarTitle Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com> * Refactor AppBarTitle widget to simplify Text widget initialization * Add "Show All Paths" feature to chat path management - Implemented localization for "Show All Paths" in multiple languages (DE, EN, ES, FR, IT, NL, PL, PT, RU, SK, SL, SV, UK, ZH). - Updated path management dialog to include a toggle for showing all paths. - Refactored path history display logic to conditionally show paths based on the toggle state. - Cleaned up unused print statements and improved code readability in path tracing and chat screens. * Refactor FeatureToggleRow visibility in chat and path management dialogs based on repeaters list * Remove unused import of 'dart:ffi' in path_trace_map.dart * Refactor repeater management logic and update UI state handling in chat and path management dialogs * Refactor RX data handling and improve repeater management logic in chat and path management dialogs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: wel97459 <12990640+wel97459@users.noreply.github.com> --- lib/connector/meshcore_connector.dart | 309 ++++++++++- lib/connector/meshcore_protocol.dart | 56 +- lib/helpers/cayenne_lpp.dart | 336 +++++------ lib/l10n/app_bg.arb | 9 +- lib/l10n/app_de.arb | 9 +- lib/l10n/app_en.arb | 17 +- lib/l10n/app_es.arb | 9 +- lib/l10n/app_fr.arb | 9 +- lib/l10n/app_it.arb | 9 +- lib/l10n/app_localizations.dart | 38 +- lib/l10n/app_localizations_bg.dart | 15 +- lib/l10n/app_localizations_de.dart | 15 +- lib/l10n/app_localizations_en.dart | 21 +- lib/l10n/app_localizations_es.dart | 15 +- lib/l10n/app_localizations_fr.dart | 16 +- lib/l10n/app_localizations_it.dart | 15 +- lib/l10n/app_localizations_nl.dart | 15 +- lib/l10n/app_localizations_pl.dart | 15 +- lib/l10n/app_localizations_pt.dart | 16 +- lib/l10n/app_localizations_ru.dart | 15 +- lib/l10n/app_localizations_sk.dart | 15 +- lib/l10n/app_localizations_sl.dart | 15 +- lib/l10n/app_localizations_sv.dart | 15 +- lib/l10n/app_localizations_uk.dart | 15 +- lib/l10n/app_localizations_zh.dart | 15 +- lib/l10n/app_nl.arb | 9 +- lib/l10n/app_pl.arb | 9 +- lib/l10n/app_pt.arb | 9 +- lib/l10n/app_ru.arb | 9 +- lib/l10n/app_sk.arb | 9 +- lib/l10n/app_sl.arb | 9 +- lib/l10n/app_sv.arb | 9 +- lib/l10n/app_uk.arb | 9 +- lib/l10n/app_zh.arb | 9 +- lib/models/contact.dart | 72 +-- lib/screens/channel_chat_screen.dart | 3 +- lib/screens/channel_message_path_screen.dart | 58 +- lib/screens/channels_screen.dart | 5 +- lib/screens/chat_screen.dart | 521 ++++++++++-------- lib/screens/contacts_screen.dart | 142 ++--- lib/screens/map_screen.dart | 15 +- ...ours_screen.dart => neighbors_screen.dart} | 170 +++--- lib/screens/path_trace_map.dart | 216 +++++--- lib/screens/repeater_hub_screen.dart | 12 +- lib/widgets/app_bar.dart | 48 ++ lib/widgets/battery_indicator.dart | 30 +- lib/widgets/path_management_dialog.dart | 242 +++++--- lib/widgets/snr_indicator.dart | 205 +++++-- pubspec.lock | 16 +- 49 files changed, 1956 insertions(+), 914 deletions(-) rename lib/screens/{neighbours_screen.dart => neighbors_screen.dart} (75%) create mode 100644 lib/widgets/app_bar.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index f74d5241..9b256e29 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -37,6 +37,42 @@ class MeshCoreUuids { static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; } +class DirectRepeater { + static const int maxAgeMinutes = 30; // Max age for direct repeater info + final int pubkeyFirstByte; + double snr; + DateTime lastUpdated; + + DirectRepeater({ + required this.pubkeyFirstByte, + required this.snr, + DateTime? lastUpdated, + }) : lastUpdated = lastUpdated ?? DateTime.now(); + + void update(double newSNR) { + snr = newSNR; + lastUpdated = DateTime.now(); + } + + int get ranking { + if (isStale()) { + return -1; // Stale repeaters get lowest rank + } + // Higher SNR gets higher rank and recency within maxAgeMinutes breaks ties. + final ageMs = + DateTime.now().millisecondsSinceEpoch - + lastUpdated.millisecondsSinceEpoch; + final maxAgeMs = maxAgeMinutes * 60 * 1000; + final recencyScore = (maxAgeMs - ageMs).clamp(0, maxAgeMs); + return ((snr - 31.75) * 1000).round() + recencyScore; + } + + bool isStale() { + return DateTime.now().difference(lastUpdated) > + const Duration(minutes: maxAgeMinutes); + } +} + enum MeshCoreConnectionState { disconnected, scanning, @@ -95,6 +131,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _batteryMillivolts; double? _selfLatitude; double? _selfLongitude; + final List _directRepeaters = List.empty(growable: true); bool _isLoadingContacts = false; bool _isLoadingChannels = false; bool _hasLoadedChannels = false; @@ -196,6 +233,7 @@ class MeshCoreConnector extends ChangeNotifier { String? get selfName => _selfName; double? get selfLatitude => _selfLatitude; double? get selfLongitude => _selfLongitude; + List get directRepeaters => _directRepeaters; int? get currentTxPower => _currentTxPower; int? get maxTxPower => _maxTxPower; int? get currentFreqHz => _currentFreqHz; @@ -1696,6 +1734,11 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; + case pushCodeNewAdvert: + debugPrint('Got New CONTACT'); + // It's the same format as respCodeContact, so we can reuse the handler + _handleContact(frame); + break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1740,6 +1783,7 @@ class MeshCoreConnector extends ChangeNotifier { case pushCodeStatusResponse: break; case pushCodeLogRxData: + _handleRxData(frame); _handleLogRxData(frame); break; case respCodeChannelInfo: @@ -2028,6 +2072,80 @@ class MeshCoreConnector extends ChangeNotifier { } } + void _handleContactAdvert(Contact contact) { + if (listEquals(contact.publicKey, _selfPublicKey)) { + return; + } + + if (contact.type == advTypeRepeater) { + _contactUnreadCount.remove(contact.publicKeyHex); + _unreadStore.saveContactUnreadCount( + Map.from(_contactUnreadCount), + ); + } + // Check if this is a new contact + final isNewContact = !_knownContactKeys.contains(contact.publicKeyHex); + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + + if (existingIndex >= 0) { + final existing = _contacts[existingIndex]; + final mergedLastMessageAt = + existing.lastMessageAt.isAfter(contact.lastMessageAt) + ? existing.lastMessageAt + : contact.lastMessageAt; + + appLogger.info( + 'Refreshing contact ${contact.name}: devicePath=${contact.pathLength}, existingOverride=${existing.pathOverride}', + tag: 'Connector', + ); + + // CRITICAL: Preserve user's path override when contact is refreshed from device + _contacts[existingIndex] = contact.copyWith( + lastMessageAt: mergedLastMessageAt, + pathOverride: existing.pathOverride, // Preserve user's path choice + pathOverrideBytes: existing.pathOverrideBytes, + ); + + appLogger.info( + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + tag: 'Connector', + ); + } else { + _contacts.add(contact); + appLogger.info( + 'Added new contact ${contact.name}: pathLen=${contact.pathLength}', + tag: 'Connector', + ); + } + _knownContactKeys.add(contact.publicKeyHex); + _loadMessagesForContact(contact.publicKeyHex); + + // Add path to history if we have a valid path + if (_pathHistoryService != null && contact.pathLength >= 0) { + _pathHistoryService!.handlePathUpdated(contact); + } + + notifyListeners(); + + // Show notification for new contact (advertisement) + if (isNewContact && _appSettingsService != null) { + final settings = _appSettingsService!.settings; + if (settings.notificationsEnabled && settings.notifyOnNewAdvert) { + _notificationService.showAdvertNotification( + contactName: contact.name, + contactType: contact.typeLabel, + contactId: contact.publicKeyHex, + ); + } + } + + if (!_isLoadingContacts) { + unawaited(_persistContacts()); + } + } + Future _persistContacts() async { await _contactStore.saveContacts(_contacts); } @@ -3287,7 +3405,11 @@ class MeshCoreConnector extends ChangeNotifier { void _handleCustomVars(Uint8List frame) { final buf = BufferReader(frame.sublist(1)); - _currentCustomVars = _parseKeyValueString(buf.readString()); + try { + _currentCustomVars = _parseKeyValueString(buf.readString()); + } catch (e) { + appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector'); + } } void _setState(MeshCoreConnectionState newState) { @@ -3311,6 +3433,191 @@ class MeshCoreConnector extends ChangeNotifier { super.dispose(); } + + void _handleRxData(Uint8List frame) { + final packet = BufferReader(frame); + double snr = 0.0; + int routeType = 0; + int payloadType = 0; + Uint8List pathBytes = Uint8List(0); + Uint8List payload = Uint8List(0); + try { + packet.skipBytes(1); // Skip frame type byte + snr = packet.readInt8() / 4.0; + packet.skipBytes(1); // Skip RSSI byte + //final rssi = packet.readByte(); + final header = packet.readByte(); + routeType = header & 0x03; + payloadType = (header >> 2) & 0x0F; + //final payloadVer = (header >> 6) & 0x03; + final pathLen = packet.readByte(); + pathBytes = packet.readBytes(pathLen); + payload = packet.readBytes(packet.remaining); + } catch (e) { + appLogger.warn('Malformed RX frame: $e', tag: 'Connector'); + return; + } + + switch (payloadType) { + case payloadTypeADVERT: + _handlePayloadAdvertReceived(payload, pathBytes, routeType, snr); + break; + default: + } + } + + void _handlePayloadAdvertReceived( + Uint8List frame, + Uint8List path, + int routeType, + double snr, + ) { + final advert = BufferReader(frame); + double latitude = 0.0; + double longitude = 0.0; + String name = ''; + String contactKeyHex = ''; + Uint8List publicKey = Uint8List(0); + int type = 0; + int timestamp = 0; + bool hasLocation = false; + bool hasName = false; + try { + publicKey = advert.readBytes(32); + contactKeyHex = publicKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); + + timestamp = advert.readInt32LE(); + //TODO add signature verification + advert.skipBytes(64); // Skip signature for now + final flags = advert.readByte(); + type = flags & 0x0F; + hasLocation = (flags & 0x10) != 0; + // For future use: + //final hasFeature1 = (flags & 0x20) != 0; + //final hasFeature2 = (flags & 0x40) != 0; + hasName = (flags & 0x80) != 0; + if (hasLocation && advert.remaining >= 8) { + latitude = advert.readInt32LE() / 1e6; + longitude = advert.readInt32LE() / 1e6; + } + if (hasName && advert.remaining > 0) { + name = advert.readString(); + } + } catch (e) { + appLogger.warn('Malformed advert frame: $e', tag: 'Connector'); + return; + } + + if (listEquals(publicKey, _selfPublicKey)) { + return; + } + + // Check if this is a new contact + final isNewContact = !_knownContactKeys.contains(contactKeyHex); + + if (isNewContact) { + final newContact = Contact( + publicKey: publicKey, + name: name, + type: type, + pathLength: path.length, + path: Uint8List.fromList( + path.reversed.toList(), + ), // Store path in reverse for easier use in outgoing messages + latitude: latitude, + longitude: longitude, + lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + ); + _handleContactAdvert(newContact); + _updateDirectRepeater(newContact, snr, path); + return; + } + + final existingIndex = _contacts.indexWhere( + (c) => c.publicKeyHex == contactKeyHex, + ); + + if (existingIndex >= 0) { + final existing = _contacts[existingIndex]; + final mergedLastMessageAt = existing.lastMessageAt.isAfter(DateTime.now()) + ? DateTime.now() + : existing.lastMessageAt; + + appLogger.info( + 'Refreshing contact ${existing.name}: devicePath=${existing.pathLength}, existingOverride=${existing.pathOverride}', + tag: 'Connector', + ); + + // CRITICAL: Preserve user's path override when contact is refreshed from device + _contacts[existingIndex] = existing.copyWith( + latitude: hasLocation ? latitude : existing.latitude, + longitude: hasLocation ? longitude : existing.longitude, + name: hasName ? name : existing.name, + path: Uint8List.fromList(path.reversed.toList()), + pathLength: path.length, + lastMessageAt: mergedLastMessageAt, + lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + pathOverride: existing.pathOverride, // Preserve user's path choice + pathOverrideBytes: existing.pathOverrideBytes, + ); + + // Add path to history if we have a valid path + if (_pathHistoryService != null && + _contacts[existingIndex].pathLength >= 0) { + _pathHistoryService!.handlePathUpdated(_contacts[existingIndex]); + } + + _updateDirectRepeater(_contacts[existingIndex], snr, path); + + appLogger.info( + 'After merge: pathOverride=${_contacts[existingIndex].pathOverride}, devicePath=${_contacts[existingIndex].pathLength}', + tag: 'Connector', + ); + } + } + + void _updateDirectRepeater(Contact contact, double snr, Uint8List path) { + final pubkeyFirstByte = path.isNotEmpty + ? path.last + : contact.publicKey.first; + + _directRepeaters.removeWhere((r) => r.isStale()); + + //We can use adverts from chat and sensor nodes, but only if the advert has a path to get the last hop. + if ((contact.type == advTypeChat || contact.type == advTypeSensor) && + path.isEmpty) { + notifyListeners(); + return; + } + + final isTracked = _directRepeaters.where( + (r) => r.pubkeyFirstByte == pubkeyFirstByte, + ); + + final sortedRepeaters = List.from(_directRepeaters) + ..sort((a, b) => b.snr.compareTo(a.snr)); + final weakestRepeater = sortedRepeaters.isNotEmpty + ? sortedRepeaters.last + : null; + + if (_directRepeaters.length >= 5 && + weakestRepeater != null && + isTracked.isEmpty) { + _directRepeaters.remove(weakestRepeater); + } + + if (isTracked.isNotEmpty) { + final repeater = isTracked.first; + repeater.update(snr); + } else if (_directRepeaters.length < 5) { + _directRepeaters.add( + DirectRepeater(pubkeyFirstByte: pubkeyFirstByte, snr: snr), + ); + } + notifyListeners(); + } } const int _phRouteMask = 0x03; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index ee835781..2933e802 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -13,12 +13,22 @@ class BufferReader { int readByte() => readBytes(1)[0]; Uint8List readBytes(int count) { + if (_pointer + count > _buffer.length) { + throw RangeError( + 'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}', + ); + } final data = _buffer.sublist(_pointer, _pointer + count); _pointer += count; return data; } void skipBytes(int count) { + if (_pointer + count > _buffer.length) { + throw RangeError( + 'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}', + ); + } _pointer += count; } @@ -151,6 +161,7 @@ const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; +const int cmdSetOtherParams = 38; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -166,7 +177,7 @@ const int reqTypeGetStatus = 0x01; const int reqTypeKeepAlive = 0x02; const int reqTypeGetTelemetry = 0x03; const int reqTypeGetAccessList = 0x05; -const int reqTypeGetNeighbours = 0x06; +const int reqTypeGetNeighbors = 0x06; // Repeater response codes const int respServerLoginOk = 0; @@ -212,6 +223,30 @@ const int advTypeRepeater = 2; const int advTypeRoom = 3; const int advTypeSensor = 4; +// Payload Types +const int payloadTypeREQ = + 0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) +const int payloadTypeRESPONSE = + 0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob) +const int payloadTypeTXTMSG = + 0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text) +const int payloadTypeACK = 0x03; // a simple ack +const int payloadTypeADVERT = 0x04; // a node advertising its Identity +const int payloadTypeGRPTXT = + 0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg") +const int payloadTypeGRPDATA = + 0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob) +const int payloadTypeANONREQ = + 0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...) +const int payloadTypePATH = + 0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra) +const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop +const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets +const int payloadTypeCONTROL = 0x0B; // a control/discovery packet +//... +const int payloadTypeRawCustom = + 0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc + // Sizes const int pubKeySize = 32; const int maxPathSize = 64; @@ -788,3 +823,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { writer.writeBytes(pubKey); return writer.toBytes(); } + +// Build CMD_SET_OTHER_PARAMS frame +// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks] +Uint8List buildSetOtherParamsFrame( + bool allowAutoAddContacts, + int allowTelemetryFlags, + int advertLocationPolicy, + int multiAcks, +) { + final writer = BufferWriter(); + writer.writeByte(cmdSetOtherParams); + writer.writeByte( + allowAutoAddContacts ? 0x00 : 0x01, + ); // Allow Auto Add Contacts + writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags + writer.writeByte(advertLocationPolicy); // Advertisement Location Policy + writer.writeByte(multiAcks); // Multi Acknowledgements + return writer.toBytes(); +} diff --git a/lib/helpers/cayenne_lpp.dart b/lib/helpers/cayenne_lpp.dart index bf9b8e77..07909e63 100644 --- a/lib/helpers/cayenne_lpp.dart +++ b/lib/helpers/cayenne_lpp.dart @@ -1,4 +1,6 @@ import 'dart:typed_data'; +import 'package:meshcore_open/utils/app_logger.dart'; + import '../connector/meshcore_protocol.dart'; class CayenneLpp { @@ -84,180 +86,192 @@ class CayenneLpp { static List> parse(Uint8List bytes) { final buffer = BufferReader(bytes); final telemetry = >[]; + try { + while (buffer.remaining >= 2) { + final channel = buffer.readUInt8(); + final type = buffer.readUInt8(); - while (buffer.remaining >= 2) { - final channel = buffer.readUInt8(); - final type = buffer.readUInt8(); + if (channel == 0 && type == 0) { + break; + } - if (channel == 0 && type == 0) { - break; - } - - switch (type) { - case lppGenericSensor: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt32BE(), - }); - break; - case lppLuminosity: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE(), - }); - break; - case lppPresence: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt8(), - }); - break; - case lppTemperature: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readInt16BE() / 10, - }); - break; - case lppRelativeHumidity: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt8() / 2, - }); - break; - case lppBarometricPressure: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE() / 10, - }); - break; - case lppVoltage: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readInt16BE() / 100, - }); - break; - case lppCurrent: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readInt16BE() / 1000, - }); - break; - case lppPercentage: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt8(), - }); - break; - case lppConcentration: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE(), - }); - break; - case lppPower: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': buffer.readUInt16BE(), - }); - break; - case lppGps: - telemetry.add({ - 'channel': channel, - 'type': type, - 'value': { - 'latitude': buffer.readInt24BE() / 10000, - 'longitude': buffer.readInt24BE() / 10000, - 'altitude': buffer.readInt24BE() / 100, - }, - }); - break; - default: - return telemetry; + switch (type) { + case lppGenericSensor: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt32BE(), + }); + break; + case lppLuminosity: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE(), + }); + break; + case lppPresence: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt8(), + }); + break; + case lppTemperature: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readInt16BE() / 10, + }); + break; + case lppRelativeHumidity: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt8() / 2, + }); + break; + case lppBarometricPressure: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE() / 10, + }); + break; + case lppVoltage: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readInt16BE() / 100, + }); + break; + case lppCurrent: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readInt16BE() / 1000, + }); + break; + case lppPercentage: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt8(), + }); + break; + case lppConcentration: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE(), + }); + break; + case lppPower: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': buffer.readUInt16BE(), + }); + break; + case lppGps: + telemetry.add({ + 'channel': channel, + 'type': type, + 'value': { + 'latitude': buffer.readInt24BE() / 10000, + 'longitude': buffer.readInt24BE() / 10000, + 'altitude': buffer.readInt24BE() / 100, + }, + }); + break; + default: + return telemetry; + } } + return telemetry; + } catch (e) { + // Handle parsing errors, possibly due to malformed data + appLogger.error('Error parsing Cayenne LPP data: $e'); + // Return any telemetry parsed so far to preserve partial data + return telemetry; } - - return telemetry; } static List> parseByChannel(Uint8List bytes) { final buffer = BufferReader(bytes); final Map> channels = {}; + try { + while (buffer.remaining >= 2) { + final channel = buffer.readUInt8(); + final type = buffer.readUInt8(); - while (buffer.remaining >= 2) { - final channel = buffer.readUInt8(); - final type = buffer.readUInt8(); + // Optional: stop on padding (00 00) + if (channel == 0 && type == 0) { + break; + } - // Optional: stop on padding (00 00) - if (channel == 0 && type == 0) { - break; + final channelData = channels.putIfAbsent( + channel, + () => {'channel': channel, 'values': {}}, + ); + + switch (type) { + case lppGenericSensor: + channelData['values']['generic'] = buffer.readUInt32BE(); + break; + case lppLuminosity: + channelData['values']['luminosity'] = buffer.readUInt16BE(); + break; + case lppPresence: + channelData['values']['presence'] = buffer.readUInt8() != 0; + break; + case lppTemperature: + channelData['values']['temperature'] = buffer.readInt16BE() / 10.0; + break; + case lppRelativeHumidity: + channelData['values']['humidity'] = buffer.readUInt8() / 2.0; + break; + case lppBarometricPressure: + channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0; + break; + case lppVoltage: + channelData['values']['voltage'] = buffer.readInt16BE() / 100.0; + break; + case lppCurrent: + channelData['values']['current'] = buffer.readInt16BE() / 1000.0; + break; + case lppPercentage: + channelData['values']['percentage'] = buffer.readUInt8(); + break; + case lppConcentration: + channelData['values']['concentration'] = buffer.readUInt16BE(); + break; + case lppPower: + channelData['values']['power'] = buffer.readUInt16BE(); + break; + case lppGps: + channelData['values']['gps'] = { + 'latitude': buffer.readInt24BE() / 10000.0, + 'longitude': buffer.readInt24BE() / 10000.0, + 'altitude': buffer.readInt24BE() / 100.0, + }; + break; + // Add more types as needed... + default: + //Stopped parsing to avoid misalignment + return channels.values.toList(); + } } - final channelData = channels.putIfAbsent( - channel, - () => {'channel': channel, 'values': {}}, - ); - - switch (type) { - case lppGenericSensor: - channelData['values']['generic'] = buffer.readUInt32BE(); - break; - case lppLuminosity: - channelData['values']['luminosity'] = buffer.readUInt16BE(); - break; - case lppPresence: - channelData['values']['presence'] = buffer.readUInt8() != 0; - break; - case lppTemperature: - channelData['values']['temperature'] = buffer.readInt16BE() / 10.0; - break; - case lppRelativeHumidity: - channelData['values']['humidity'] = buffer.readUInt8() / 2.0; - break; - case lppBarometricPressure: - channelData['values']['pressure'] = buffer.readUInt16BE() / 10.0; - break; - case lppVoltage: - channelData['values']['voltage'] = buffer.readInt16BE() / 100.0; - break; - case lppCurrent: - channelData['values']['current'] = buffer.readInt16BE() / 1000.0; - break; - case lppPercentage: - channelData['values']['percentage'] = buffer.readUInt8(); - break; - case lppConcentration: - channelData['values']['concentration'] = buffer.readUInt16BE(); - break; - case lppPower: - channelData['values']['power'] = buffer.readUInt16BE(); - break; - case lppGps: - channelData['values']['gps'] = { - 'latitude': buffer.readInt24BE() / 10000.0, - 'longitude': buffer.readInt24BE() / 10000.0, - 'altitude': buffer.readInt24BE() / 100.0, - }; - break; - // Add more types as needed... - default: - // Unknown type: skip or handle error? - continue; - } + final List> channelsOut = channels.values.toList(); + channelsOut.sort((a, b) => a['channel'].compareTo(b['channel'])); + return channelsOut; + } catch (e) { + // Handle parsing errors, possibly due to malformed data + appLogger.error('Error parsing Cayenne LPP data: $e'); + return < + Map + >[]; // Return an empty list on error to avoid crashing the app } - - final List> channelsOut = channels.values.toList(); - channelsOut.sort((a, b) => a['channel'].compareTo(b['channel'])); - return channelsOut; } } diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 567c74c6..b6f4301e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.", - "repeater_neighbours": "Съседи", + "repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.", + "repeater_neighbors": "Съседи", "neighbors_receivedData": "Получени данни за съседи", "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", - "neighbors_repeatersNeighbours": "Повторители Съседи", + "neighbors_repeatersNeighbors": "Повторители Съседи", "neighbors_noData": "Няма налични данни за съседи.", "channels_createPrivateChannel": "Създай Частен Канал", "channels_joinPrivateChannel": "Присъедини се към Частен Канал", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOff": "Bluetooth е изключен.", "scanner_enableBluetooth": "Активирайте Bluetooth", "scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.", + "snrIndicator_lastSeen": "Последно видян", + "snrIndicator_nearByRepeaters": "Близки повтарящи се устройства", + "chat_ShowAllPaths": "Покажи всички пътища", "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", "settings_clientRepeat": "Без електричество – повторение" diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7aba89b1..077c3989 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Nachbarn", - "repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", + "repeater_neighbors": "Nachbarn", + "repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", "neighbors_receivedData": "Empfangene Nachbarsdaten", "neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", - "neighbors_repeatersNeighbours": "Nachbarn", + "neighbors_repeatersNeighbors": "Nachbarn", "neighbors_noData": "Keine Nachbarsdaten verfügbar.", "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", @@ -1622,6 +1622,9 @@ "scanner_bluetoothOffMessage": "Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.", "scanner_bluetoothOff": "Bluetooth ist deaktiviert.", "scanner_enableBluetooth": "Bluetooth aktivieren", + "snrIndicator_lastSeen": "Zuletzt gesehen", + "snrIndicator_nearByRepeaters": "In der Nähe befindliche Repeater", + "chat_ShowAllPaths": "Alle Pfade anzeigen", "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen." diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index cfd63300..bf49d7ef 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -558,6 +558,7 @@ }, "debugFrame_hexDump": "Hex Dump:", "chat_pathManagement": "Path Management", + "chat_ShowAllPaths": "Show all paths", "chat_routingMode": "Routing mode", "chat_autoUseSavedPath": "Auto (use saved path)", "chat_forceFloodMode": "Force Flood Mode", @@ -905,8 +906,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_neighbors": "Neighbors", + "repeater_neighborsSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", "repeater_statusTitle": "Repeater Status", @@ -1266,8 +1267,8 @@ } } }, - "neighbors_receivedData": "Received Neighbours Data", - "neighbors_requestTimedOut": "Neighbours request timed out.", + "neighbors_receivedData": "Received Neighbors Data", + "neighbors_requestTimedOut": "Neighbors request timed out.", "neighbors_errorLoading": "Error loading neighbors: {error}", "@neighbors_errorLoading": { "placeholders": { @@ -1276,8 +1277,8 @@ } } }, - "neighbors_repeatersNeighbours": "Repeaters Neighbours", - "neighbors_noData": "No neighbours data available.", + "neighbors_repeatersNeighbors": "Repeaters Neighbors", + "neighbors_noData": "No neighbors data available.", "neighbors_unknownContact": "Unknown {pubkey}", "@neighbors_unknownContact": { "placeholders": { @@ -1624,5 +1625,7 @@ "settings_gpxExportChat": "Companion locations", "settings_gpxExportAllContacts": "All contacts locations", "settings_gpxExportShareText": "Map data exported from meshcore-open", - "settings_gpxExportShareSubject": "meshcore-open GPX map data export" + "settings_gpxExportShareSubject": "meshcore-open GPX map data export", + "snrIndicator_nearByRepeaters": "Nearby Repeaters", + "snrIndicator_lastSeen": "Last seen" } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4250a6f6..1896b4f3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Vecinos", - "repeater_neighboursSubtitle": "Ver vecinos de salto cero.", + "repeater_neighbors": "Vecinos", + "repeater_neighborsSubtitle": "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_repeatersNeighbors": "Repetidores Vecinos", "neighbors_noData": "No hay datos de vecinos disponibles.", "channels_joinPrivateChannel": "Únete a un Canal Privado", "channels_createPrivateChannel": "Crear un Canal Privado", @@ -1622,6 +1622,9 @@ "scanner_bluetoothOffMessage": "Por favor, active el Bluetooth para escanear dispositivos.", "scanner_bluetoothOff": "Bluetooth está desactivado.", "scanner_enableBluetooth": "Habilitar Bluetooth", + "snrIndicator_nearByRepeaters": "Repetidores cercanos", + "snrIndicator_lastSeen": "Visto por última vez", + "chat_ShowAllPaths": "Mostrar todos los caminos", "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", "settings_clientRepeat": "Repetir sin conexión", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios." diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 56d4a410..d1befced 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Voisins", - "repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.", + "repeater_neighbors": "Voisins", + "repeater_neighborsSubtitle": "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_repeatersNeighbors": "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é", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "Veuillez activer le Bluetooth pour rechercher des appareils.", "scanner_bluetoothOff": "Le Bluetooth est désactivé.", "scanner_enableBluetooth": "Activer le Bluetooth", + "snrIndicator_lastSeen": "Dernière fois vu", + "snrIndicator_nearByRepeaters": "Répéteurs à proximité", + "chat_ShowAllPaths": "Afficher tous les chemins", "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", "settings_clientRepeat": "Répétition hors réseau" diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 239c7659..22371baf 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Vicini", - "repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.", + "repeater_neighbors": "Vicini", + "repeater_neighborsSubtitle": "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_repeatersNeighbors": "Ripetitori Vicini", "neighbors_noData": "Nessun dato sugli vicini disponibile.", "channels_createPrivateChannel": "Crea un Canale Privato", "channels_createPrivateChannelDesc": "Protetta con una chiave segreta.", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOff": "Il Bluetooth è disattivato.", "scanner_bluetoothOffMessage": "Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.", "scanner_enableBluetooth": "Abilita il Bluetooth", + "snrIndicator_nearByRepeaters": "Ripetitori vicini", + "snrIndicator_lastSeen": "Ultimo accesso", + "chat_ShowAllPaths": "Mostra tutti i percorsi", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri." diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7235e90b..2bcda786 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2032,6 +2032,12 @@ abstract class AppLocalizations { /// **'Path Management'** String get chat_pathManagement; + /// No description provided for @chat_ShowAllPaths. + /// + /// In en, this message translates to: + /// **'Show all paths'** + String get chat_ShowAllPaths; + /// No description provided for @chat_routingMode. /// /// In en, this message translates to: @@ -3027,17 +3033,17 @@ abstract class AppLocalizations { /// **'Send commands to the repeater'** String get repeater_cliSubtitle; - /// No description provided for @repeater_neighbours. + /// No description provided for @repeater_neighbors. /// /// In en, this message translates to: /// **'Neighbors'** - String get repeater_neighbours; + String get repeater_neighbors; - /// No description provided for @repeater_neighboursSubtitle. + /// No description provided for @repeater_neighborsSubtitle. /// /// In en, this message translates to: /// **'View zero hop neighbors.'** - String get repeater_neighboursSubtitle; + String get repeater_neighborsSubtitle; /// No description provided for @repeater_settings. /// @@ -4181,13 +4187,13 @@ abstract class AppLocalizations { /// No description provided for @neighbors_receivedData. /// /// In en, this message translates to: - /// **'Received Neighbours Data'** + /// **'Received Neighbors Data'** String get neighbors_receivedData; /// No description provided for @neighbors_requestTimedOut. /// /// In en, this message translates to: - /// **'Neighbours request timed out.'** + /// **'Neighbors request timed out.'** String get neighbors_requestTimedOut; /// No description provided for @neighbors_errorLoading. @@ -4196,16 +4202,16 @@ abstract class AppLocalizations { /// **'Error loading neighbors: {error}'** String neighbors_errorLoading(String error); - /// No description provided for @neighbors_repeatersNeighbours. + /// No description provided for @neighbors_repeatersNeighbors. /// /// In en, this message translates to: - /// **'Repeaters Neighbours'** - String get neighbors_repeatersNeighbours; + /// **'Repeaters Neighbors'** + String get neighbors_repeatersNeighbors; /// No description provided for @neighbors_noData. /// /// In en, this message translates to: - /// **'No neighbours data available.'** + /// **'No neighbors data available.'** String get neighbors_noData; /// No description provided for @neighbors_unknownContact. @@ -5023,6 +5029,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'meshcore-open GPX map data export'** String get settings_gpxExportShareSubject; + + /// No description provided for @snrIndicator_nearByRepeaters. + /// + /// In en, this message translates to: + /// **'Nearby Repeaters'** + String get snrIndicator_nearByRepeaters; + + /// No description provided for @snrIndicator_lastSeen. + /// + /// In en, this message translates to: + /// **'Last seen'** + String get snrIndicator_lastSeen; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index fdf9ec7e..137d48a5 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1080,6 +1080,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get chat_pathManagement => 'Управление на пътища'; + @override + String get chat_ShowAllPaths => 'Покажи всички пътища'; + @override String get chat_routingMode => 'Режим на маршрутизиране'; @@ -1677,10 +1680,10 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; @override - String get repeater_neighbours => 'Съседи'; + String get repeater_neighbors => 'Съседи'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Преглед на съседни възли с нулев скок.'; @override @@ -2380,7 +2383,7 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Повторители Съседи'; + String get neighbors_repeatersNeighbors => 'Повторители Съседи'; @override String get neighbors_noData => 'Няма налични данни за съседи.'; @@ -2890,4 +2893,10 @@ class AppLocalizationsBg extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open износ на данни за карта в формат GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Близки повтарящи се устройства'; + + @override + String get snrIndicator_lastSeen => 'Последно видян'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c0fc4c86..927ac488 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1080,6 +1080,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get chat_pathManagement => 'Pfadverwaltung'; + @override + String get chat_ShowAllPaths => 'Alle Pfade anzeigen'; + @override String get chat_routingMode => 'Routenmodus'; @@ -1676,10 +1679,10 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliSubtitle => 'Sende Befehle an den Repeater'; @override - String get repeater_neighbours => 'Nachbarn'; + String get repeater_neighbors => 'Nachbarn'; @override - String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.'; + String get repeater_neighborsSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.'; @override String get repeater_settings => 'Einstellungen'; @@ -2382,7 +2385,7 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Nachbarn'; + String get neighbors_repeatersNeighbors => 'Nachbarn'; @override String get neighbors_noData => 'Keine Nachbarsdaten verfügbar.'; @@ -2898,4 +2901,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'GPX-Kartendaten aus meshcore-open exportieren'; + + @override + String get snrIndicator_nearByRepeaters => 'In der Nähe befindliche Repeater'; + + @override + String get snrIndicator_lastSeen => 'Zuletzt gesehen'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4f0bed12..ef7c0c34 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1065,6 +1065,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chat_pathManagement => 'Path Management'; + @override + String get chat_ShowAllPaths => 'Show all paths'; + @override String get chat_routingMode => 'Routing mode'; @@ -1650,10 +1653,10 @@ class AppLocalizationsEn extends AppLocalizations { String get repeater_cliSubtitle => 'Send commands to the repeater'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbors => 'Neighbors'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighborsSubtitle => 'View zero hop neighbors.'; @override String get repeater_settings => 'Settings'; @@ -2329,10 +2332,10 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get neighbors_receivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Received Neighbors Data'; @override - String get neighbors_requestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Neighbors request timed out.'; @override String neighbors_errorLoading(String error) { @@ -2340,10 +2343,10 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbors => 'Repeaters Neighbors'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'No neighbors data available.'; @override String neighbors_unknownContact(String pubkey) { @@ -2845,4 +2848,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open GPX map data export'; + + @override + String get snrIndicator_nearByRepeaters => 'Nearby Repeaters'; + + @override + String get snrIndicator_lastSeen => 'Last seen'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f56e4e4a..f72196db 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1079,6 +1079,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get chat_pathManagement => 'Gestión de Rutas'; + @override + String get chat_ShowAllPaths => 'Mostrar todos los caminos'; + @override String get chat_routingMode => 'Modo de enrutamiento'; @@ -1674,10 +1677,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliSubtitle => 'Enviar comandos al repetidor'; @override - String get repeater_neighbours => 'Vecinos'; + String get repeater_neighbors => 'Vecinos'; @override - String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.'; + String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.'; @override String get repeater_settings => 'Configuración'; @@ -2376,7 +2379,7 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Repetidores Vecinos'; + String get neighbors_repeatersNeighbors => 'Repetidores Vecinos'; @override String get neighbors_noData => 'No hay datos de vecinos disponibles.'; @@ -2889,4 +2892,10 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open exportación de datos de mapa GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Repetidores cercanos'; + + @override + String get snrIndicator_lastSeen => 'Visto por última vez'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e1325dae..89785685 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1082,6 +1082,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get chat_pathManagement => 'Gestion des chemins'; + @override + String get chat_ShowAllPaths => 'Afficher tous les chemins'; + @override String get chat_routingMode => 'Mode de routage'; @@ -1682,11 +1685,10 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cliSubtitle => 'Envoyer des commandes au répéteur'; @override - String get repeater_neighbours => 'Voisins'; + String get repeater_neighbors => 'Voisins'; @override - String get repeater_neighboursSubtitle => - 'Afficher les voisins de saut nuls.'; + String get repeater_neighborsSubtitle => 'Afficher les voisins de saut nuls.'; @override String get repeater_settings => 'Paramètres'; @@ -2391,7 +2393,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Répéteurs Voisins'; + String get neighbors_repeatersNeighbors => 'Répéteurs Voisins'; @override String get neighbors_noData => @@ -2913,4 +2915,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open exporter les données de carte GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Répéteurs à proximité'; + + @override + String get snrIndicator_lastSeen => 'Dernière fois vu'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 15d5354d..a2b790f6 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1077,6 +1077,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get chat_pathManagement => 'Gestione Percorsi'; + @override + String get chat_ShowAllPaths => 'Mostra tutti i percorsi'; + @override String get chat_routingMode => 'Modalità di routing'; @@ -1672,10 +1675,10 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_cliSubtitle => 'Invia comandi al ripetitore'; @override - String get repeater_neighbours => 'Vicini'; + String get repeater_neighbors => 'Vicini'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Visualizza vicini di salto pari a zero.'; @override @@ -2376,7 +2379,7 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Ripetitori Vicini'; + String get neighbors_repeatersNeighbors => 'Ripetitori Vicini'; @override String get neighbors_noData => 'Nessun dato sugli vicini disponibile.'; @@ -2893,4 +2896,10 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open esportazione dati mappa GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Ripetitori vicini'; + + @override + String get snrIndicator_lastSeen => 'Ultimo accesso'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 17b3bce1..a958e795 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1074,6 +1074,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get chat_pathManagement => 'Beheer van Paden'; + @override + String get chat_ShowAllPaths => 'Toon alle paden'; + @override String get chat_routingMode => 'Routeerwijze'; @@ -1668,10 +1671,10 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater'; @override - String get repeater_neighbours => 'Buren'; + String get repeater_neighbors => 'Buren'; @override - String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.'; + String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.'; @override String get repeater_settings => 'Instellingen'; @@ -2367,7 +2370,7 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Herhalingen Buren'; + String get neighbors_repeatersNeighbors => 'Herhalingen Buren'; @override String get neighbors_noData => 'Geen gegevens van buren beschikbaar.'; @@ -2881,4 +2884,10 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open GPX kaartgegevens exporteren'; + + @override + String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden'; + + @override + String get snrIndicator_lastSeen => 'Laatst gezien'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 147e1606..55bc6ec7 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1079,6 +1079,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get chat_pathManagement => 'Zarządzanie ścieżkami'; + @override + String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki'; + @override String get chat_routingMode => 'Tryb routingu'; @@ -1676,10 +1679,10 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; @override - String get repeater_neighbours => 'Sąsiedzi'; + String get repeater_neighbors => 'Sąsiedzi'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Wyświetl sąsiedztwo zerowych hopów.'; @override @@ -2375,7 +2378,7 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi'; + String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi'; @override String get neighbors_noData => 'Brak danych dotyczących sąsiadów.'; @@ -2895,4 +2898,10 @@ class AppLocalizationsPl extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'Eksport danych mapy GPX meshcore-open'; + + @override + String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu'; + + @override + String get snrIndicator_lastSeen => 'Ostatnio widziany'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b4817757..596d268b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1079,6 +1079,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get chat_pathManagement => 'Gerenciamento de Caminhos'; + @override + String get chat_ShowAllPaths => 'Mostrar todos os caminhos'; + @override String get chat_routingMode => 'Modo de roteamento'; @@ -1674,11 +1677,10 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliSubtitle => 'Enviar comandos ao repetidor'; @override - String get repeater_neighbours => 'Vizinhos'; + String get repeater_neighbors => 'Vizinhos'; @override - String get repeater_neighboursSubtitle => - 'Visualizar vizinhos de salto zero.'; + String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.'; @override String get repeater_settings => 'Configurações'; @@ -2377,7 +2379,7 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos'; + String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos'; @override String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; @@ -2890,4 +2892,10 @@ class AppLocalizationsPt extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open exportação de dados de mapa GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Repetidores Próximos'; + + @override + String get snrIndicator_lastSeen => 'Visto pela última vez'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index e5875b13..4647746a 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1077,6 +1077,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chat_pathManagement => 'Управление маршрутами'; + @override + String get chat_ShowAllPaths => 'Показать все пути'; + @override String get chat_routingMode => 'Режим маршрутизации'; @@ -1676,10 +1679,10 @@ class AppLocalizationsRu extends AppLocalizations { String get repeater_cliSubtitle => 'Отправка команд репитеру'; @override - String get repeater_neighbours => 'Соседи'; + String get repeater_neighbors => 'Соседи'; @override - String get repeater_neighboursSubtitle => 'Просмотр соседей на нулевом хопе.'; + String get repeater_neighborsSubtitle => 'Просмотр соседей на нулевом хопе.'; @override String get repeater_settings => 'Настройки'; @@ -2379,7 +2382,7 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Соседи репитеров'; + String get neighbors_repeatersNeighbors => 'Соседи репитеров'; @override String get neighbors_noData => 'Данные о соседях недоступны.'; @@ -2901,4 +2904,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open экспорт данных карты GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Ближайшие ретрансляторы'; + + @override + String get snrIndicator_lastSeen => 'Последний раз видели'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 4e8b4cb7..8e186631 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1074,6 +1074,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get chat_pathManagement => 'Správa ciest'; + @override + String get chat_ShowAllPaths => 'Zobraziť všetky cesty'; + @override String get chat_routingMode => 'Režim trasy'; @@ -1669,10 +1672,10 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; @override - String get repeater_neighbours => 'Súsezný'; + String get repeater_neighbors => 'Súsezný'; @override - String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.'; + String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.'; @override String get repeater_settings => 'Nastavenia'; @@ -2363,7 +2366,7 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná'; + String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná'; @override String get neighbors_noData => @@ -2877,4 +2880,10 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open export dát GPX mapových údajov'; + + @override + String get snrIndicator_nearByRepeaters => 'Miestne opakovače'; + + @override + String get snrIndicator_lastSeen => 'Naposledy videný'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index e01151e3..b95e7115 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1072,6 +1072,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get chat_pathManagement => 'Upravljanje poti'; + @override + String get chat_ShowAllPaths => 'Prikaži vse poti'; + @override String get chat_routingMode => 'Navodilo za usmerjevalni način'; @@ -1668,10 +1671,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Pošlji ukazne povelje na ponovitveno enoto.'; @override - String get repeater_neighbours => 'Sosedi'; + String get repeater_neighbors => 'Sosedi'; @override - String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.'; + String get repeater_neighborsSubtitle => 'Pogledati nič sosednjih hopjev.'; @override String get repeater_settings => 'Nastavitve'; @@ -2367,7 +2370,7 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi'; + String get neighbors_repeatersNeighbors => 'Ponovitve Sosedi'; @override String get neighbors_noData => 'Niso na voljo podatki o sosedih.'; @@ -2882,4 +2885,10 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open izvoz podatkov GPX karte'; + + @override + String get snrIndicator_nearByRepeaters => 'Bližnji ponovitelji'; + + @override + String get snrIndicator_lastSeen => 'Zadnjič videno'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index f0817114..10047ca8 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1069,6 +1069,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get chat_pathManagement => 'Stigarhantering'; + @override + String get chat_ShowAllPaths => 'Visa alla vägar'; + @override String get chat_routingMode => 'Ruttläge'; @@ -1658,10 +1661,10 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn'; @override - String get repeater_neighbours => 'Grannar'; + String get repeater_neighbors => 'Grannar'; @override - String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.'; + String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.'; @override String get repeater_settings => 'Inställningar'; @@ -2352,7 +2355,7 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Upprepar grannar'; + String get neighbors_repeatersNeighbors => 'Upprepar grannar'; @override String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; @@ -2862,4 +2865,10 @@ class AppLocalizationsSv extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open export av GPX-kartdata'; + + @override + String get snrIndicator_nearByRepeaters => 'Närliggande uppreparstationer'; + + @override + String get snrIndicator_lastSeen => 'Senast sedd'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 847c3e5d..9edc64a6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1075,6 +1075,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get chat_pathManagement => 'Керування шляхами'; + @override + String get chat_ShowAllPaths => 'Показати всі шляхи'; + @override String get chat_routingMode => 'Режим маршрутизації'; @@ -1675,10 +1678,10 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_cliSubtitle => 'Надіслати команди ретранслятору'; @override - String get repeater_neighbours => 'Сусіди'; + String get repeater_neighbors => 'Сусіди'; @override - String get repeater_neighboursSubtitle => + String get repeater_neighborsSubtitle => 'Показати сусідів нульового стрибка.'; @override @@ -2380,7 +2383,7 @@ class AppLocalizationsUk extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => 'Ретранслятори-сусіди'; + String get neighbors_repeatersNeighbors => 'Ретранслятори-сусіди'; @override String get neighbors_noData => 'Дані про сусідів недоступні.'; @@ -2907,4 +2910,10 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'експорт даних карти meshcore-open у форматі GPX'; + + @override + String get snrIndicator_nearByRepeaters => 'Ближні ретранслятори'; + + @override + String get snrIndicator_lastSeen => 'Останній раз бачили'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index fdc45312..9753da6e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1031,6 +1031,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get chat_pathManagement => '路径管理'; + @override + String get chat_ShowAllPaths => '显示所有路径'; + @override String get chat_routingMode => '路由模式'; @@ -1596,10 +1599,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_cliSubtitle => '向复用器发送指令'; @override - String get repeater_neighbours => '邻居'; + String get repeater_neighbors => '邻居'; @override - String get repeater_neighboursSubtitle => '查看邻居节点(无需中间节点)。'; + String get repeater_neighborsSubtitle => '查看邻居节点(无需中间节点)。'; @override String get repeater_settings => '设置'; @@ -2246,7 +2249,7 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get neighbors_repeatersNeighbours => '重复使用的邻居'; + String get neighbors_repeatersNeighbors => '重复使用的邻居'; @override String get neighbors_noData => '没有可用的邻居信息。'; @@ -2714,4 +2717,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_gpxExportShareSubject => 'meshcore-open GPX 地图数据导出'; + + @override + String get snrIndicator_nearByRepeaters => '附近的重复器'; + + @override + String get snrIndicator_lastSeen => '最近访问'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 7c397b46..859e48d9 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Buren", - "repeater_neighboursSubtitle": "Bekijk nul hops buren.", + "repeater_neighbors": "Buren", + "repeater_neighborsSubtitle": "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_repeatersNeighbors": "Herhalingen Buren", "neighbors_noData": "Geen gegevens van buren beschikbaar.", "channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.", "channels_createPrivateChannel": "Maak een Privé Kanaal", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Activeer Bluetooth", "scanner_bluetoothOffMessage": "Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.", "scanner_bluetoothOff": "Bluetooth is uitgeschakeld", + "snrIndicator_lastSeen": "Laatst gezien", + "snrIndicator_nearByRepeaters": "Nabije herhalingseenheden", + "chat_ShowAllPaths": "Toon alle paden", "settings_clientRepeat": "Herhalen: Afgekoppeld", "settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.", "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist." diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 5ebeebff..d03b9119 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Sąsiedzi", - "repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", + "repeater_neighbors": "Sąsiedzi", + "repeater_neighborsSubtitle": "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_repeatersNeighbors": "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ł", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.", "scanner_bluetoothOff": "Bluetooth jest wyłączony", "scanner_enableBluetooth": "Włącz Bluetooth", + "snrIndicator_lastSeen": "Ostatnio widziany", + "snrIndicator_nearByRepeaters": "Nadajniki w pobliżu", + "chat_ShowAllPaths": "Pokaż wszystkie ścieżki", "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz." diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index a88e0388..83a77197 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Vizinhos", + "repeater_neighbors": "Vizinhos", "neighbors_receivedData": "Dados dos Vizinhos Recebidos", - "repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.", + "repeater_neighborsSubtitle": "Visualizar vizinhos de salto zero.", "neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.", "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", - "neighbors_repeatersNeighbours": "Repetidores Vizinhos", + "neighbors_repeatersNeighbors": "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.", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Ative o Bluetooth", "scanner_bluetoothOff": "Bluetooth está desativado", "scanner_bluetoothOffMessage": "Por favor, ative o Bluetooth para escanear por dispositivos.", + "snrIndicator_nearByRepeaters": "Repetidores Próximos", + "snrIndicator_lastSeen": "Visto pela última vez", + "chat_ShowAllPaths": "Mostrar todos os caminhos", "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", "settings_clientRepeat": "Repetição sem rede", "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos." diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index fc17eee1..380ba10b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -467,8 +467,8 @@ "repeater_telemetrySubtitle": "Просмотр телеметрии датчиков и системной статистики", "repeater_cli": "CLI", "repeater_cliSubtitle": "Отправка команд репитеру", - "repeater_neighbours": "Соседи", - "repeater_neighboursSubtitle": "Просмотр соседей на нулевом хопе.", + "repeater_neighbors": "Соседи", + "repeater_neighborsSubtitle": "Просмотр соседей на нулевом хопе.", "repeater_settings": "Настройки", "repeater_settingsSubtitle": "Настройка параметров репитера", "repeater_statusTitle": "Статус репитера", @@ -661,7 +661,7 @@ "neighbors_receivedData": "Полученные данные о соседях", "neighbors_requestTimedOut": "Время ожидания данных о соседях истекло.", "neighbors_errorLoading": "Ошибка загрузки соседей: {error}", - "neighbors_repeatersNeighbours": "Соседи репитеров", + "neighbors_repeatersNeighbors": "Соседи репитеров", "neighbors_noData": "Данные о соседях недоступны.", "neighbors_unknownContact": "Неизвестный {pubkey}", "neighbors_heardA ago": "Слышали: {time} назад", @@ -834,6 +834,9 @@ "scanner_enableBluetooth": "Включите Bluetooth", "scanner_bluetoothOff": "Bluetooth выключен", "scanner_bluetoothOffMessage": "Пожалуйста, включите Bluetooth, чтобы найти устройства.", + "snrIndicator_nearByRepeaters": "Ближайшие ретрансляторы", + "snrIndicator_lastSeen": "Последний раз видели", + "chat_ShowAllPaths": "Показать все пути", "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", "settings_clientRepeat": "Повторение \"вне сети\"" diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 14cd3eca..aca4a29b 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.", + "repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.", "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", "neighbors_receivedData": "Obdielo dáta suseda", - "repeater_neighbours": "Súsezný", + "repeater_neighbors": "Súsezný", "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", - "neighbors_repeatersNeighbours": "Opakovadlá Súsezná", + "neighbors_repeatersNeighbors": "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", @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.", "scanner_bluetoothOff": "Bluetooth je vypnutý", "scanner_enableBluetooth": "Povolte Bluetooth", + "snrIndicator_lastSeen": "Naposledy videný", + "snrIndicator_nearByRepeaters": "Miestne opakovače", + "chat_ShowAllPaths": "Zobraziť všetky cesty", "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných." diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index e633965e..59b84342 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.", - "repeater_neighbours": "Sosedi", + "repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.", + "repeater_neighbors": "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_repeatersNeighbors": "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.", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Omogočite Bluetooth", "scanner_bluetoothOffMessage": "Prosimo, vklopite Bluetooth, da lahko poiščete naprave.", "scanner_bluetoothOff": "Bluetooth je izklopljen", + "snrIndicator_lastSeen": "Zadnjič videno", + "snrIndicator_nearByRepeaters": "Bližnji ponovitelji", + "chat_ShowAllPaths": "Prikaži vse poti", "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", "settings_clientRepeat": "Neovadno ponavljanje" diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 4e50409a..fa786f70 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1351,12 +1351,12 @@ } } }, - "repeater_neighbours": "Grannar", - "repeater_neighboursSubtitle": "Visa noll hoppgrannar.", + "repeater_neighbors": "Grannar", + "repeater_neighborsSubtitle": "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_repeatersNeighbors": "Upprepar grannar", "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", "channels_createPrivateChannel": "Skapa en privat kanal", "channels_joinPrivateChannel": "Gå med i en Privat Kanal", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Aktivera Bluetooth", "scanner_bluetoothOffMessage": "Vänligen aktivera Bluetooth för att söka efter enheter.", "scanner_bluetoothOff": "Bluetooth är avstängt", + "snrIndicator_lastSeen": "Senast sedd", + "snrIndicator_nearByRepeaters": "Närliggande uppreparstationer", + "chat_ShowAllPaths": "Visa alla vägar", "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", "settings_clientRepeat": "Upprepa utan elnät", "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz." diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index afa11793..3f7b276f 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1352,12 +1352,12 @@ } } }, - "repeater_neighbours": "Сусіди", - "repeater_neighboursSubtitle": "Показати сусідів нульового стрибка.", + "repeater_neighbors": "Сусіди", + "repeater_neighborsSubtitle": "Показати сусідів нульового стрибка.", "neighbors_receivedData": "Дані сусідів отримано", "neighbors_requestTimedOut": "Час запиту сусідів вичерпано.", "neighbors_errorLoading": "Помилка завантаження сусідів: {error}", - "neighbors_repeatersNeighbours": "Ретранслятори-сусіди", + "neighbors_repeatersNeighbors": "Ретранслятори-сусіди", "neighbors_noData": "Дані про сусідів недоступні.", "channels_createPrivateChannelDesc": "Захищено секретним ключем.", "channels_joinPrivateChannel": "Приєднатися до приватного каналу", @@ -1594,6 +1594,9 @@ "scanner_enableBluetooth": "Увімкніть Bluetooth", "scanner_bluetoothOffMessage": "Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.", "scanner_bluetoothOff": "Bluetooth вимкнено", + "snrIndicator_lastSeen": "Останній раз бачили", + "snrIndicator_nearByRepeaters": "Ближні ретранслятори", + "chat_ShowAllPaths": "Показати всі шляхи", "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", "settings_clientRepeat": "Автономна система" diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 312ed1a1..bc433927 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -895,8 +895,8 @@ "repeater_telemetrySubtitle": "查看传感器和系统状态的数据。", "repeater_cli": "命令行界面", "repeater_cliSubtitle": "向复用器发送指令", - "repeater_neighbours": "邻居", - "repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。", + "repeater_neighbors": "邻居", + "repeater_neighborsSubtitle": "查看邻居节点(无需中间节点)。", "repeater_settings": "设置", "repeater_settingsSubtitle": "配置重复器参数", "repeater_statusTitle": "重复器状态", @@ -1266,7 +1266,7 @@ } } }, - "neighbors_repeatersNeighbours": "重复使用的邻居", + "neighbors_repeatersNeighbors": "重复使用的邻居", "neighbors_noData": "没有可用的邻居信息。", "neighbors_unknownContact": "Unknown {pubkey}", "@neighbors_unknownContact": { @@ -1594,6 +1594,9 @@ "scanner_bluetoothOffMessage": "请打开蓝牙功能,以便搜索设备。", "scanner_bluetoothOff": "蓝牙已关闭", "scanner_enableBluetooth": "启用蓝牙", + "snrIndicator_lastSeen": "最近访问", + "snrIndicator_nearByRepeaters": "附近的重复器", + "chat_ShowAllPaths": "显示所有路径", "settings_clientRepeat": "离网重复", "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。" diff --git a/lib/models/contact.dart b/lib/models/contact.dart index a98580f3..143a62a2 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -119,7 +119,7 @@ class Contact { final pathBytes = _pathBytesForDisplay; Uint8List? traceBytes; - if (pathLength <= 0) { + if (pathBytes.isEmpty) { traceBytes = Uint8List(1); traceBytes[0] = publicKey[0]; return traceBytes; @@ -160,43 +160,47 @@ class Contact { } static Contact? fromFrame(Uint8List data) { - if (data.length < contactFrameSize) return null; + if (data.isEmpty) return null; if (data[0] != respCodeContact) return null; + try { + final pubKey = Uint8List.fromList( + data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), + ); + final type = data[contactTypeOffset]; + final pathLen = data[contactPathLenOffset].toSigned(8); + final safePathLen = pathLen > 0 + ? (pathLen > maxPathSize ? maxPathSize : pathLen) + : 0; + final pathBytes = safePathLen > 0 + ? Uint8List.fromList( + data.sublist(contactPathOffset, contactPathOffset + safePathLen), + ) + : Uint8List(0); + final name = readCString(data, contactNameOffset, maxNameSize); + final lastmod = readUint32LE(data, contactLastmodOffset); - final pubKey = Uint8List.fromList( - data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), - ); - final type = data[contactTypeOffset]; - final pathLen = data[contactPathLenOffset].toSigned(8); - final safePathLen = pathLen > 0 - ? (pathLen > maxPathSize ? maxPathSize : pathLen) - : 0; - final pathBytes = safePathLen > 0 - ? Uint8List.fromList( - data.sublist(contactPathOffset, contactPathOffset + safePathLen), - ) - : Uint8List(0); - final name = readCString(data, contactNameOffset, maxNameSize); - final lastmod = readUint32LE(data, contactLastmodOffset); + double? lat, lon; + final latRaw = readInt32LE(data, contactLatOffset); + final lonRaw = readInt32LE(data, contactLonOffset); + if (latRaw != 0 || lonRaw != 0) { + lat = latRaw / 1e6; + lon = lonRaw / 1e6; + } - double? lat, lon; - final latRaw = readInt32LE(data, contactLatOffset); - final lonRaw = readInt32LE(data, contactLonOffset); - if (latRaw != 0 || lonRaw != 0) { - lat = latRaw / 1e6; - lon = lonRaw / 1e6; + return Contact( + publicKey: pubKey, + name: name.isEmpty ? 'Unknown' : name, + type: type, + pathLength: pathLen, + path: pathBytes, + latitude: lat, + longitude: lon, + lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000), + ); + } catch (e) { + // If parsing fails, return null + return null; } - - return Contact( - publicKey: pubKey, - name: name.isEmpty ? 'Unknown' : name, - type: type, - pathLength: pathLen, - path: pathBytes, - latitude: lat, - longitude: lon, - lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000), - ); } @override diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index c82356d2..021ad7d8 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -901,7 +901,8 @@ class _ChannelChatScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ChannelMessagePathScreen(message: message), + builder: (context) => + ChannelMessagePathScreen(message: message, channelMessage: true), ), ); } diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 1b0544ca..e6fcacce 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -17,18 +17,27 @@ import '../models/contact.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; - - const ChannelMessagePathScreen({super.key, required this.message}); + final bool channelMessage; + const ChannelMessagePathScreen({ + super.key, + required this.message, + this.channelMessage = false, + }); @override Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { final l10n = context.l10n; - final primaryPath = _selectPrimaryPath( + final primaryPathTmp = _selectPrimaryPath( message.pathBytes, message.pathVariants, ); + + final primaryPath = !channelMessage && !message.isOutgoing + ? Uint8List.fromList(primaryPathTmp.reversed.toList()) + : primaryPathTmp; + final hops = _buildPathHops(primaryPath, connector.contacts, l10n); final hasHopDetails = primaryPath.isNotEmpty; final observedLabel = _formatObservedHops( @@ -37,7 +46,6 @@ class ChannelMessagePathScreen extends StatelessWidget { l10n, ); final extraPaths = _otherPaths(primaryPath, message.pathVariants); - return Scaffold( appBar: AppBar( title: Text(l10n.channelPath_title), @@ -50,9 +58,9 @@ class ChannelMessagePathScreen extends StatelessWidget { MaterialPageRoute( builder: (context) => PathTraceMapScreen( title: context.l10n.contacts_repeaterPathTrace, - path: Uint8List.fromList(primaryPath), + path: primaryPath, flipPathRound: true, - reversePathRound: true, + reversePathRound: !message.isOutgoing && !channelMessage, ), ), ), @@ -62,7 +70,7 @@ class ChannelMessagePathScreen extends StatelessWidget { tooltip: l10n.channelPath_viewMap, onPressed: hasHopDetails ? () { - _openPathMap(context); + _openPathMap(context, channelMessage: channelMessage); } : null, ), @@ -157,7 +165,11 @@ class ChannelMessagePathScreen extends StatelessWidget { ), subtitle: Text(_formatPathPrefixes(variants[i])), trailing: const Icon(Icons.map_outlined, size: 20), - onTap: () => _openPathMap(context, initialPath: variants[i]), + onTap: () => _openPathMap( + context, + initialPath: variants[i], + channelMessage: channelMessage, + ), ), ), ], @@ -248,13 +260,18 @@ class ChannelMessagePathScreen extends StatelessWidget { ); } - void _openPathMap(BuildContext context, {Uint8List? initialPath}) { + void _openPathMap( + BuildContext context, { + Uint8List? initialPath, + bool channelMessage = false, + }) { Navigator.push( context, MaterialPageRoute( builder: (context) => ChannelMessagePathMapScreen( message: message, initialPath: initialPath, + channelMessage: channelMessage, ), ), ); @@ -264,11 +281,13 @@ class ChannelMessagePathScreen extends StatelessWidget { class ChannelMessagePathMapScreen extends StatefulWidget { final ChannelMessage message; final Uint8List? initialPath; + final bool channelMessage; const ChannelMessagePathMapScreen({ super.key, required this.message, this.initialPath, + this.channelMessage = false, }); @override @@ -323,11 +342,18 @@ class _ChannelMessagePathMapScreenState primaryPath, widget.message.pathVariants, ); - final selectedPath = _resolveSelectedPath( + final selectedPathTmp = _resolveSelectedPath( _selectedPath, observedPaths, primaryPath, ); + + final selectedPath = + ((!widget.message.isOutgoing && !widget.channelMessage) || + (widget.message.isOutgoing && widget.channelMessage)) + ? Uint8List.fromList(selectedPathTmp.reversed.toList()) + : selectedPathTmp; + final selectedIndex = _indexForPath(selectedPath, observedPaths); final hops = _buildPathHops( selectedPath, @@ -336,12 +362,22 @@ class _ChannelMessagePathMapScreenState ); final points = []; + + if ((widget.message.isOutgoing && !widget.channelMessage) || + (widget.message.isOutgoing && widget.channelMessage)) { + points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + } + for (final hop in hops) { if (hop.hasLocation) { points.add(hop.position!); } } - points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + + if ((!widget.message.isOutgoing && !widget.channelMessage) || + (!widget.message.isOutgoing && widget.channelMessage)) { + points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + } final polylines = points.length > 1 ? [ diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 6b8b92d4..26062dea 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; @@ -14,7 +15,6 @@ import '../storage/community_store.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; import '../utils/route_transitions.dart'; -import '../widgets/battery_indicator.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; import '../widgets/qr_code_display.dart'; @@ -116,8 +116,7 @@ class _ChannelsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - leading: BatteryIndicator(connector: connector), - title: Text(context.l10n.channels_title), + title: AppBarTitle(context.l10n.channels_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index f00f242a..ad897a0b 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -19,7 +19,9 @@ import '../helpers/utf8_length_limiter.dart'; import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; +import '../models/path_history.dart'; import '../services/path_history_service.dart'; +import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; import '../utils/emoji_utils.dart'; @@ -431,242 +433,317 @@ class _ChatScreenState extends State { void _showPathHistory(BuildContext context) { final connector = Provider.of(context, listen: false); - + bool showAllPaths = false; showDialog( context: context, - builder: (context) => Consumer( - builder: (context, pathService, _) { - final paths = pathService.getRecentPaths(widget.contact.publicKeyHex); - return AlertDialog( - title: Row( - children: [ - const Icon(Icons.timeline), - const SizedBox(width: 8), - Text(context.l10n.chat_pathManagement), - ], - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => Consumer( + builder: (context, pathService, _) { + final paths = pathService.getRecentPaths( + widget.contact.publicKeyHex, + ); + + final repeatersList = List.of(connector.directRepeaters) + ..sort((a, b) => b.ranking.compareTo(a.ranking)); + + if (repeatersList.isEmpty) { + showAllPaths = true; + } + + final directRepeater = repeatersList.isEmpty + ? null + : repeatersList.first; + final secondDirectRepeater = repeatersList.length < 2 + ? null + : repeatersList.elementAt(1); + final thirdDirectRepeater = repeatersList.length < 3 + ? null + : repeatersList.elementAt(2); + + List>> + pathsWithRepeaters = paths.map((path) { + final isDirectRepeater = + directRepeater != null && + path.pathBytes.isNotEmpty && + directRepeater.pubkeyFirstByte == path.pathBytes.first; + final isSecondDirectRepeater = + secondDirectRepeater != null && + path.pathBytes.isNotEmpty && + secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + final isThirdDirectRepeater = + thirdDirectRepeater != null && + path.pathBytes.isNotEmpty && + thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + + int ranking = -1; + Color color = Colors.grey; + if (isDirectRepeater) { + color = Colors.green; + ranking = 3; + } else if (isSecondDirectRepeater) { + color = Colors.yellow; + ranking = 2; + } else if (isThirdDirectRepeater) { + color = Colors.red; + ranking = 1; + } else if (path.wasFloodDiscovery) { + color = Colors.blue; + ranking = 0; + } + + return MapEntry(ranking, MapEntry(color, path)); + }).toList(); + + pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key)); + + return AlertDialog( + title: Row( children: [ - if (paths.isNotEmpty) ...[ + const Icon(Icons.timeline), + const SizedBox(width: 8), + Text(context.l10n.chat_pathManagement), + ], + ), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (pathsWithRepeaters.isNotEmpty) ...[ + if (repeatersList.isNotEmpty) + FeatureToggleRow( + title: context.l10n.chat_ShowAllPaths, + subtitle: "", + value: showAllPaths, + onChanged: (val) { + setDialogState(() { + showAllPaths = val; + }); + }, + ), + Text( + context.l10n.chat_recentAckPaths, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + if (pathsWithRepeaters.length >= 100) ...[ + const SizedBox(height: 8), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.amber[100], + borderRadius: BorderRadius.circular(8), + ), + child: Text( + context.l10n.chat_pathHistoryFull, + style: const TextStyle(fontSize: 12), + ), + ), + ], + const SizedBox(height: 8), + ...pathsWithRepeaters.map((entry) { + final path = entry.value.value; + final color = entry.value.key; + if (!showAllPaths && entry.key < 1) { + return const SizedBox.shrink(); + } else { + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: color, + child: Text( + '${path.hopCount}', + style: const TextStyle(fontSize: 12), + ), + ), + title: Text( + '${path.hopCount} ${path.hopCount == 1 ? context.l10n.chat_hopSingular : context.l10n.chat_hopPlural}', + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(path.timestamp)} • ${path.successCount} ${context.l10n.chat_successes}', + style: const TextStyle(fontSize: 11), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.close, size: 16), + tooltip: context.l10n.chat_removePath, + onPressed: () async { + await pathService.removePathRecord( + widget.contact.publicKeyHex, + path.pathBytes, + ); + }, + ), + path.wasFloodDiscovery + ? const Icon( + Icons.waves, + size: 16, + color: Colors.grey, + ) + : const Icon( + Icons.route, + size: 16, + color: Colors.grey, + ), + ], + ), + onLongPress: () => + _showFullPathDialog(context, path.pathBytes), + onTap: () async { + if (path.pathBytes.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context + .l10n + .chat_pathDetailsNotAvailable, + ), + duration: const Duration(seconds: 2), + ), + ); + return; + } + + final pathBytes = Uint8List.fromList( + path.pathBytes, + ); + final pathLength = path.pathBytes.length; + + // Set the path override to persist user's choice + await connector.setPathOverride( + widget.contact, + pathLen: pathLength, + pathBytes: pathBytes, + ); + + if (!context.mounted) return; + Navigator.pop(context); + await _notifyPathSet( + connector, + widget.contact, + pathBytes, + path.hopCount, + ); + }, + ), + ); + } + }), + const Divider(), + ] else ...[ + Text(context.l10n.chat_noPathHistoryYet), + const Divider(), + ], + const SizedBox(height: 8), Text( - context.l10n.chat_recentAckPaths, + context.l10n.chat_pathActions, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 12, ), ), - if (paths.length >= 100) ...[ - const SizedBox(height: 8), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.amber[100], - borderRadius: BorderRadius.circular(8), - ), - child: Text( - context.l10n.chat_pathHistoryFull, - style: const TextStyle(fontSize: 12), - ), - ), - ], const SizedBox(height: 8), - ...paths.map((path) { - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: path.wasFloodDiscovery - ? Colors.blue - : Colors.green, - child: Text( - '${path.hopCount}', - style: const TextStyle(fontSize: 12), - ), + ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.purple, + child: Icon(Icons.edit_road, size: 16), + ), + title: Text( + context.l10n.chat_setCustomPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_setCustomPathSubtitle, + style: const TextStyle(fontSize: 11), + ), + onTap: () { + Navigator.pop(context); + _showCustomPathDialog(context); + }, + ), + ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.orange, + child: Icon(Icons.clear_all, size: 16), + ), + title: Text( + context.l10n.chat_clearPath, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_clearPathSubtitle, + style: const TextStyle(fontSize: 11), + ), + onTap: () async { + await connector.clearContactPath(widget.contact); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_pathCleared), + duration: const Duration(seconds: 2), ), - title: Text( - '${path.hopCount} ${path.hopCount == 1 ? context.l10n.chat_hopSingular : context.l10n.chat_hopPlural}', - style: const TextStyle(fontSize: 14), + ); + Navigator.pop(context); + }, + ), + ListTile( + dense: true, + leading: const CircleAvatar( + radius: 16, + backgroundColor: Colors.blue, + child: Icon(Icons.waves, size: 16), + ), + title: Text( + context.l10n.chat_forceFloodMode, + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + context.l10n.chat_floodModeSubtitle, + style: const TextStyle(fontSize: 11), + ), + onTap: () async { + await connector.setPathOverride( + widget.contact, + pathLen: -1, + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.chat_floodModeEnabled), + duration: const Duration(seconds: 2), ), - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(path.timestamp)} • ${path.successCount} ${context.l10n.chat_successes}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: context.l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - widget.contact.publicKeyHex, - path.pathBytes, - ); - }, - ), - path.wasFloodDiscovery - ? const Icon( - Icons.waves, - size: 16, - color: Colors.grey, - ) - : const Icon( - Icons.route, - size: 16, - color: Colors.grey, - ), - ], - ), - onLongPress: () => - _showFullPathDialog(context, path.pathBytes), - onTap: () async { - if (path.pathBytes.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context.l10n.chat_pathDetailsNotAvailable, - ), - duration: const Duration(seconds: 2), - ), - ); - return; - } - - final pathBytes = Uint8List.fromList( - path.pathBytes, - ); - final pathLength = path.pathBytes.length; - - // Set the path override to persist user's choice - await connector.setPathOverride( - widget.contact, - pathLen: pathLength, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - await _notifyPathSet( - connector, - widget.contact, - pathBytes, - path.hopCount, - ); - }, - ), - ); - }), - const Divider(), - ] else ...[ - Text(context.l10n.chat_noPathHistoryYet), - const Divider(), + ); + Navigator.pop(context); + }, + ), ], - const SizedBox(height: 8), - Text( - context.l10n.chat_pathActions, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - const SizedBox(height: 8), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.purple, - child: Icon(Icons.edit_road, size: 16), - ), - title: Text( - context.l10n.chat_setCustomPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_setCustomPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () { - Navigator.pop(context); - _showCustomPathDialog(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.orange, - child: Icon(Icons.clear_all, size: 16), - ), - title: Text( - context.l10n.chat_clearPath, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_clearPathSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.clearContactPath(widget.contact); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_pathCleared), - duration: const Duration(seconds: 2), - ), - ); - Navigator.pop(context); - }, - ), - ListTile( - dense: true, - leading: const CircleAvatar( - radius: 16, - backgroundColor: Colors.blue, - child: Icon(Icons.waves, size: 16), - ), - title: Text( - context.l10n.chat_forceFloodMode, - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - context.l10n.chat_floodModeSubtitle, - style: const TextStyle(fontSize: 11), - ), - onTap: () async { - await connector.setPathOverride( - widget.contact, - pathLen: -1, - ); - if (!context.mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.chat_floodModeEnabled), - duration: const Duration(seconds: 2), - ), - ); - Navigator.pop(context); - }, - ), - ], + ), ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.common_close), - ), - ], - ); - }, + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_close), + ), + ], + ); + }, + ), ), ); } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 6799d695..d4701075 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/utils/app_logger.dart'; +import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -16,7 +18,6 @@ import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; import '../utils/emoji_utils.dart'; import '../utils/route_transitions.dart'; -import '../widgets/battery_indicator.dart'; import '../widgets/list_filter_widget.dart'; import '../widgets/empty_state.dart'; import '../widgets/quick_switch_bar.dart'; @@ -90,79 +91,90 @@ class _ContactsScreenState extends State _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final frameBuffer = BufferReader(frame); - final code = frameBuffer.readUInt8(); + try { + final code = frameBuffer.readUInt8(); - if (code == respCodeExportContact) { - final advertPacket = frameBuffer.readRemainingBytes(); - // Validate packet has expected minimum size (98+ bytes per protocol) - if (advertPacket.length < 98) { - if (mounted) { + if (code == respCodeExportContact) { + final advertPacket = frameBuffer.readRemainingBytes(); + // Validate packet has expected minimum size (98+ bytes per protocol) + if (advertPacket.length < 98) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_invalidAdvertFormat), + ), + ); + } + _pendingOperations.remove(ContactOperationType.export); + return; + } + final hexString = pubKeyToHex(advertPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + } + + if (code == respCodeOk) { + // Show a snackbar indicating success + if (!mounted) return; + + if (_pendingOperations.contains(ContactOperationType.import)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.contacts_invalidAdvertFormat), + content: Text(context.l10n.contacts_zeroHopContactAdvertSent), ), ); } - _pendingOperations.remove(ContactOperationType.export); - return; - } - final hexString = pubKeyToHex(advertPacket); - Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); - } - if (code == respCodeOk) { - // Show a snackbar indicating success - if (!mounted) return; + if (_pendingOperations.contains(ContactOperationType.export)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_contactAdvertCopied), + ), + ); + } - if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImported)), - ); + _pendingOperations.clear(); } - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertSent), - ), - ); + if (code == respCodeErr) { + // Show a snackbar indicating failure + if (!mounted) return; + + if (_pendingOperations.contains(ContactOperationType.import)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_contactImportFailed), + ), + ); + } + + if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), + ), + ); + } + if (_pendingOperations.contains(ContactOperationType.export)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.contacts_contactAdvertCopyFailed), + ), + ); + } + + _pendingOperations.clear(); } - - if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), - ); - } - - _pendingOperations.clear(); - } - - if (code == respCodeErr) { - // Show a snackbar indicating failure - if (!mounted) return; - - if (_pendingOperations.contains(ContactOperationType.import)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), - ); - } - - if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_zeroHopContactAdvertFailed), - ), - ); - } - if (_pendingOperations.contains(ContactOperationType.export)) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.contacts_contactAdvertCopyFailed), - ), - ); - } - - _pendingOperations.clear(); + } catch (e) { + appLogger.error( + 'Error processing received frame: $e', + tag: 'ContactsScreen', + ); } }); } @@ -229,9 +241,7 @@ class _ContactsScreenState extends State canPop: allowBack, child: Scaffold( appBar: AppBar( - leading: BatteryIndicator(connector: connector), - title: Text(context.l10n.contacts_title), - centerTitle: true, + title: AppBarTitle(context.l10n.contacts_title), automaticallyImplyLeading: false, actions: [ PopupMenuButton( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 552f579e..1fad04b6 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -17,7 +18,6 @@ import '../services/map_marker_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; -import '../widgets/battery_indicator.dart'; import '../widgets/quick_switch_bar.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; @@ -105,7 +105,7 @@ class _MapScreenState extends State { double _zoomFromStdDev(double latStdDev, double lonStdDev) { final maxSpread = max(latStdDev, lonStdDev); if (maxSpread <= 0) return 13.0; - // Approzimate: each zoom level halves the visible area + // Approximate: each zoom level halves the visible area // ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7 final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3; return zoom.clamp(4.0, 15.0); @@ -262,8 +262,7 @@ class _MapScreenState extends State { canPop: allowBack, child: Scaffold( appBar: AppBar( - leading: BatteryIndicator(connector: connector), - title: Text(context.l10n.map_title), + title: AppBarTitle(context.l10n.map_title), centerTitle: true, automaticallyImplyLeading: false, actions: [ @@ -384,8 +383,8 @@ class _MapScreenState extends State { connector.selfLatitude!, connector.selfLongitude!, ), - width: 35, - height: 35, + width: 40, + height: 40, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -826,7 +825,7 @@ class _MapScreenState extends State { color: _getNodeColor(contact.type), ), const SizedBox(width: 8), - Expanded(child: Text(contact.name)), + Expanded(child: SelectableText(contact.name)), ], ), content: Column( @@ -997,7 +996,7 @@ class _MapScreenState extends State { ), ), const SizedBox(height: 2), - Text(value, style: const TextStyle(fontSize: 14)), + SelectableText(value, style: const TextStyle(fontSize: 14)), ], ), ); diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbors_screen.dart similarity index 75% rename from lib/screens/neighbours_screen.dart rename to lib/screens/neighbors_screen.dart index b6061883..3dee3391 100644 --- a/lib/screens/neighbours_screen.dart +++ b/lib/screens/neighbors_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/utils/app_logger.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; @@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../widgets/snr_indicator.dart'; -class NeighboursScreen extends StatefulWidget { +class NeighborsScreen extends StatefulWidget { final Contact repeater; final String password; - const NeighboursScreen({ + const NeighborsScreen({ super.key, required this.repeater, required this.password, }); @override - State createState() => _NeighboursScreenState(); + State createState() => _NeighborsScreenState(); } -class _NeighboursScreenState extends State { - static const int _reqNeighboursKeyLen = 4; +class _NeighborsScreenState extends State { + static const int _reqNeighborsKeyLen = 4; static const int _statusPayloadOffset = 8; static const int _statusStatsSize = 52; static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize; Uint8List _tagData = Uint8List(4); - int _neighbourCount = 0; + int _neighborCount = 0; bool _isLoading = false; bool _isLoaded = false; @@ -41,7 +42,7 @@ class _NeighboursScreenState extends State { StreamSubscription? _frameSubscription; RepeaterCommandService? _commandService; PathSelection? _pendingStatusSelection; - List>? _parsedNeighbours; + List>? _parsedNeighbors; @override void initState() { @@ -49,7 +50,7 @@ class _NeighboursScreenState extends State { final connector = Provider.of(context, listen: false); _commandService = RepeaterCommandService(connector); _setupMessageListener(); - _loadNeighbours(); + _loadNeighbors(); _hasData = false; } @@ -62,13 +63,12 @@ class _NeighboursScreenState extends State { 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)); + _handleNeighborsResponse(connector, frame.sublist(6)); } }); } @@ -91,65 +91,77 @@ class _NeighboursScreenState extends State { return '${h}h ${m2}m'; } - static List> parseNeighboursData( + static List> parseNeighborsData( 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; - } + final Map> neighbors = {}; + try { + for (var i = 0; i < resultsCount; i++) { + final neighborData = neighbors.putIfAbsent( + i, + () => { + 'contact': null, + 'publicKey': {}, + 'lastHeard': {}, + 'snr': {}, + }, + ); + neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen); + neighborData['lastHeard'] = buffer.readUInt32LE(); + neighborData['snr'] = buffer.readInt8() / 4.0; + } - return neighbours.values.toList(); + return neighbors.values.toList(); + } catch (e) { + appLogger.error( + 'Error parsing neighbors data: $e', + tag: 'NeighborsScreen', + ); + return []; + } } - void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) { + void _handleNeighborsResponse(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; + try { + final neighborCount = buffer.readUInt16LE(); + final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE()); + connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( + repeater, + ) { + for (var neighborData in parsedNeighbors) { + final publicKey = neighborData['publicKey']; + if (listEquals( + repeater.publicKey.sublist(0, _reqNeighborsKeyLen), + publicKey, + )) { + neighborData['contact'] = repeater; + } } - } - }); + }); - setState(() { - _parsedNeighbours = parsedNeighbours; - _neighbourCount = neighbourCount; - }); + setState(() { + _parsedNeighbors = parsedNeighbors; + _neighborCount = neighborCount; + }); - 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; - }); + 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; + }); + } catch (e) { + appLogger.error('Error handling neighbors response: $e'); + } } Contact _resolveRepeater(MeshCoreConnector connector) { @@ -159,7 +171,7 @@ class _NeighboursScreenState extends State { ); } - Future _loadNeighbours() async { + Future _loadNeighbors() async { if (_commandService == null) return; setState(() { @@ -172,17 +184,17 @@ class _NeighboursScreenState extends State { final selection = await connector.preparePathForContactSend(repeater); _pendingStatusSelection = selection; - //[version][number of requested neighbours][offset_16bit][order by][len of public key] + //[version][number of requested neighbors][offset_16bit][order by][len of public key] final frame = buildSendBinaryReq( repeater.publicKey, payload: Uint8List.fromList([ - reqTypeGetNeighbours, + reqTypeGetNeighbors, 0x00, 0x0F, 0x00, 0x00, 0x00, - _reqNeighboursKeyLen, + _reqNeighborsKeyLen, ]), ); await connector.sendFrame(frame); @@ -258,7 +270,7 @@ class _NeighboursScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - l10n.neighbors_repeatersNeighbours, + l10n.neighbors_repeatersNeighbors, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Text( @@ -345,7 +357,7 @@ class _NeighboursScreenState extends State { child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.refresh), - onPressed: _isLoading ? null : _loadNeighbours, + onPressed: _isLoading ? null : _loadNeighbors, tooltip: l10n.repeater_refresh, ), ], @@ -353,13 +365,13 @@ class _NeighboursScreenState extends State { body: SafeArea( top: false, child: RefreshIndicator( - onRefresh: _loadNeighbours, + onRefresh: _loadNeighbors, child: ListView( padding: const EdgeInsets.all(16), children: [ if (!_isLoaded && !_hasData && - (_parsedNeighbours == null || _parsedNeighbours!.isEmpty)) + (_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) Center( child: Text( l10n.neighbors_noData, @@ -368,10 +380,9 @@ class _NeighboursScreenState extends State { ), if (_isLoaded || _hasData && - !(_parsedNeighbours == null || - _parsedNeighbours!.isEmpty)) - _buildNeighboursInfoCard( - "${l10n.repeater_neighbours} - $_neighbourCount", + !(_parsedNeighbors == null || _parsedNeighbors!.isEmpty)) + _buildNeighborsInfoCard( + "${l10n.repeater_neighbors} - $_neighborCount", ), ], ), @@ -380,7 +391,7 @@ class _NeighboursScreenState extends State { ); } - Widget _buildNeighboursInfoCard(String title) { + Widget _buildNeighborsInfoCard(String title) { final connector = Provider.of(context, listen: false); return Card( child: Padding( @@ -405,7 +416,7 @@ class _NeighboursScreenState extends State { ], ), const Divider(), - for (final entry in _parsedNeighbours!.asMap().entries) + for (final entry in _parsedNeighbors!.asMap().entries) _buildInfoRow( entry.value['contact'] != null ? entry.value['contact'].name @@ -430,6 +441,7 @@ class _NeighboursScreenState extends State { double snr, int spreadingFactor, ) { + final snrUi = snrUiFromSNR(snr, spreadingFactor); return Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row( @@ -443,9 +455,15 @@ class _NeighboursScreenState extends State { style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: Text(value), - trailing: SNRIcon( - snr: snr, - snrLevels: getSNRfromSF(spreadingFactor), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(snrUi.icon, color: snrUi.color, size: 18.0), + Text( + snrUi.text, + style: TextStyle(fontSize: 10, color: snrUi.color), + ), + ], ), ), ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 7677d0df..8e24bee1 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -10,6 +10,7 @@ import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/l10n/l10n.dart'; import 'package:meshcore_open/models/contact.dart'; import 'package:meshcore_open/services/map_tile_cache_service.dart'; +import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; import 'package:provider/provider.dart'; @@ -32,7 +33,7 @@ String formatDistance(double distanceMeters) { class PathTraceData { final Uint8List pathData; - final Uint8List snrData; + final List snrData; final Map pathContacts; PathTraceData({ @@ -45,6 +46,7 @@ class PathTraceData { class PathTraceMapScreen extends StatefulWidget { final String title; final Uint8List path; + final int? repeaterId; final bool flipPathRound; final bool reversePathRound; @@ -52,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget { super.key, required this.title, required this.path, + this.repeaterId, this.flipPathRound = false, this.reversePathRound = false, }); @@ -96,7 +99,7 @@ class _PathTraceMapScreenState extends State { super.dispose(); } - Uint8List addReturnpath(Uint8List pathBytes) { + Uint8List addReturnPath(Uint8List pathBytes) { Uint8List? traceBytes; final len = (pathBytes.length + pathBytes.length - 1); traceBytes = Uint8List(len); @@ -124,7 +127,7 @@ class _PathTraceMapScreenState extends State { : widget.path; if (widget.flipPathRound) { - path = addReturnpath(pathTmp); + path = addReturnPath(pathTmp); } else { path = pathTmp; } @@ -146,42 +149,57 @@ class _PathTraceMapScreenState extends State { _frameSubscription = connector.receivedFrames.listen((frame) { if (frame.isEmpty) return; final frameBuffer = BufferReader(frame); - final code = frameBuffer.readUInt8(); + try { + final code = frameBuffer.readUInt8(); - if (code == respCodeSent) { - frameBuffer.skipBytes(1); //reserved - tagData = frameBuffer.readBytes(4); - final timeoutSeconds = frameBuffer.readUInt32LE(); + if (code == respCodeSent) { + frameBuffer.skipBytes(1); //reserved + tagData = frameBuffer.readBytes(4); + final timeoutMilliseconds = frameBuffer.readUInt32LE(); - // Start timeout timer for trace response - _timeoutTimer?.cancel(); - _timeoutTimer = Timer(Duration(milliseconds: timeoutSeconds), () { + // Start timeout timer for trace response + _timeoutTimer?.cancel(); + _timeoutTimer = Timer( + Duration(milliseconds: timeoutMilliseconds), + () { + if (!mounted) return; + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + }, + ); + } + + if (code == respCodeErr) { + _timeoutTimer?.cancel(); if (!mounted) return; setState(() { _isLoading = false; _failed2Loaded = true; }); - }); - } + } - if (code == respCodeErr) { + // Check if it's a binary response + if (frame.length > 8 && + code == pushCodeTraceData && + listEquals(frame.sublist(4, 8), tagData)) { + _timeoutTimer?.cancel(); + if (!mounted) return; + frameBuffer.skipBytes(3); //reserved + path length + flag + if (listEquals(frameBuffer.readBytes(4), tagData)) { + _handleTraceResponse(frame); + } + } + } catch (e) { _timeoutTimer?.cancel(); if (!mounted) return; setState(() { _isLoading = false; _failed2Loaded = true; }); - } - // Check if it's a binary response - if (frame.length > 8 && - code == pushCodeTraceData && - listEquals(frame.sublist(4, 8), tagData)) { - _timeoutTimer?.cancel(); - if (!mounted) return; - frameBuffer.skipBytes(3); //reserved + path length + flag - if (listEquals(frameBuffer.readBytes(4), tagData)) { - _handleTraceResponse(frame); - } + // Handle any parsing errors gracefully + appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen'); } }); } @@ -190,63 +208,83 @@ class _PathTraceMapScreenState extends State { final connector = Provider.of(context, listen: false); final buffer = BufferReader(frame); - buffer.skipBytes(2); // Skip push code and reserved byte - int pathLength = buffer.readUInt8(); - buffer.skipBytes(5); // Skip Flag byte and tag data - buffer.skipBytes(4); // Skip auth code - Uint8List pathData = buffer.readBytes(pathLength); - Uint8List snrData = buffer.readRemainingBytes(); + try { + buffer.skipBytes(2); // Skip push code and reserved byte + int pathLength = buffer.readUInt8(); + buffer.skipBytes(5); // Skip Flag byte and tag data + buffer.skipBytes(4); // Skip auth code + Uint8List pathData = buffer.readBytes(pathLength); + List snrData = buffer + .readRemainingBytes() + .map((snr) => snr.toSigned(8).toDouble() / 4) + .toList(); - Map pathContacts = {}; + Map pathContacts = {}; - connector.contacts.where((c) => c.type != advTypeChat).forEach((repeater) { - for (var repeaterData in pathData) { - if (listEquals( - repeater.publicKey.sublist(0, 1), - Uint8List.fromList([repeaterData]), - )) { - pathContacts[repeaterData] = repeater; + connector.contacts.where((c) => c.type != advTypeChat).forEach(( + repeater, + ) { + for (var repeaterData in pathData) { + if (listEquals( + repeater.publicKey.sublist(0, 1), + Uint8List.fromList([repeaterData]), + )) { + pathContacts[repeaterData] = repeater; + } } - } - }); + }); - setState(() { - _isLoading = false; - _hasData = true; - _traceData = PathTraceData( - pathData: pathData, - snrData: snrData, - pathContacts: pathContacts, - ); - _points = []; - _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); - for (final hop in _traceData!.pathData) { - final contact = _traceData!.pathContacts[hop]; - if (contact != null && - contact.hasLocation && - contact.latitude != null && - contact.longitude != null) { - _points.add(LatLng(contact.latitude!, contact.longitude!)); + setState(() { + _isLoading = false; + _hasData = true; + _traceData = PathTraceData( + pathData: pathData, + snrData: snrData, + pathContacts: pathContacts, + ); + _points = []; + _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); + for (final hop in _traceData!.pathData) { + final contact = _traceData!.pathContacts[hop]; + if (contact != null && + contact.hasLocation && + contact.latitude != null && + contact.longitude != null) { + _points.add(LatLng(contact.latitude!, contact.longitude!)); + } } - } - _polylines = _points.length > 1 - ? [ - Polyline( - points: _points, - strokeWidth: 4, - color: Colors.blueAccent, - ), - ] - : []; + _polylines = _points.length > 1 + ? [ + Polyline( + points: _points, + strokeWidth: 4, + color: Colors.blueAccent, + ), + ] + : []; - _initialCenter = _points.isNotEmpty ? _points.first : const LatLng(0, 0); - _initialZoom = _points.isNotEmpty ? 13.0 : 2.0; - _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; - _mapKey = ValueKey( - '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', + _initialCenter = _points.isNotEmpty + ? _points.first + : const LatLng(0, 0); + _initialZoom = _points.isNotEmpty ? 13.0 : 2.0; + _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; + _mapKey = ValueKey( + '${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', + ); + _pathDistanceMeters = getPathDistanceMeters(_points); + }); + } catch (e) { + appLogger.error( + 'Error handling trace response: $e', + tag: 'PathTraceMapScreen', ); - _pathDistanceMeters = getPathDistanceMeters(_points); - }); + if (mounted) { + setState(() { + _isLoading = false; + _failed2Loaded = true; + }); + } + } } @override @@ -532,6 +570,12 @@ class _PathTraceMapScreenState extends State { itemCount: pathTraceData.pathData.length + 1, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { + final snrUi = snrUiFromSNR( + index < pathTraceData.snrData.length + ? pathTraceData.snrData[index] + : null, + context.read().currentSf, + ); return Column( children: [ ListTile( @@ -550,12 +594,22 @@ class _PathTraceMapScreenState extends State { ), style: const TextStyle(fontSize: 14), ), - trailing: SNRIcon( - snr: - pathTraceData.snrData[index].toSigned( - 8, - ) / - 4.0, + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + snrUi.icon, + color: snrUi.color, + size: 18.0, + ), + Text( + snrUi.text, + style: TextStyle( + fontSize: 10, + color: snrUi.color, + ), + ), + ], ), onTap: () { // Handle item tap diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 903f89e6..a5f503d5 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -6,7 +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'; +import 'neighbors_screen.dart'; class RepeaterHubScreen extends StatelessWidget { final Contact repeater; @@ -174,17 +174,15 @@ class RepeaterHubScreen extends StatelessWidget { _buildManagementCard( context, icon: Icons.group, - title: l10n.repeater_neighbours, - subtitle: l10n.repeater_neighboursSubtitle, + title: l10n.repeater_neighbors, + subtitle: l10n.repeater_neighborsSubtitle, color: Colors.orange, onTap: () { Navigator.push( context, MaterialPageRoute( - builder: (context) => NeighboursScreen( - repeater: repeater, - password: password, - ), + builder: (context) => + NeighborsScreen(repeater: repeater, password: password), ), ); }, diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart new file mode 100644 index 00000000..c88a5965 --- /dev/null +++ b/lib/widgets/app_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/widgets/battery_indicator.dart'; +import 'package:provider/provider.dart'; + +import 'snr_indicator.dart'; + +class AppBarTitle extends StatelessWidget { + final String title; + final Widget? leading; + final Widget? trailing; + const AppBarTitle(this.title, {this.leading, this.trailing, super.key}); + + @override + Widget build(BuildContext context) { + final connector = context.watch(); + + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + leading ?? const SizedBox.shrink(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, overflow: TextOverflow.ellipsis), + if (connector.isConnected && connector.selfName != null) + Text( + '(${connector.selfName})', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + overflow: TextOverflow.ellipsis, + ), + ], + ), + const SizedBox(width: 8), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BatteryIndicator(connector: connector), + SNRIndicator(connector: connector), + ], + ), + trailing ?? const SizedBox.shrink(), + ], + ); + } +} diff --git a/lib/widgets/battery_indicator.dart b/lib/widgets/battery_indicator.dart index 78374150..ccea59dd 100644 --- a/lib/widgets/battery_indicator.dart +++ b/lib/widgets/battery_indicator.dart @@ -68,20 +68,24 @@ class _BatteryIndicatorState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(batteryUi.icon, size: 18, color: batteryUi.color), - const SizedBox(width: 2), - Flexible( - child: Text( - displayText, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: batteryUi.color, + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(batteryUi.icon, size: 18, color: batteryUi.color), + const SizedBox(height: 2), + Flexible( + child: Text( + displayText, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: batteryUi.color, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - overflow: TextOverflow.visible, - maxLines: 1, - softWrap: false, - ), + ], ), ], ), diff --git a/lib/widgets/path_management_dialog.dart b/lib/widgets/path_management_dialog.dart index 483697f1..c2b6d12a 100644 --- a/lib/widgets/path_management_dialog.dart +++ b/lib/widgets/path_management_dialog.dart @@ -1,7 +1,9 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:meshcore_open/models/path_history.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; +import 'package:meshcore_open/widgets/elements_ui.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -19,15 +21,22 @@ class PathManagementDialog { } } -class _PathManagementDialog extends StatelessWidget { +class _PathManagementDialog extends StatefulWidget { final Contact contact; const _PathManagementDialog({required this.contact}); + @override + State<_PathManagementDialog> createState() => _PathManagementDialogState(); +} + +class _PathManagementDialogState extends State<_PathManagementDialog> { + bool _showAllPaths = false; + Contact _resolveContact(MeshCoreConnector connector) { return connector.contacts.firstWhere( - (c) => c.publicKeyHex == contact.publicKeyHex, - orElse: () => contact, + (c) => c.publicKeyHex == widget.contact.publicKeyHex, + orElse: () => widget.contact, ); } @@ -134,6 +143,59 @@ class _PathManagementDialog extends StatelessWidget { final currentContact = _resolveContact(connector); final paths = pathService.getRecentPaths(currentContact.publicKeyHex); + final repeatersList = List.of(connector.directRepeaters) + ..sort((a, b) => b.ranking.compareTo(a.ranking)); + + if (repeatersList.isEmpty) { + _showAllPaths = true; + } + + final directRepeater = repeatersList.isEmpty + ? null + : repeatersList.first; + final secondDirectRepeater = repeatersList.length < 2 + ? null + : repeatersList.elementAt(1); + final thirdDirectRepeater = repeatersList.length < 3 + ? null + : repeatersList.elementAt(2); + + List>> pathsWithRepeaters = + paths.map((path) { + final isDirectRepeater = + directRepeater != null && + path.pathBytes.isNotEmpty && + directRepeater.pubkeyFirstByte == path.pathBytes.first; + final isSecondDirectRepeater = + secondDirectRepeater != null && + path.pathBytes.isNotEmpty && + secondDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + final isThirdDirectRepeater = + thirdDirectRepeater != null && + path.pathBytes.isNotEmpty && + thirdDirectRepeater.pubkeyFirstByte == path.pathBytes.first; + + int ranking = -1; + Color color = Colors.grey; + if (isDirectRepeater) { + color = Colors.green; + ranking = 3; + } else if (isSecondDirectRepeater) { + color = Colors.yellow; + ranking = 2; + } else if (isThirdDirectRepeater) { + color = Colors.red; + ranking = 1; + } else if (path.wasFloodDiscovery) { + color = Colors.blue; + ranking = 0; + } + + return MapEntry(ranking, MapEntry(color, path)); + }).toList(); + + pathsWithRepeaters.sort((a, b) => b.key.compareTo(a.key)); + return AlertDialog( title: Text(l10n.chat_pathManagement), content: SingleChildScrollView( @@ -147,6 +209,17 @@ class _PathManagementDialog extends StatelessWidget { ), const SizedBox(height: 12), if (paths.isNotEmpty) ...[ + if (repeatersList.isNotEmpty) + FeatureToggleRow( + title: l10n.chat_ShowAllPaths, + subtitle: "", + value: _showAllPaths, + onChanged: (val) { + setState(() { + _showAllPaths = val; + }); + }, + ), Text( l10n.chat_recentAckPaths, style: const TextStyle( @@ -154,7 +227,7 @@ class _PathManagementDialog extends StatelessWidget { fontSize: 12, ), ), - if (paths.length >= 100) ...[ + if (pathsWithRepeaters.length >= 100) ...[ const SizedBox(height: 8), Container( width: double.infinity, @@ -173,92 +246,99 @@ class _PathManagementDialog extends StatelessWidget { ), ], const SizedBox(height: 8), - ...paths.map((path) { - return Card( - margin: const EdgeInsets.symmetric(vertical: 4), - child: ListTile( - dense: true, - leading: CircleAvatar( - radius: 16, - backgroundColor: path.wasFloodDiscovery - ? Colors.blue - : Colors.green, - child: Text( - '${path.hopCount}', - style: const TextStyle(fontSize: 12), - ), - ), - title: Text( - l10n.chat_hopsCount(path.hopCount), - style: const TextStyle(fontSize: 14), - ), - subtitle: Text( - '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}', - style: const TextStyle(fontSize: 11), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.close, size: 16), - tooltip: l10n.chat_removePath, - onPressed: () async { - await pathService.removePathRecord( - currentContact.publicKeyHex, - path.pathBytes, - ); - }, + ...pathsWithRepeaters.map((entry) { + final path = entry.value.value; + final color = entry.value.key; + + if (!_showAllPaths && entry.key < 1) { + return const SizedBox.shrink(); + } else { + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: ListTile( + dense: true, + leading: CircleAvatar( + radius: 16, + backgroundColor: color, + child: Text( + '${path.hopCount}', + style: const TextStyle(fontSize: 12), ), - path.wasFloodDiscovery - ? const Icon( - Icons.waves, - size: 16, - color: Colors.grey, - ) - : const Icon( - Icons.route, - size: 16, - color: Colors.grey, + ), + title: Text( + l10n.chat_hopsCount(path.hopCount), + style: const TextStyle(fontSize: 14), + ), + subtitle: Text( + '${(path.tripTimeMs / 1000).toStringAsFixed(2)}s • ${_formatRelativeTime(context, path.timestamp)} • ${path.successCount} ${l10n.chat_successes}', + style: const TextStyle(fontSize: 11), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.close, size: 16), + tooltip: l10n.chat_removePath, + onPressed: () async { + await pathService.removePathRecord( + currentContact.publicKeyHex, + path.pathBytes, + ); + }, + ), + path.wasFloodDiscovery + ? const Icon( + Icons.waves, + size: 16, + color: Colors.grey, + ) + : const Icon( + Icons.route, + size: 16, + color: Colors.grey, + ), + ], + ), + onLongPress: () => + _showFullPathDialog(context, path.pathBytes), + onTap: () async { + if (path.pathBytes.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + l10n.chat_pathDetailsNotAvailable, ), - ], - ), - onLongPress: () => - _showFullPathDialog(context, path.pathBytes), - onTap: () async { - if (path.pathBytes.isEmpty) { + duration: const Duration(seconds: 2), + ), + ); + return; + } + + final pathBytes = Uint8List.fromList( + path.pathBytes, + ); + final pathLength = path.pathBytes.length; + + await connector.setPathOverride( + currentContact, + pathLen: pathLength, + pathBytes: pathBytes, + ); + + if (!context.mounted) return; + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - l10n.chat_pathDetailsNotAvailable, + l10n.path_usingHopsPath(path.hopCount), ), duration: const Duration(seconds: 2), ), ); - return; - } - - final pathBytes = Uint8List.fromList(path.pathBytes); - final pathLength = path.pathBytes.length; - - await connector.setPathOverride( - currentContact, - pathLen: pathLength, - pathBytes: pathBytes, - ); - - if (!context.mounted) return; - Navigator.pop(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - l10n.path_usingHopsPath(path.hopCount), - ), - duration: const Duration(seconds: 2), - ), - ); - }, - ), - ); + }, + ), + ); + } }), const Divider(), ] else ...[ diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart index da68a65a..db4fb8e1 100644 --- a/lib/widgets/snr_indicator.dart +++ b/lib/widgets/snr_indicator.dart @@ -1,4 +1,13 @@ import 'package:flutter/material.dart'; +import '../connector/meshcore_connector.dart'; +import '../l10n/l10n.dart'; + +class SNRUi { + final IconData icon; + final Color color; + final String text; + const SNRUi(this.icon, this.color, this.text); +} List getSNRfromSF(int spreadingFactor) { switch (spreadingFactor) { @@ -19,44 +28,178 @@ List getSNRfromSF(int spreadingFactor) { } } -class SNRIcon extends StatelessWidget { - final double snr; - final List snrLevels; +SNRUi snrUiFromSNR(double? snr, int? spreadingFactor) { + if (snr == null || + spreadingFactor == null || + spreadingFactor < 7 || + spreadingFactor > 12) { + return const SNRUi(Icons.signal_cellular_off, Colors.grey, '—'); + } - const SNRIcon({ - super.key, - required this.snr, - this.snrLevels = const [4.0, -2.0, -4.0, -6.0], - }); + final snrLevels = getSNRfromSF(spreadingFactor); + + IconData icon; + Color color; + String text = '${snr.toStringAsFixed(1)} dB'; + + 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 SNRUi(icon, color, text); +} + +class SNRIndicator extends StatefulWidget { + final MeshCoreConnector connector; + + const SNRIndicator({super.key, required this.connector}); + @override + State createState() => _SNRIndicatorState(); +} + +class _SNRIndicatorState extends State { @override Widget build(BuildContext context) { - IconData icon; - Color color; + final directRepeaters = widget.connector.directRepeaters; + final directBestRepeaters = List.of(directRepeaters) + ..sort((a, b) => (b.ranking).compareTo(a.ranking)); + final directRepeater = directBestRepeaters.isEmpty + ? null + : directBestRepeaters.first; - 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; + final snrUi = snrUiFromSNR( + directBestRepeaters.isNotEmpty ? directRepeater!.snr : null, + widget.connector.currentSf, + ); + + return InkWell( + onTap: () { + if (directRepeater != null) { + _showFullPathDialog(context, directBestRepeaters); + } + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(snrUi.icon, size: 18, color: snrUi.color), + Text( + snrUi.text, + style: TextStyle(fontSize: 12, color: snrUi.color), + ), + ], + ), + if (directRepeater != null) + Text( + '${directRepeaters.length}: ${directRepeater.pubkeyFirstByte.toRadixString(16).padLeft(2, '0')}: ${_formatLastUpdated(directRepeater.lastUpdated)}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: Colors.grey, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + String _formatLastUpdated(DateTime lastSeen) { + final now = DateTime.now(); + final diff = now.difference(lastSeen); + if (diff.isNegative) { + return "0s"; } + if (diff.inMinutes < 1) { + return "${diff.inSeconds}s"; + } + if (diff.inMinutes < 60) { + return "${diff.inMinutes}m"; + } + if (diff.inHours < 24) { + final hours = diff.inHours; + return "${hours}h"; + } + final days = diff.inDays; + return "${days}d"; + } - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, color: color), - Text('$snr dB', style: TextStyle(fontSize: 10, color: color)), - ], + void _showFullPathDialog( + BuildContext context, + List directBestRepeaters, + ) { + final l10n = context.l10n; + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l10n.snrIndicator_nearByRepeaters), + content: SizedBox( + child: Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 4), + itemCount: directBestRepeaters.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (context, index) { + final repeater = directBestRepeaters[index]; + final snrUi = snrUiFromSNR( + repeater.snr, + widget.connector.currentSf, + ); + + final name = widget.connector.contacts + .where((c) => c.publicKey.first == repeater.pubkeyFirstByte) + .map((c) => c.name) + .firstOrNull; + + return Column( + children: [ + ListTile( + leading: Icon(snrUi.icon, color: snrUi.color), + title: Text( + name ?? + repeater.pubkeyFirstByte + .toRadixString(16) + .padLeft(2, '0'), + ), + subtitle: Text( + 'SNR: ${repeater.snr.toStringAsFixed(1)} dB\n${l10n.snrIndicator_lastSeen}: ${_formatLastUpdated(repeater.lastUpdated)}', + ), + ), + ], + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.common_close), + ), + ], + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 09e93017..f6958384 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -497,18 +497,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: From f4b18d97a12f1e44297fbc1a93895d19ee60fb14 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:08:23 -0500 Subject: [PATCH 37/99] Added Line Of Sight Feature for repeater placement, Added app wide Units Setting (#198) * feat: add LOS workflow, global units, l10n cleanup, and mobile UI overflow fixes Squashes prior PR commits into one changeset including: LOS map/service/tests, global metric/imperial unit system adoption, notification/BLE safety fixes, app-wide localization backfill/mojibake cleanup, and responsive UI title/overflow hardening. * l10n: revert unrelated locale churn for LOS feature * feat: keep LOS with app-wide unit settings * fix: resolve post-merge app bar/import analyzer errors * style: format screen files for CI --- lib/l10n/app_bg.arb | 117 +- lib/l10n/app_de.arb | 117 +- lib/l10n/app_en.arb | 115 ++ lib/l10n/app_es.arb | 117 +- lib/l10n/app_fr.arb | 117 +- lib/l10n/app_it.arb | 117 +- lib/l10n/app_localizations.dart | 208 ++++ lib/l10n/app_localizations_bg.dart | 129 +++ lib/l10n/app_localizations_de.dart | 130 +++ lib/l10n/app_localizations_en.dart | 128 +++ lib/l10n/app_localizations_es.dart | 131 +++ lib/l10n/app_localizations_fr.dart | 130 +++ lib/l10n/app_localizations_it.dart | 130 +++ lib/l10n/app_localizations_nl.dart | 130 +++ lib/l10n/app_localizations_pl.dart | 129 +++ lib/l10n/app_localizations_pt.dart | 129 +++ lib/l10n/app_localizations_ru.dart | 129 +++ lib/l10n/app_localizations_sk.dart | 129 +++ lib/l10n/app_localizations_sl.dart | 129 +++ lib/l10n/app_localizations_sv.dart | 127 +++ lib/l10n/app_localizations_uk.dart | 130 +++ lib/l10n/app_localizations_zh.dart | 124 ++ lib/l10n/app_nl.arb | 117 +- lib/l10n/app_pl.arb | 117 +- lib/l10n/app_pt.arb | 117 +- lib/l10n/app_ru.arb | 117 +- lib/l10n/app_sk.arb | 117 +- lib/l10n/app_sl.arb | 117 +- lib/l10n/app_sv.arb | 117 +- lib/l10n/app_uk.arb | 117 +- lib/l10n/app_zh.arb | 117 +- lib/main.dart | 23 + lib/models/app_settings.dart | 28 + lib/screens/app_debug_log_screen.dart | 3 +- lib/screens/app_settings_screen.dart | 56 +- lib/screens/ble_debug_log_screen.dart | 3 +- lib/screens/channel_message_path_screen.dart | 181 ++- lib/screens/community_qr_scanner_screen.dart | 3 +- lib/screens/contacts_screen.dart | 55 +- lib/screens/line_of_sight_map_screen.dart | 1005 +++++++++++++++++ lib/screens/map_cache_screen.dart | 6 +- lib/screens/map_screen.dart | 134 ++- lib/screens/path_trace_map.dart | 191 +++- lib/screens/scanner_screen.dart | 3 +- lib/screens/settings_screen.dart | 6 +- lib/screens/telemetry_screen.dart | 20 +- lib/services/app_settings_service.dart | 10 + lib/services/ble_debug_log_service.dart | 24 +- lib/services/line_of_sight_service.dart | 406 +++++++ lib/services/notification_service.dart | 151 ++- lib/widgets/adaptive_app_bar_title.dart | 17 + test/services/line_of_sight_service_test.dart | 72 ++ 52 files changed, 6078 insertions(+), 214 deletions(-) create mode 100644 lib/screens/line_of_sight_map_screen.dart create mode 100644 lib/services/line_of_sight_service.dart create mode 100644 lib/widgets/adaptive_app_bar_title.dart create mode 100644 test/services/line_of_sight_service_test.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index b6f4301e..5689f951 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Покажи всички пътища", "settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.", "settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.", - "settings_clientRepeat": "Без електричество – повторение" + "settings_clientRepeat": "Без електричество – повторение", + "settings_aboutOpenMeteoAttribution": "Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "единици", + "appSettings_unitsMetric": "Метрика (m / km)", + "appSettings_unitsImperial": "Имперска (ft / mi)", + "map_lineOfSight": "Линия на видимост", + "map_losScreenTitle": "Линия на видимост", + "losSelectStartEnd": "Изберете начални и крайни възли за LOS.", + "losRunFailed": "Проверката на пряката видимост е неуспешна: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Изчистете всички точки", + "losRunToViewElevationProfile": "Стартирайте LOS, за да видите профила на надморската височина", + "losMenuTitle": "LOS меню", + "losMenuSubtitle": "Докоснете възли или натиснете продължително карта за персонализирани точки", + "losShowDisplayNodes": "Показване на възли на дисплея", + "losCustomPoints": "Персонализирани точки", + "losCustomPointLabel": "Персонализирано {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Антена B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Стартирайте LOS", + "losNoElevationData": "Няма данни за надморска височина", + "losProfileClear": "{distance} {distanceUnit}, чист LOS, минимално разстояние {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, блокиран от {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: проверка...", + "losStatusNoData": "LOS: няма данни", + "losStatusSummary": "LOS: {clear}/{total} ясно, {blocked} блокирано, {unknown} неизвестно", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Няма налични данни за надморска височина за една или повече проби.", + "losErrorInvalidInput": "Невалидни данни за точки/надморска височина за изчисляване на LOS.", + "losRenameCustomPoint": "Преименувайте персонализирана точка", + "losPointName": "Име на точката", + "losShowPanelTooltip": "Показване на LOS панел", + "losHidePanelTooltip": "Скриване на LOS панела", + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 077c3989..22fdf6bd 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1627,5 +1627,120 @@ "chat_ShowAllPaths": "Alle Pfade anzeigen", "settings_clientRepeat": "Wiederholung, ohne Stromanschluss", "settings_clientRepeatFreqWarning": "Die Kommunikation ohne Stromversorgung erfordert Frequenzen von 433, 869 oder 918 MHz.", - "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen." + "settings_clientRepeatSubtitle": "Ermöglichen Sie diesem Gerät, Mesh-Pakete für andere zu wiederholen.", + "settings_aboutOpenMeteoAttribution": "LOS-Höhendaten: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Einheiten", + "appSettings_unitsMetric": "Metrisch (m/km)", + "appSettings_unitsImperial": "Imperial (ft/mi)", + "map_lineOfSight": "Sichtlinie", + "map_losScreenTitle": "Sichtlinie", + "losSelectStartEnd": "Wählen Sie Start- und Endknoten für LOS aus.", + "losRunFailed": "Sichtlinienprüfung fehlgeschlagen: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Löschen Sie alle Punkte", + "losRunToViewElevationProfile": "Führen Sie LOS aus, um das Höhenprofil anzuzeigen", + "losMenuTitle": "LOS-Menü", + "losMenuSubtitle": "Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen", + "losShowDisplayNodes": "Anzeigeknoten anzeigen", + "losCustomPoints": "Benutzerdefinierte Punkte", + "losCustomPointLabel": "Benutzerdefiniert {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punkt A", + "losPointB": "Punkt B", + "losAntennaA": "Antenne A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenne B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Führen Sie LOS aus", + "losNoElevationData": "Keine Höhendaten", + "losProfileClear": "{distance} {distanceUnit}, freie Sichtlinie, Mindestabstand {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blockiert durch {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: Überprüfen...", + "losStatusNoData": "LOS: keine Daten", + "losStatusSummary": "Sichtlinie: {clear}/{total} frei, {blocked} blockiert, {unknown} unbekannt", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Für eine oder mehrere Proben sind keine Höhendaten verfügbar.", + "losErrorInvalidInput": "Ungültige Punkte/Höhendaten für die LOS-Berechnung.", + "losRenameCustomPoint": "Benennen Sie den benutzerdefinierten Punkt um", + "losPointName": "Punktname", + "losShowPanelTooltip": "LOS-Panel anzeigen", + "losHidePanelTooltip": "LOS-Panel ausblenden", + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bf49d7ef..ae245398 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -131,6 +131,7 @@ }, "settings_aboutLegalese": "2026 MeshCore Open Source Project", "settings_aboutDescription": "An open-source Flutter client for MeshCore LoRa mesh networking devices.", + "settings_aboutOpenMeteoAttribution": "LOS elevation data: Open-Meteo (CC BY 4.0)", "settings_infoName": "Name", "settings_infoId": "ID", "settings_infoStatus": "Status", @@ -242,6 +243,9 @@ "appSettings_last24Hours": "Last 24 hours", "appSettings_lastWeek": "Last week", "appSettings_offlineMapCache": "Offline Map Cache", + "appSettings_unitsTitle": "Units", + "appSettings_unitsMetric": "Metric (m / km)", + "appSettings_unitsImperial": "Imperial (ft / mi)", "appSettings_noAreaSelected": "No area selected", "appSettings_areaSelectedZoom": "Area selected (zoom {minZoom}-{maxZoom})", "@appSettings_areaSelectedZoom": { @@ -639,6 +643,8 @@ }, "chat_invalidLink": "Invalid link format", "map_title": "Node Map", + "map_lineOfSight": "Line of Sight", + "map_losScreenTitle": "Line of Sight", "map_noNodesWithLocation": "No nodes with location data", "map_nodesNeedGps": "Nodes need to share their GPS coordinates\nto appear on the map", "map_nodesCount": "Nodes: {count}", @@ -1548,6 +1554,115 @@ "pathTrace_refreshTooltip": "Refresh Path Trace.", "pathTrace_someHopsNoLocation": "One or more of the hops is missing a location!", "pathTrace_clearTooltip": "Clear path.", + "losSelectStartEnd": "Select start and end nodes for LOS.", + "losRunFailed": "Line-of-sight check failed: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Clear all points", + "losRunToViewElevationProfile": "Run LOS to view elevation profile", + "losMenuTitle": "LOS Menu", + "losMenuSubtitle": "Tap nodes or long-press map for custom points", + "losShowDisplayNodes": "Show display nodes", + "losCustomPoints": "Custom points", + "losCustomPointLabel": "Custom {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Point A", + "losPointB": "Point B", + "losAntennaA": "Antenna A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenna B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Run LOS", + "losNoElevationData": "No elevation data", + "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: checking...", + "losStatusNoData": "LOS: no data", + "losStatusSummary": "LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Elevation data unavailable for one or more samples.", + "losErrorInvalidInput": "Invalid points/elevation data for LOS calculation.", + "losRenameCustomPoint": "Rename custom point", + "losPointName": "Point name", + "losShowPanelTooltip": "Show LOS panel", + "losHidePanelTooltip": "Hide LOS panel", + "losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)", "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1896b4f3..3a7fe537 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1627,5 +1627,120 @@ "chat_ShowAllPaths": "Mostrar todos los caminos", "settings_clientRepeatFreqWarning": "Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.", "settings_clientRepeat": "Repetir sin conexión", - "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios." + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita los paquetes de red para otros usuarios.", + "settings_aboutOpenMeteoAttribution": "Datos de elevación LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unidades", + "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsImperial": "Imperial (pies/millas)", + "map_lineOfSight": "Línea de visión", + "map_losScreenTitle": "Línea de visión", + "losSelectStartEnd": "Seleccione los nodos de inicio y fin para LOS.", + "losRunFailed": "Error en la comprobación de la línea de visión: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Borrar todos los puntos", + "losRunToViewElevationProfile": "Ejecute LOS para ver el perfil de elevación", + "losMenuTitle": "Menú LOS", + "losMenuSubtitle": "Toque nodos o mantenga presionado el mapa para puntos personalizados", + "losShowDisplayNodes": "Mostrar nodos de visualización", + "losCustomPoints": "Puntos personalizados", + "losCustomPointLabel": "Personalizado {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punto A", + "losPointB": "Punto B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Ejecutar LOS", + "losNoElevationData": "Sin datos de elevación", + "losProfileClear": "{distance} {distanceUnit}, despejar LOS, autorización mínima {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: comprobando...", + "losStatusNoData": "LOS: sin datos", + "losStatusSummary": "LOS: {clear}/{total} claro, {blocked} bloqueado, {unknown} desconocido", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Datos de elevación no disponibles para una o más muestras.", + "losErrorInvalidInput": "Datos de puntos/elevación no válidos para el cálculo de LOS.", + "losRenameCustomPoint": "Cambiar el nombre del punto personalizado", + "losPointName": "Nombre del punto", + "losShowPanelTooltip": "Mostrar panel LOS", + "losHidePanelTooltip": "Ocultar panel LOS", + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index d1befced..f962ee5c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Afficher tous les chemins", "settings_clientRepeatFreqWarning": "Pour les transmissions hors réseau, il est nécessaire d'utiliser les fréquences de 433, 869 ou 918 MHz.", "settings_clientRepeatSubtitle": "Permettez à cet appareil de répéter les paquets de données pour les autres.", - "settings_clientRepeat": "Répétition hors réseau" + "settings_clientRepeat": "Répétition hors réseau", + "settings_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unités", + "appSettings_unitsMetric": "Métrique (m/km)", + "appSettings_unitsImperial": "Impérial (ft / mi)", + "map_lineOfSight": "Ligne de vue", + "map_losScreenTitle": "Ligne de vue", + "losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.", + "losRunFailed": "Échec de la vérification de la ligne de vue : {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Effacer tous les points", + "losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés", + "losShowDisplayNodes": "Afficher les nœuds d'affichage", + "losCustomPoints": "Points personnalisés", + "losCustomPointLabel": "Personnalisé {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Point A", + "losPointB": "Point B", + "losAntennaA": "Antenne A : {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenne B : {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Exécuter la LOS", + "losNoElevationData": "Aucune donnée d'altitude", + "losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS : vérification...", + "losStatusNoData": "LOS : aucune donnée", + "losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.", + "losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.", + "losRenameCustomPoint": "Renommer le point personnalisé", + "losPointName": "Nom du point", + "losShowPanelTooltip": "Afficher le panneau LOS", + "losHidePanelTooltip": "Masquer le panneau LOS", + "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 22371baf..61110043 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Mostra tutti i percorsi", "settings_clientRepeat": "Ripetizione \"fuori dalla rete\"", "settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.", - "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri." + "settings_clientRepeatSubtitle": "Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.", + "settings_aboutOpenMeteoAttribution": "Dati di elevazione LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unità", + "appSettings_unitsMetric": "Metrico (m/km)", + "appSettings_unitsImperial": "Imperiale (ft / mi)", + "map_lineOfSight": "Linea di vista", + "map_losScreenTitle": "Linea di vista", + "losSelectStartEnd": "Seleziona i nodi iniziali e finali per la LOS.", + "losRunFailed": "Controllo della linea di vista fallito: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Cancella tutti i punti", + "losRunToViewElevationProfile": "Eseguire LOS per visualizzare il profilo altimetrico", + "losMenuTitle": "Menù LOS", + "losMenuSubtitle": "Tocca i nodi o premi a lungo la mappa per punti personalizzati", + "losShowDisplayNodes": "Mostra i nodi di visualizzazione", + "losCustomPoints": "Punti personalizzati", + "losCustomPointLabel": "Personalizzato {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punto A", + "losPointB": "Punto B", + "losAntennaA": "Antenna A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenna B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Esegui LOS", + "losNoElevationData": "Nessun dato di elevazione", + "losProfileClear": "{distance} {distanceUnit}, libera LOS, distanza minima {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloccato da {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: controllo...", + "losStatusNoData": "LOS: nessun dato", + "losStatusSummary": "LOS: {clear}/{total} libera, {blocked} bloccato, {unknown} sconosciuto", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Dati di elevazione non disponibili per uno o più campioni.", + "losErrorInvalidInput": "Dati punti/elevazione non validi per il calcolo della LOS.", + "losRenameCustomPoint": "Rinomina punto personalizzato", + "losPointName": "Nome del punto", + "losShowPanelTooltip": "Mostra il pannello LOS", + "losHidePanelTooltip": "Nascondi il pannello LOS", + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 2bcda786..20d04228 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -700,6 +700,12 @@ abstract class AppLocalizations { /// **'An open-source Flutter client for MeshCore LoRa mesh networking devices.'** String get settings_aboutDescription; + /// No description provided for @settings_aboutOpenMeteoAttribution. + /// + /// In en, this message translates to: + /// **'LOS elevation data: Open-Meteo (CC BY 4.0)'** + String get settings_aboutOpenMeteoAttribution; + /// No description provided for @settings_infoName. /// /// In en, this message translates to: @@ -1240,6 +1246,24 @@ abstract class AppLocalizations { /// **'Offline Map Cache'** String get appSettings_offlineMapCache; + /// No description provided for @appSettings_unitsTitle. + /// + /// In en, this message translates to: + /// **'Units'** + String get appSettings_unitsTitle; + + /// No description provided for @appSettings_unitsMetric. + /// + /// In en, this message translates to: + /// **'Metric (m / km)'** + String get appSettings_unitsMetric; + + /// No description provided for @appSettings_unitsImperial. + /// + /// In en, this message translates to: + /// **'Imperial (ft / mi)'** + String get appSettings_unitsImperial; + /// No description provided for @appSettings_noAreaSelected. /// /// In en, this message translates to: @@ -2290,6 +2314,18 @@ abstract class AppLocalizations { /// **'Node Map'** String get map_title; + /// No description provided for @map_lineOfSight. + /// + /// In en, this message translates to: + /// **'Line of Sight'** + String get map_lineOfSight; + + /// No description provided for @map_losScreenTitle. + /// + /// In en, this message translates to: + /// **'Line of Sight'** + String get map_losScreenTitle; + /// No description provided for @map_noNodesWithLocation. /// /// In en, this message translates to: @@ -4772,6 +4808,178 @@ abstract class AppLocalizations { /// **'Clear path.'** String get pathTrace_clearTooltip; + /// No description provided for @losSelectStartEnd. + /// + /// In en, this message translates to: + /// **'Select start and end nodes for LOS.'** + String get losSelectStartEnd; + + /// No description provided for @losRunFailed. + /// + /// In en, this message translates to: + /// **'Line-of-sight check failed: {error}'** + String losRunFailed(String error); + + /// No description provided for @losClearAllPoints. + /// + /// In en, this message translates to: + /// **'Clear all points'** + String get losClearAllPoints; + + /// No description provided for @losRunToViewElevationProfile. + /// + /// In en, this message translates to: + /// **'Run LOS to view elevation profile'** + String get losRunToViewElevationProfile; + + /// No description provided for @losMenuTitle. + /// + /// In en, this message translates to: + /// **'LOS Menu'** + String get losMenuTitle; + + /// No description provided for @losMenuSubtitle. + /// + /// In en, this message translates to: + /// **'Tap nodes or long-press map for custom points'** + String get losMenuSubtitle; + + /// No description provided for @losShowDisplayNodes. + /// + /// In en, this message translates to: + /// **'Show display nodes'** + String get losShowDisplayNodes; + + /// No description provided for @losCustomPoints. + /// + /// In en, this message translates to: + /// **'Custom points'** + String get losCustomPoints; + + /// No description provided for @losCustomPointLabel. + /// + /// In en, this message translates to: + /// **'Custom {index}'** + String losCustomPointLabel(int index); + + /// No description provided for @losPointA. + /// + /// In en, this message translates to: + /// **'Point A'** + String get losPointA; + + /// No description provided for @losPointB. + /// + /// In en, this message translates to: + /// **'Point B'** + String get losPointB; + + /// No description provided for @losAntennaA. + /// + /// In en, this message translates to: + /// **'Antenna A: {value} {unit}'** + String losAntennaA(String value, String unit); + + /// No description provided for @losAntennaB. + /// + /// In en, this message translates to: + /// **'Antenna B: {value} {unit}'** + String losAntennaB(String value, String unit); + + /// No description provided for @losRun. + /// + /// In en, this message translates to: + /// **'Run LOS'** + String get losRun; + + /// No description provided for @losNoElevationData. + /// + /// In en, this message translates to: + /// **'No elevation data'** + String get losNoElevationData; + + /// No description provided for @losProfileClear. + /// + /// In en, this message translates to: + /// **'{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}'** + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ); + + /// No description provided for @losProfileBlocked. + /// + /// In en, this message translates to: + /// **'{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}'** + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ); + + /// No description provided for @losStatusChecking. + /// + /// In en, this message translates to: + /// **'LOS: checking...'** + String get losStatusChecking; + + /// No description provided for @losStatusNoData. + /// + /// In en, this message translates to: + /// **'LOS: no data'** + String get losStatusNoData; + + /// No description provided for @losStatusSummary. + /// + /// In en, this message translates to: + /// **'LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown'** + String losStatusSummary(int clear, int total, int blocked, int unknown); + + /// No description provided for @losErrorElevationUnavailable. + /// + /// In en, this message translates to: + /// **'Elevation data unavailable for one or more samples.'** + String get losErrorElevationUnavailable; + + /// No description provided for @losErrorInvalidInput. + /// + /// In en, this message translates to: + /// **'Invalid points/elevation data for LOS calculation.'** + String get losErrorInvalidInput; + + /// No description provided for @losRenameCustomPoint. + /// + /// In en, this message translates to: + /// **'Rename custom point'** + String get losRenameCustomPoint; + + /// No description provided for @losPointName. + /// + /// In en, this message translates to: + /// **'Point name'** + String get losPointName; + + /// No description provided for @losShowPanelTooltip. + /// + /// In en, this message translates to: + /// **'Show LOS panel'** + String get losShowPanelTooltip; + + /// No description provided for @losHidePanelTooltip. + /// + /// In en, this message translates to: + /// **'Hide LOS panel'** + String get losHidePanelTooltip; + + /// No description provided for @losElevationAttribution. + /// + /// In en, this message translates to: + /// **'Elevation data: Open-Meteo (CC BY 4.0)'** + String get losElevationAttribution; + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 137d48a5..9c66ff22 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -326,6 +326,10 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_aboutDescription => 'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Име'; @@ -622,6 +626,15 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; + @override + String get appSettings_unitsTitle => 'единици'; + + @override + String get appSettings_unitsMetric => 'Метрика (m / km)'; + + @override + String get appSettings_unitsImperial => 'Имперска (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Няма избрана област'; @@ -1243,6 +1256,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get map_title => 'Карта на възлите'; + @override + String get map_lineOfSight => 'Линия на видимост'; + + @override + String get map_losScreenTitle => 'Линия на видимост'; + @override String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.'; @@ -2724,6 +2743,116 @@ class AppLocalizationsBg extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Изчисти пътя'; + @override + String get losSelectStartEnd => 'Изберете начални и крайни възли за LOS.'; + + @override + String losRunFailed(String error) { + return 'Проверката на пряката видимост е неуспешна: $error'; + } + + @override + String get losClearAllPoints => 'Изчистете всички точки'; + + @override + String get losRunToViewElevationProfile => + 'Стартирайте LOS, за да видите профила на надморската височина'; + + @override + String get losMenuTitle => 'LOS меню'; + + @override + String get losMenuSubtitle => + 'Докоснете възли или натиснете продължително карта за персонализирани точки'; + + @override + String get losShowDisplayNodes => 'Показване на възли на дисплея'; + + @override + String get losCustomPoints => 'Персонализирани точки'; + + @override + String losCustomPointLabel(int index) { + return 'Персонализирано $index'; + } + + @override + String get losPointA => 'Точка А'; + + @override + String get losPointB => 'Точка Б'; + + @override + String losAntennaA(String value, String unit) { + return 'Антена A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Антена B: $value $unit'; + } + + @override + String get losRun => 'Стартирайте LOS'; + + @override + String get losNoElevationData => 'Няма данни за надморска височина'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, чист LOS, минимално разстояние $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, блокиран от $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: проверка...'; + + @override + String get losStatusNoData => 'LOS: няма данни'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total ясно, $blocked блокирано, $unknown неизвестно'; + } + + @override + String get losErrorElevationUnavailable => + 'Няма налични данни за надморска височина за една или повече проби.'; + + @override + String get losErrorInvalidInput => + 'Невалидни данни за точки/надморска височина за изчисляване на LOS.'; + + @override + String get losRenameCustomPoint => 'Преименувайте персонализирана точка'; + + @override + String get losPointName => 'Име на точката'; + + @override + String get losShowPanelTooltip => 'Показване на LOS панел'; + + @override + String get losHidePanelTooltip => 'Скриване на LOS панела'; + + @override + String get losElevationAttribution => + 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 927ac488..ef7cd9d2 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -320,6 +320,10 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_aboutDescription => 'Ein Open-Source-Flutter-Client für MeshCore LoRa-Meshnetzwerkgeräte.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS-Höhendaten: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Name'; @@ -619,6 +623,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline-Karten-Cache'; + @override + String get appSettings_unitsTitle => 'Einheiten'; + + @override + String get appSettings_unitsMetric => 'Metrisch (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (ft/mi)'; + @override String get appSettings_noAreaSelected => 'Kein Bereich ausgewählt'; @@ -1242,6 +1255,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get map_title => 'Karte'; + @override + String get map_lineOfSight => 'Sichtlinie'; + + @override + String get map_losScreenTitle => 'Sichtlinie'; + @override String get map_noNodesWithLocation => 'Keine Knoten mit Standortdaten'; @@ -2729,6 +2748,117 @@ class AppLocalizationsDe extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Pfad löschen'; + @override + String get losSelectStartEnd => + 'Wählen Sie Start- und Endknoten für LOS aus.'; + + @override + String losRunFailed(String error) { + return 'Sichtlinienprüfung fehlgeschlagen: $error'; + } + + @override + String get losClearAllPoints => 'Löschen Sie alle Punkte'; + + @override + String get losRunToViewElevationProfile => + 'Führen Sie LOS aus, um das Höhenprofil anzuzeigen'; + + @override + String get losMenuTitle => 'LOS-Menü'; + + @override + String get losMenuSubtitle => + 'Tippen Sie auf Knoten oder drücken Sie lange auf die Karte, um benutzerdefinierte Punkte anzuzeigen'; + + @override + String get losShowDisplayNodes => 'Anzeigeknoten anzeigen'; + + @override + String get losCustomPoints => 'Benutzerdefinierte Punkte'; + + @override + String losCustomPointLabel(int index) { + return 'Benutzerdefiniert $index'; + } + + @override + String get losPointA => 'Punkt A'; + + @override + String get losPointB => 'Punkt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenne A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenne B: $value $unit'; + } + + @override + String get losRun => 'Führen Sie LOS aus'; + + @override + String get losNoElevationData => 'Keine Höhendaten'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, freie Sichtlinie, Mindestabstand $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blockiert durch $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: Überprüfen...'; + + @override + String get losStatusNoData => 'LOS: keine Daten'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'Sichtlinie: $clear/$total frei, $blocked blockiert, $unknown unbekannt'; + } + + @override + String get losErrorElevationUnavailable => + 'Für eine oder mehrere Proben sind keine Höhendaten verfügbar.'; + + @override + String get losErrorInvalidInput => + 'Ungültige Punkte/Höhendaten für die LOS-Berechnung.'; + + @override + String get losRenameCustomPoint => + 'Benennen Sie den benutzerdefinierten Punkt um'; + + @override + String get losPointName => 'Punktname'; + + @override + String get losShowPanelTooltip => 'LOS-Panel anzeigen'; + + @override + String get losHidePanelTooltip => 'LOS-Panel ausblenden'; + + @override + String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ef7c0c34..7f07e263 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -318,6 +318,10 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_aboutDescription => 'An open-source Flutter client for MeshCore LoRa mesh networking devices.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS elevation data: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Name'; @@ -614,6 +618,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Map Cache'; + @override + String get appSettings_unitsTitle => 'Units'; + + @override + String get appSettings_unitsMetric => 'Metric (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (ft / mi)'; + @override String get appSettings_noAreaSelected => 'No area selected'; @@ -1222,6 +1235,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get map_title => 'Node Map'; + @override + String get map_lineOfSight => 'Line of Sight'; + + @override + String get map_losScreenTitle => 'Line of Sight'; + @override String get map_noNodesWithLocation => 'No nodes with location data'; @@ -2683,6 +2702,115 @@ class AppLocalizationsEn extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Clear path.'; + @override + String get losSelectStartEnd => 'Select start and end nodes for LOS.'; + + @override + String losRunFailed(String error) { + return 'Line-of-sight check failed: $error'; + } + + @override + String get losClearAllPoints => 'Clear all points'; + + @override + String get losRunToViewElevationProfile => + 'Run LOS to view elevation profile'; + + @override + String get losMenuTitle => 'LOS Menu'; + + @override + String get losMenuSubtitle => 'Tap nodes or long-press map for custom points'; + + @override + String get losShowDisplayNodes => 'Show display nodes'; + + @override + String get losCustomPoints => 'Custom points'; + + @override + String losCustomPointLabel(int index) { + return 'Custom $index'; + } + + @override + String get losPointA => 'Point A'; + + @override + String get losPointB => 'Point B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenna A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenna B: $value $unit'; + } + + @override + String get losRun => 'Run LOS'; + + @override + String get losNoElevationData => 'No elevation data'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blocked by $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: checking...'; + + @override + String get losStatusNoData => 'LOS: no data'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown'; + } + + @override + String get losErrorElevationUnavailable => + 'Elevation data unavailable for one or more samples.'; + + @override + String get losErrorInvalidInput => + 'Invalid points/elevation data for LOS calculation.'; + + @override + String get losRenameCustomPoint => 'Rename custom point'; + + @override + String get losPointName => 'Point name'; + + @override + String get losShowPanelTooltip => 'Show LOS panel'; + + @override + String get losHidePanelTooltip => 'Hide LOS panel'; + + @override + String get losElevationAttribution => + 'Elevation data: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f72196db..6409675a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -323,6 +323,10 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_aboutDescription => 'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Datos de elevación LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nombre'; @@ -620,6 +624,15 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Caché de Mapa Offline'; + @override + String get appSettings_unitsTitle => 'Unidades'; + + @override + String get appSettings_unitsMetric => 'Métrico (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (pies/millas)'; + @override String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área'; @@ -1240,6 +1253,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get map_title => 'Mapa de Nodos'; + @override + String get map_lineOfSight => 'Línea de visión'; + + @override + String get map_losScreenTitle => 'Línea de visión'; + @override String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación'; @@ -2722,6 +2741,118 @@ class AppLocalizationsEs extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Borrar ruta'; + @override + String get losSelectStartEnd => + 'Seleccione los nodos de inicio y fin para LOS.'; + + @override + String losRunFailed(String error) { + return 'Error en la comprobación de la línea de visión: $error'; + } + + @override + String get losClearAllPoints => 'Borrar todos los puntos'; + + @override + String get losRunToViewElevationProfile => + 'Ejecute LOS para ver el perfil de elevación'; + + @override + String get losMenuTitle => 'Menú LOS'; + + @override + String get losMenuSubtitle => + 'Toque nodos o mantenga presionado el mapa para puntos personalizados'; + + @override + String get losShowDisplayNodes => 'Mostrar nodos de visualización'; + + @override + String get losCustomPoints => 'Puntos personalizados'; + + @override + String losCustomPointLabel(int index) { + return 'Personalizado $index'; + } + + @override + String get losPointA => 'Punto A'; + + @override + String get losPointB => 'Punto B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Ejecutar LOS'; + + @override + String get losNoElevationData => 'Sin datos de elevación'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: comprobando...'; + + @override + String get losStatusNoData => 'LOS: sin datos'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido'; + } + + @override + String get losErrorElevationUnavailable => + 'Datos de elevación no disponibles para una o más muestras.'; + + @override + String get losErrorInvalidInput => + 'Datos de puntos/elevación no válidos para el cálculo de LOS.'; + + @override + String get losRenameCustomPoint => + 'Cambiar el nombre del punto personalizado'; + + @override + String get losPointName => 'Nombre del punto'; + + @override + String get losShowPanelTooltip => 'Mostrar panel LOS'; + + @override + String get losHidePanelTooltip => 'Ocultar panel LOS'; + + @override + String get losElevationAttribution => + 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 89785685..3536cf56 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -324,6 +324,10 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_aboutDescription => 'Un client Flutter open source pour les appareils de réseau mesh MeshCore LoRa.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Données d\'élévation LOS : Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nom'; @@ -622,6 +626,15 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Cache de Carte Hors Ligne'; + @override + String get appSettings_unitsTitle => 'Unités'; + + @override + String get appSettings_unitsMetric => 'Métrique (m/km)'; + + @override + String get appSettings_unitsImperial => 'Impérial (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Aucune zone sélectionnée'; @@ -1246,6 +1259,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get map_title => 'Carte des nœuds'; + @override + String get map_lineOfSight => 'Ligne de vue'; + + @override + String get map_losScreenTitle => 'Ligne de vue'; + @override String get map_noNodesWithLocation => 'Aucun nœud avec des données de localisation'; @@ -2738,6 +2757,117 @@ class AppLocalizationsFr extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Effacer le chemin'; + @override + String get losSelectStartEnd => + 'Sélectionnez les nœuds de début et de fin pour LOS.'; + + @override + String losRunFailed(String error) { + return 'Échec de la vérification de la ligne de vue : $error'; + } + + @override + String get losClearAllPoints => 'Effacer tous les points'; + + @override + String get losRunToViewElevationProfile => + 'Exécutez LOS pour afficher le profil d\'altitude'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés'; + + @override + String get losShowDisplayNodes => 'Afficher les nœuds d\'affichage'; + + @override + String get losCustomPoints => 'Points personnalisés'; + + @override + String losCustomPointLabel(int index) { + return 'Personnalisé $index'; + } + + @override + String get losPointA => 'Point A'; + + @override + String get losPointB => 'Point B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenne A : $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenne B : $value $unit'; + } + + @override + String get losRun => 'Exécuter la LOS'; + + @override + String get losNoElevationData => 'Aucune donnée d\'altitude'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, LOS clair, clairance minimale $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloqué par $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS : vérification...'; + + @override + String get losStatusNoData => 'LOS : aucune donnée'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS : $clear/$total clair, $blocked bloqué, $unknown inconnu'; + } + + @override + String get losErrorElevationUnavailable => + 'Données d\'altitude indisponibles pour un ou plusieurs échantillons.'; + + @override + String get losErrorInvalidInput => + 'Données de points/d\'altitude non valides pour le calcul de la LOS.'; + + @override + String get losRenameCustomPoint => 'Renommer le point personnalisé'; + + @override + String get losPointName => 'Nom du point'; + + @override + String get losShowPanelTooltip => 'Afficher le panneau LOS'; + + @override + String get losHidePanelTooltip => 'Masquer le panneau LOS'; + + @override + String get losElevationAttribution => + 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a2b790f6..521cfb71 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -322,6 +322,10 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_aboutDescription => 'Un client Flutter open-source per i dispositivi di rete mesh LoRa Core di MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Dati di elevazione LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nome'; @@ -619,6 +623,15 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Cache Mappa Offline'; + @override + String get appSettings_unitsTitle => 'Unità'; + + @override + String get appSettings_unitsMetric => 'Metrico (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperiale (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Nessun\'area selezionata'; @@ -1239,6 +1252,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get map_title => 'Mappa Nodi'; + @override + String get map_lineOfSight => 'Linea di vista'; + + @override + String get map_losScreenTitle => 'Linea di vista'; + @override String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione'; @@ -2723,6 +2742,117 @@ class AppLocalizationsIt extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Pulisci percorso'; + @override + String get losSelectStartEnd => + 'Seleziona i nodi iniziali e finali per la LOS.'; + + @override + String losRunFailed(String error) { + return 'Controllo della linea di vista fallito: $error'; + } + + @override + String get losClearAllPoints => 'Cancella tutti i punti'; + + @override + String get losRunToViewElevationProfile => + 'Eseguire LOS per visualizzare il profilo altimetrico'; + + @override + String get losMenuTitle => 'Menù LOS'; + + @override + String get losMenuSubtitle => + 'Tocca i nodi o premi a lungo la mappa per punti personalizzati'; + + @override + String get losShowDisplayNodes => 'Mostra i nodi di visualizzazione'; + + @override + String get losCustomPoints => 'Punti personalizzati'; + + @override + String losCustomPointLabel(int index) { + return 'Personalizzato $index'; + } + + @override + String get losPointA => 'Punto A'; + + @override + String get losPointB => 'Punto B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenna A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenna B: $value $unit'; + } + + @override + String get losRun => 'Esegui LOS'; + + @override + String get losNoElevationData => 'Nessun dato di elevazione'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, libera LOS, distanza minima $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloccato da $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: controllo...'; + + @override + String get losStatusNoData => 'LOS: nessun dato'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total libera, $blocked bloccato, $unknown sconosciuto'; + } + + @override + String get losErrorElevationUnavailable => + 'Dati di elevazione non disponibili per uno o più campioni.'; + + @override + String get losErrorInvalidInput => + 'Dati punti/elevazione non validi per il calcolo della LOS.'; + + @override + String get losRenameCustomPoint => 'Rinomina punto personalizzato'; + + @override + String get losPointName => 'Nome del punto'; + + @override + String get losShowPanelTooltip => 'Mostra il pannello LOS'; + + @override + String get losHidePanelTooltip => 'Nascondi il pannello LOS'; + + @override + String get losElevationAttribution => + 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a958e795..a7a4c0b8 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -320,6 +320,10 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_aboutDescription => 'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Naam'; @@ -617,6 +621,15 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Kaarten Cache'; + @override + String get appSettings_unitsTitle => 'Eenheden'; + + @override + String get appSettings_unitsMetric => 'Metrisch (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperiaal (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Geen gebied geselecteerd'; @@ -1235,6 +1248,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get map_title => 'Node Map'; + @override + String get map_lineOfSight => 'Zichtlijn'; + + @override + String get map_losScreenTitle => 'Zichtlijn'; + @override String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens'; @@ -2714,6 +2733,117 @@ class AppLocalizationsNl extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Weg wissen'; + @override + String get losSelectStartEnd => + 'Selecteer begin- en eindknooppunten voor LOS.'; + + @override + String losRunFailed(String error) { + return 'Zichtlijncontrole mislukt: $error'; + } + + @override + String get losClearAllPoints => 'Wis alle punten'; + + @override + String get losRunToViewElevationProfile => + 'Voer LOS uit om het hoogteprofiel te bekijken'; + + @override + String get losMenuTitle => 'LOS-menu'; + + @override + String get losMenuSubtitle => + 'Tik op knooppunten of druk lang op de kaart voor aangepaste punten'; + + @override + String get losShowDisplayNodes => 'Toon weergaveknooppunten'; + + @override + String get losCustomPoints => 'Aangepaste punten'; + + @override + String losCustomPointLabel(int index) { + return 'Aangepast $index'; + } + + @override + String get losPointA => 'Punt A'; + + @override + String get losPointB => 'Punt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenne A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenne B: $value $unit'; + } + + @override + String get losRun => 'Voer LOS uit'; + + @override + String get losNoElevationData => 'Geen hoogtegegevens'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: controleren...'; + + @override + String get losStatusNoData => 'LOS: geen gegevens'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend'; + } + + @override + String get losErrorElevationUnavailable => + 'Hoogtegegevens niet beschikbaar voor een of meer monsters.'; + + @override + String get losErrorInvalidInput => + 'Ongeldige punten/hoogtegegevens voor LOS-berekening.'; + + @override + String get losRenameCustomPoint => 'Hernoem aangepast punt'; + + @override + String get losPointName => 'Puntnaam'; + + @override + String get losShowPanelTooltip => 'Toon LOS-paneel'; + + @override + String get losHidePanelTooltip => 'LOS-paneel verbergen'; + + @override + String get losElevationAttribution => + 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 55bc6ec7..88154720 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -323,6 +323,10 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_aboutDescription => 'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Imię'; @@ -621,6 +625,15 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Bufor Map Offline'; + @override + String get appSettings_unitsTitle => 'Jednostki'; + + @override + String get appSettings_unitsMetric => 'Metryczne (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperialne (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.'; @@ -1241,6 +1254,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get map_title => 'Mapa węzłów'; + @override + String get map_lineOfSight => 'Linia wzroku'; + + @override + String get map_losScreenTitle => 'Linia wzroku'; + @override String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi'; @@ -2721,6 +2740,116 @@ class AppLocalizationsPl extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Wyczyść ścieżkę'; + @override + String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.'; + + @override + String losRunFailed(String error) { + return 'Sprawdzenie pola widzenia nie powiodło się: $error'; + } + + @override + String get losClearAllPoints => 'Wyczyść wszystkie punkty'; + + @override + String get losRunToViewElevationProfile => + 'Uruchom LOS, aby wyświetlić profil wysokości'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty'; + + @override + String get losShowDisplayNodes => 'Pokaż węzły wyświetlające'; + + @override + String get losCustomPoints => 'Punkty niestandardowe'; + + @override + String losCustomPointLabel(int index) { + return 'Niestandardowe $index'; + } + + @override + String get losPointA => 'Punkt A'; + + @override + String get losPointB => 'Punkt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Uruchom LOS-a'; + + @override + String get losNoElevationData => 'Brak danych o wysokości'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: sprawdzam...'; + + @override + String get losStatusNoData => 'LOS: brak danych'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane'; + } + + @override + String get losErrorElevationUnavailable => + 'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.'; + + @override + String get losErrorInvalidInput => + 'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.'; + + @override + String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego'; + + @override + String get losPointName => 'Nazwa punktu'; + + @override + String get losShowPanelTooltip => 'Pokaż panel LOS'; + + @override + String get losHidePanelTooltip => 'Ukryj panel LOS'; + + @override + String get losElevationAttribution => + 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 596d268b..c7fc7073 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -324,6 +324,10 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_aboutDescription => 'Um cliente Flutter de código aberto para dispositivos de rede mesh LoRa Core da MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Dados de elevação LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Nome'; @@ -620,6 +624,15 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; + @override + String get appSettings_unitsTitle => 'Unidades'; + + @override + String get appSettings_unitsMetric => 'Métrico (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperial (ft/mi)'; + @override String get appSettings_noAreaSelected => 'Nenhuma área selecionada'; @@ -1240,6 +1253,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get map_title => 'Mapa de Nós'; + @override + String get map_lineOfSight => 'Linha de visão'; + + @override + String get map_losScreenTitle => 'Linha de visão'; + @override String get map_noNodesWithLocation => 'Não existem nós com dados de localização.'; @@ -2723,6 +2742,116 @@ class AppLocalizationsPt extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Limpar caminho'; + @override + String get losSelectStartEnd => 'Selecione nós iniciais e finais para LOS.'; + + @override + String losRunFailed(String error) { + return 'Falha na verificação da linha de visão: $error'; + } + + @override + String get losClearAllPoints => 'Limpe todos os pontos'; + + @override + String get losRunToViewElevationProfile => + 'Execute o LOS para visualizar o perfil de elevação'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados'; + + @override + String get losShowDisplayNodes => 'Mostrar nós de exibição'; + + @override + String get losCustomPoints => 'Pontos personalizados'; + + @override + String losCustomPointLabel(int index) { + return '$index personalizado'; + } + + @override + String get losPointA => 'Ponto A'; + + @override + String get losPointB => 'Ponto B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Executar LOS'; + + @override + String get losNoElevationData => 'Sem dados de elevação'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, limpar LOS, liberação mínima $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: verificando...'; + + @override + String get losStatusNoData => 'LOS: sem dados'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total limpo, $blocked bloqueado, $unknown desconhecido'; + } + + @override + String get losErrorElevationUnavailable => + 'Dados de elevação indisponíveis para uma ou mais amostras.'; + + @override + String get losErrorInvalidInput => + 'Dados de pontos/elevação inválidos para cálculo de LOS.'; + + @override + String get losRenameCustomPoint => 'Renomear ponto personalizado'; + + @override + String get losPointName => 'Nome do ponto'; + + @override + String get losShowPanelTooltip => 'Mostrar painel LOS'; + + @override + String get losHidePanelTooltip => 'Ocultar painel LOS'; + + @override + String get losElevationAttribution => + 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4647746a..2e992bd1 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -321,6 +321,10 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_aboutDescription => 'Открытое клиентское приложение на Flutter для устройств MeshCore с LoRa-сетями.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Данные о высоте LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Имя'; @@ -620,6 +624,15 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; + @override + String get appSettings_unitsTitle => 'Единицы'; + + @override + String get appSettings_unitsMetric => 'Метрическая (м/км)'; + + @override + String get appSettings_unitsImperial => 'Имперская (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Область не выбрана'; @@ -1242,6 +1255,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get map_title => 'Карта нод'; + @override + String get map_lineOfSight => 'Линия видимости'; + + @override + String get map_losScreenTitle => 'Линия видимости'; + @override String get map_noNodesWithLocation => 'Нет нод с данными о местоположении'; @@ -2726,6 +2745,116 @@ class AppLocalizationsRu extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Очистить путь'; + @override + String get losSelectStartEnd => 'Выберите начальный и конечный узлы для LOS.'; + + @override + String losRunFailed(String error) { + return 'Проверка прямой видимости не удалась: $error'; + } + + @override + String get losClearAllPoints => 'Очистить все точки'; + + @override + String get losRunToViewElevationProfile => + 'Запустите LOS, чтобы просмотреть профиль высот.'; + + @override + String get losMenuTitle => 'ЛОС Меню'; + + @override + String get losMenuSubtitle => + 'Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.'; + + @override + String get losShowDisplayNodes => 'Показать узлы отображения'; + + @override + String get losCustomPoints => 'Пользовательские точки'; + + @override + String losCustomPointLabel(int index) { + return 'Пользовательский $index'; + } + + @override + String get losPointA => 'Точка А'; + + @override + String get losPointB => 'Точка Б'; + + @override + String losAntennaA(String value, String unit) { + return 'Антенна А: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Антенна Б: $value $unit'; + } + + @override + String get losRun => 'Запустить ЛОС'; + + @override + String get losNoElevationData => 'Нет данных о высоте'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, свободная зона видимости, минимальный зазор $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, заблокирован $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'ЛОС: проверяю...'; + + @override + String get losStatusNoData => 'ЛОС: нет данных'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total очищено, $blocked заблокировано, $unknown неизвестно.'; + } + + @override + String get losErrorElevationUnavailable => + 'Данные о высоте недоступны для одного или нескольких образцов.'; + + @override + String get losErrorInvalidInput => + 'Неверные данные о точках/высоте для расчета LOS.'; + + @override + String get losRenameCustomPoint => 'Переименовать пользовательскую точку'; + + @override + String get losPointName => 'Имя точки'; + + @override + String get losShowPanelTooltip => 'Показать панель LOS'; + + @override + String get losHidePanelTooltip => 'Скрыть панель LOS'; + + @override + String get losElevationAttribution => + 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8e186631..a51e0591 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -320,6 +320,10 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_aboutDescription => 'Otvorený zdrojový Flutter klient pre MeshCore LoRa sieťové zariadenia.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Meno'; @@ -614,6 +618,15 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; + @override + String get appSettings_unitsTitle => 'Jednotky'; + + @override + String get appSettings_unitsMetric => 'Metrické (m / km)'; + + @override + String get appSettings_unitsImperial => 'Imperiálne (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť'; @@ -1236,6 +1249,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get map_title => 'Mapa uzlov'; + @override + String get map_lineOfSight => 'Line of Sight'; + + @override + String get map_losScreenTitle => 'Line of Sight'; + @override String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe'; @@ -2709,6 +2728,116 @@ class AppLocalizationsSk extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Zmazať cestu'; + @override + String get losSelectStartEnd => 'Vyberte počiatočný a koncový uzol pre LOS.'; + + @override + String losRunFailed(String error) { + return 'Kontrola priamej viditeľnosti zlyhala: $error'; + } + + @override + String get losClearAllPoints => 'Vymazať všetky body'; + + @override + String get losRunToViewElevationProfile => + 'Ak chcete zobraziť výškový profil, spustite LOS'; + + @override + String get losMenuTitle => 'Menu LOS'; + + @override + String get losMenuSubtitle => + 'Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body'; + + @override + String get losShowDisplayNodes => 'Zobraziť uzly zobrazenia'; + + @override + String get losCustomPoints => 'Vlastné body'; + + @override + String losCustomPointLabel(int index) { + return 'Vlastné $index'; + } + + @override + String get losPointA => 'Bod A'; + + @override + String get losPointB => 'Bod B'; + + @override + String losAntennaA(String value, String unit) { + return 'Anténa A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Anténa B: $value $unit'; + } + + @override + String get losRun => 'Spustite LOS'; + + @override + String get losNoElevationData => 'Žiadne údaje o nadmorskej výške'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, vymazať LOS, min. vôľa $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blokovaný $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: kontrolujem...'; + + @override + String get losStatusNoData => 'LOS: žiadne údaje'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total vymazané, $blocked blokované, $unknown neznáme'; + } + + @override + String get losErrorElevationUnavailable => + 'Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.'; + + @override + String get losErrorInvalidInput => + 'Neplatné body/údaje o nadmorskej výške pre výpočet LOS.'; + + @override + String get losRenameCustomPoint => 'Premenovať vlastný bod'; + + @override + String get losPointName => 'Názov bodu'; + + @override + String get losShowPanelTooltip => 'Zobraziť panel LOS'; + + @override + String get losHidePanelTooltip => 'Skryť panel LOS'; + + @override + String get losElevationAttribution => + 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index b95e7115..5ac7e8b4 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -319,6 +319,10 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_aboutDescription => 'Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Podatki o višini LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Ime'; @@ -615,6 +619,15 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; + @override + String get appSettings_unitsTitle => 'Enote'; + + @override + String get appSettings_unitsMetric => 'Metrična (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperialno (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Območje ni izbrano'; @@ -1231,6 +1244,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get map_title => 'Mapa omrežja'; + @override + String get map_lineOfSight => 'Linija vida'; + + @override + String get map_losScreenTitle => 'Linija vida'; + @override String get map_noNodesWithLocation => 'Nihče od notranjih elementov nima podatkov o lokaciji.'; @@ -2712,6 +2731,116 @@ class AppLocalizationsSl extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Počisti pot'; + @override + String get losSelectStartEnd => 'Izberite začetno in končno vozlišče za LOS.'; + + @override + String losRunFailed(String error) { + return 'Preverjanje vidnega polja ni uspelo: $error'; + } + + @override + String get losClearAllPoints => 'Počisti vse točke'; + + @override + String get losRunToViewElevationProfile => + 'Zaženite LOS za ogled višinskega profila'; + + @override + String get losMenuTitle => 'LOS meni'; + + @override + String get losMenuSubtitle => + 'Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri'; + + @override + String get losShowDisplayNodes => 'Pokaži prikazna vozlišča'; + + @override + String get losCustomPoints => 'Točke po meri'; + + @override + String losCustomPointLabel(int index) { + return 'Po meri $index'; + } + + @override + String get losPointA => 'Točka A'; + + @override + String get losPointB => 'Točka B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antena A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antena B: $value $unit'; + } + + @override + String get losRun => 'Zaženi LOS'; + + @override + String get losNoElevationData => 'Ni podatkov o višini'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, čisti LOS, najmanjša razdalja $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blokiral $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: preverjam ...'; + + @override + String get losStatusNoData => 'LOS: ni podatkov'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total jasno, $blocked blokirano, $unknown neznano'; + } + + @override + String get losErrorElevationUnavailable => + 'Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.'; + + @override + String get losErrorInvalidInput => + 'Neveljavni podatki o točkah/višini za izračun LOS.'; + + @override + String get losRenameCustomPoint => 'Preimenujte točko po meri'; + + @override + String get losPointName => 'Ime točke'; + + @override + String get losShowPanelTooltip => 'Pokaži ploščo LOS'; + + @override + String get losHidePanelTooltip => 'Skrij ploščo LOS'; + + @override + String get losElevationAttribution => + 'Podatki o višini: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 10047ca8..6d355d91 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -317,6 +317,10 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_aboutDescription => 'En öppen källkods Flutter-klient för MeshCore LoRa meshnätverksenheter.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS-höjddata: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Namn'; @@ -610,6 +614,15 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Offline Kartcache'; + @override + String get appSettings_unitsTitle => 'Enheter'; + + @override + String get appSettings_unitsMetric => 'Metriskt (m/km)'; + + @override + String get appSettings_unitsImperial => 'Imperialt (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Ingen area markerad'; @@ -1228,6 +1241,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get map_title => 'Nodkarta'; + @override + String get map_lineOfSight => 'Synlinje'; + + @override + String get map_losScreenTitle => 'Synlinje'; + @override String get map_noNodesWithLocation => 'Inga noder med platsinformation'; @@ -2697,6 +2716,114 @@ class AppLocalizationsSv extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Rensa väg'; + @override + String get losSelectStartEnd => 'Välj start- och slutnoder för LOS.'; + + @override + String losRunFailed(String error) { + return 'Synlinjekontroll misslyckades: $error'; + } + + @override + String get losClearAllPoints => 'Rensa alla punkter'; + + @override + String get losRunToViewElevationProfile => 'Kör LOS för att se höjdprofil'; + + @override + String get losMenuTitle => 'LOS-menyn'; + + @override + String get losMenuSubtitle => + 'Tryck på noder eller tryck länge på kartan för anpassade punkter'; + + @override + String get losShowDisplayNodes => 'Visa displaynoder'; + + @override + String get losCustomPoints => 'Anpassade poäng'; + + @override + String losCustomPointLabel(int index) { + return 'Anpassad $index'; + } + + @override + String get losPointA => 'Punkt A'; + + @override + String get losPointB => 'Punkt B'; + + @override + String losAntennaA(String value, String unit) { + return 'Antenn A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Antenn B: $value $unit'; + } + + @override + String get losRun => 'Kör LOS'; + + @override + String get losNoElevationData => 'Inga höjddata'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, rensa LOS, min clearance $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, blockerad av $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: kollar...'; + + @override + String get losStatusNoData => 'LOS: inga data'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total rensa, $blocked blockerad, $unknown okänd'; + } + + @override + String get losErrorElevationUnavailable => + 'Höjddata är inte tillgänglig för ett eller flera prover.'; + + @override + String get losErrorInvalidInput => + 'Ogiltiga poäng/höjddata för LOS-beräkning.'; + + @override + String get losRenameCustomPoint => 'Byt namn på anpassad punkt'; + + @override + String get losPointName => 'Punktnamn'; + + @override + String get losShowPanelTooltip => 'Visa LOS-panelen'; + + @override + String get losHidePanelTooltip => 'Dölj LOS-panelen'; + + @override + String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 9edc64a6..0f3d550c 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -322,6 +322,10 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_aboutDescription => 'Клієнт Flutter з відкритим вихідним кодом для пристроїв мережі MeshCore LoRa.'; + @override + String get settings_aboutOpenMeteoAttribution => + 'Дані про висоту LOS: Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => 'Ім\'я'; @@ -618,6 +622,15 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; + @override + String get appSettings_unitsTitle => 'одиниці'; + + @override + String get appSettings_unitsMetric => 'Метричний (м / км)'; + + @override + String get appSettings_unitsImperial => 'Імперська (ft / mi)'; + @override String get appSettings_noAreaSelected => 'Область не вибрано'; @@ -1240,6 +1253,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get map_title => 'Карта вузлів'; + @override + String get map_lineOfSight => 'Пряма видимість'; + + @override + String get map_losScreenTitle => 'Пряма видимість'; + @override String get map_noNodesWithLocation => 'Немає вузлів з даними про розташування'; @@ -2733,6 +2752,117 @@ class AppLocalizationsUk extends AppLocalizations { @override String get pathTrace_clearTooltip => 'Очистити шлях'; + @override + String get losSelectStartEnd => + 'Виберіть початковий і кінцевий вузли для LOS.'; + + @override + String losRunFailed(String error) { + return 'Помилка перевірки прямої видимості: $error'; + } + + @override + String get losClearAllPoints => 'Очистити всі пункти'; + + @override + String get losRunToViewElevationProfile => + 'Запустіть LOS, щоб переглянути профіль висоти'; + + @override + String get losMenuTitle => 'Меню LOS'; + + @override + String get losMenuSubtitle => + 'Торкніться вузлів або утримуйте карту, щоб отримати власні точки'; + + @override + String get losShowDisplayNodes => 'Показати вузли відображення'; + + @override + String get losCustomPoints => 'Користувальницькі точки'; + + @override + String losCustomPointLabel(int index) { + return 'Спеціальний $index'; + } + + @override + String get losPointA => 'Точка А'; + + @override + String get losPointB => 'Точка Б'; + + @override + String losAntennaA(String value, String unit) { + return 'Антена A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return 'Антена B: $value $unit'; + } + + @override + String get losRun => 'Запустіть LOS'; + + @override + String get losNoElevationData => 'Немає даних про висоту'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit, чистий LOS, мінімальний зазор $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit, заблоковано $obstruction $heightUnit'; + } + + @override + String get losStatusChecking => 'LOS: перевірка...'; + + @override + String get losStatusNoData => 'LOS: немає даних'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS: $clear/$total очищено, $blocked заблоковано, $unknown невідомо'; + } + + @override + String get losErrorElevationUnavailable => + 'Дані про висоту недоступні для одного чи кількох зразків.'; + + @override + String get losErrorInvalidInput => + 'Недійсні дані про точки/висоту для розрахунку LOS.'; + + @override + String get losRenameCustomPoint => 'Перейменуйте спеціальну точку'; + + @override + String get losPointName => 'Назва точки'; + + @override + String get losShowPanelTooltip => 'Показати панель LOS'; + + @override + String get losHidePanelTooltip => 'Приховати панель LOS'; + + @override + String get losElevationAttribution => + 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9753da6e..36a114a5 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -307,6 +307,10 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_aboutDescription => '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; + @override + String get settings_aboutOpenMeteoAttribution => + 'LOS 高程数据:Open-Meteo (CC BY 4.0)'; + @override String get settings_infoName => '姓名'; @@ -585,6 +589,15 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_offlineMapCache => '离线地图缓存'; + @override + String get appSettings_unitsTitle => '单位'; + + @override + String get appSettings_unitsMetric => '公制(米/公里)'; + + @override + String get appSettings_unitsImperial => '英制 (ft / mi)'; + @override String get appSettings_noAreaSelected => '未选择任何区域'; @@ -1182,6 +1195,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get map_title => '节点图'; + @override + String get map_lineOfSight => '视线'; + + @override + String get map_losScreenTitle => '视线'; + @override String get map_noNodesWithLocation => '没有包含位置信息的节点'; @@ -2579,6 +2598,111 @@ class AppLocalizationsZh extends AppLocalizations { @override String get pathTrace_clearTooltip => '清除路径'; + @override + String get losSelectStartEnd => '选择 LOS 的起始节点和结束节点。'; + + @override + String losRunFailed(String error) { + return '视线检查失败:$error'; + } + + @override + String get losClearAllPoints => '清除所有点'; + + @override + String get losRunToViewElevationProfile => '运行 LOS 查看高程剖面'; + + @override + String get losMenuTitle => '服务水平菜单'; + + @override + String get losMenuSubtitle => '点击节点或长按地图以获取自定义点'; + + @override + String get losShowDisplayNodes => '显示显示节点'; + + @override + String get losCustomPoints => '自定义积分'; + + @override + String losCustomPointLabel(int index) { + return '自定义 $index'; + } + + @override + String get losPointA => 'A点'; + + @override + String get losPointB => 'B点'; + + @override + String losAntennaA(String value, String unit) { + return '天线 A: $value $unit'; + } + + @override + String losAntennaB(String value, String unit) { + return '天线 B:$value $unit'; + } + + @override + String get losRun => '运行视距'; + + @override + String get losNoElevationData => '无海拔数据'; + + @override + String losProfileClear( + String distance, + String distanceUnit, + String clearance, + String heightUnit, + ) { + return '$distance $distanceUnit,清除 LOS,最小间隙 $clearance $heightUnit'; + } + + @override + String losProfileBlocked( + String distance, + String distanceUnit, + String obstruction, + String heightUnit, + ) { + return '$distance $distanceUnit,被 $obstruction $heightUnit 阻止'; + } + + @override + String get losStatusChecking => '洛斯:正在检查...'; + + @override + String get losStatusNoData => 'LOS:无数据'; + + @override + String losStatusSummary(int clear, int total, int blocked, int unknown) { + return 'LOS:$clear/$total 清除,$blocked 阻塞,$unknown 未知'; + } + + @override + String get losErrorElevationUnavailable => '一个或多个样本的海拔数据不可用。'; + + @override + String get losErrorInvalidInput => '用于 LOS 计算的点/高程数据无效。'; + + @override + String get losRenameCustomPoint => '重命名自定义点'; + + @override + String get losPointName => '点名称'; + + @override + String get losShowPanelTooltip => '显示 LOS 面板'; + + @override + String get losHidePanelTooltip => '隐藏 LOS 面板'; + + @override + String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 859e48d9..733e4dce 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Toon alle paden", "settings_clientRepeat": "Herhalen: Afgekoppeld", "settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.", - "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist." + "settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.", + "settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Eenheden", + "appSettings_unitsMetric": "Metrisch (m / km)", + "appSettings_unitsImperial": "Imperiaal (ft / mi)", + "map_lineOfSight": "Zichtlijn", + "map_losScreenTitle": "Zichtlijn", + "losSelectStartEnd": "Selecteer begin- en eindknooppunten voor LOS.", + "losRunFailed": "Zichtlijncontrole mislukt: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Wis alle punten", + "losRunToViewElevationProfile": "Voer LOS uit om het hoogteprofiel te bekijken", + "losMenuTitle": "LOS-menu", + "losMenuSubtitle": "Tik op knooppunten of druk lang op de kaart voor aangepaste punten", + "losShowDisplayNodes": "Toon weergaveknooppunten", + "losCustomPoints": "Aangepaste punten", + "losCustomPointLabel": "Aangepast {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punt A", + "losPointB": "Punt B", + "losAntennaA": "Antenne A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenne B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Voer LOS uit", + "losNoElevationData": "Geen hoogtegegevens", + "losProfileClear": "{distance} {distanceUnit}, vrije LOS, min. vrije ruimte {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, geblokkeerd door {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: controleren...", + "losStatusNoData": "LOS: geen gegevens", + "losStatusSummary": "LOS: {clear}/{total} gewist, {blocked} geblokkeerd, {unknown} onbekend", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Hoogtegegevens niet beschikbaar voor een of meer monsters.", + "losErrorInvalidInput": "Ongeldige punten/hoogtegegevens voor LOS-berekening.", + "losRenameCustomPoint": "Hernoem aangepast punt", + "losPointName": "Puntnaam", + "losShowPanelTooltip": "Toon LOS-paneel", + "losHidePanelTooltip": "LOS-paneel verbergen", + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d03b9119..35efee18 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Pokaż wszystkie ścieżki", "settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.", "settings_clientRepeat": "Powtórzenie: Niezależne od sieci", - "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz." + "settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.", + "settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Jednostki", + "appSettings_unitsMetric": "Metryczne (m / km)", + "appSettings_unitsImperial": "Imperialne (ft / mi)", + "map_lineOfSight": "Linia wzroku", + "map_losScreenTitle": "Linia wzroku", + "losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.", + "losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Wyczyść wszystkie punkty", + "losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty", + "losShowDisplayNodes": "Pokaż węzły wyświetlające", + "losCustomPoints": "Punkty niestandardowe", + "losCustomPointLabel": "Niestandardowe {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punkt A", + "losPointB": "Punkt B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Uruchom LOS-a", + "losNoElevationData": "Brak danych o wysokości", + "losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: sprawdzam...", + "losStatusNoData": "LOS: brak danych", + "losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.", + "losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.", + "losRenameCustomPoint": "Zmień nazwę punktu niestandardowego", + "losPointName": "Nazwa punktu", + "losShowPanelTooltip": "Pokaż panel LOS", + "losHidePanelTooltip": "Ukryj panel LOS", + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 83a77197..fd742d94 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Mostrar todos os caminhos", "settings_clientRepeatFreqWarning": "A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.", "settings_clientRepeat": "Repetição sem rede", - "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos." + "settings_clientRepeatSubtitle": "Permita que este dispositivo repita pacotes de rede para outros dispositivos.", + "settings_aboutOpenMeteoAttribution": "Dados de elevação LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Unidades", + "appSettings_unitsMetric": "Métrico (m/km)", + "appSettings_unitsImperial": "Imperial (ft/mi)", + "map_lineOfSight": "Linha de visão", + "map_losScreenTitle": "Linha de visão", + "losSelectStartEnd": "Selecione nós iniciais e finais para LOS.", + "losRunFailed": "Falha na verificação da linha de visão: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Limpe todos os pontos", + "losRunToViewElevationProfile": "Execute o LOS para visualizar o perfil de elevação", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Toque nos nós ou mantenha pressionado o mapa para obter pontos personalizados", + "losShowDisplayNodes": "Mostrar nós de exibição", + "losCustomPoints": "Pontos personalizados", + "losCustomPointLabel": "{index} personalizado", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Ponto A", + "losPointB": "Ponto B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Executar LOS", + "losNoElevationData": "Sem dados de elevação", + "losProfileClear": "{distance} {distanceUnit}, limpar LOS, liberação mínima {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, bloqueado por {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: verificando...", + "losStatusNoData": "LOS: sem dados", + "losStatusSummary": "LOS: {clear}/{total} limpo, {blocked} bloqueado, {unknown} desconhecido", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Dados de elevação indisponíveis para uma ou mais amostras.", + "losErrorInvalidInput": "Dados de pontos/elevação inválidos para cálculo de LOS.", + "losRenameCustomPoint": "Renomear ponto personalizado", + "losPointName": "Nome do ponto", + "losShowPanelTooltip": "Mostrar painel LOS", + "losHidePanelTooltip": "Ocultar painel LOS", + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 380ba10b..04b2e049 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -839,5 +839,120 @@ "chat_ShowAllPaths": "Показать все пути", "settings_clientRepeatFreqWarning": "Для работы в режиме \"без подключения к сети\" требуется частота 433, 869 или 918 МГц.", "settings_clientRepeatSubtitle": "Позвольте этому устройству повторять пакеты данных для других устройств.", - "settings_clientRepeat": "Повторение \"вне сети\"" + "settings_clientRepeat": "Повторение \"вне сети\"", + "settings_aboutOpenMeteoAttribution": "Данные о высоте LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Единицы", + "appSettings_unitsMetric": "Метрическая (м/км)", + "appSettings_unitsImperial": "Имперская (ft / mi)", + "map_lineOfSight": "Линия видимости", + "map_losScreenTitle": "Линия видимости", + "losSelectStartEnd": "Выберите начальный и конечный узлы для LOS.", + "losRunFailed": "Проверка прямой видимости не удалась: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Очистить все точки", + "losRunToViewElevationProfile": "Запустите LOS, чтобы просмотреть профиль высот.", + "losMenuTitle": "ЛОС Меню", + "losMenuSubtitle": "Коснитесь узлов или нажмите и удерживайте карту для выбора пользовательских точек.", + "losShowDisplayNodes": "Показать узлы отображения", + "losCustomPoints": "Пользовательские точки", + "losCustomPointLabel": "Пользовательский {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антенна А: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Антенна Б: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Запустить ЛОС", + "losNoElevationData": "Нет данных о высоте", + "losProfileClear": "{distance} {distanceUnit}, свободная зона видимости, минимальный зазор {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, заблокирован {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "ЛОС: проверяю...", + "losStatusNoData": "ЛОС: нет данных", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблокировано, {unknown} неизвестно.", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Данные о высоте недоступны для одного или нескольких образцов.", + "losErrorInvalidInput": "Неверные данные о точках/высоте для расчета LOS.", + "losRenameCustomPoint": "Переименовать пользовательскую точку", + "losPointName": "Имя точки", + "losShowPanelTooltip": "Показать панель LOS", + "losHidePanelTooltip": "Скрыть панель LOS", + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index aca4a29b..6663094a 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Zobraziť všetky cesty", "settings_clientRepeat": "Opätovné použitie bez elektrickej siete", "settings_clientRepeatFreqWarning": "Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.", - "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných." + "settings_clientRepeatSubtitle": "Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.", + "settings_aboutOpenMeteoAttribution": "Údaje o nadmorskej výške LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Jednotky", + "appSettings_unitsMetric": "Metrické (m / km)", + "appSettings_unitsImperial": "Imperiálne (ft / mi)", + "map_lineOfSight": "Line of Sight", + "map_losScreenTitle": "Line of Sight", + "losSelectStartEnd": "Vyberte počiatočný a koncový uzol pre LOS.", + "losRunFailed": "Kontrola priamej viditeľnosti zlyhala: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Vymazať všetky body", + "losRunToViewElevationProfile": "Ak chcete zobraziť výškový profil, spustite LOS", + "losMenuTitle": "Menu LOS", + "losMenuSubtitle": "Klepnutím na uzly alebo dlhým stlačením mapy získate vlastné body", + "losShowDisplayNodes": "Zobraziť uzly zobrazenia", + "losCustomPoints": "Vlastné body", + "losCustomPointLabel": "Vlastné {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Bod A", + "losPointB": "Bod B", + "losAntennaA": "Anténa A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Anténa B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Spustite LOS", + "losNoElevationData": "Žiadne údaje o nadmorskej výške", + "losProfileClear": "{distance} {distanceUnit}, vymazať LOS, min. vôľa {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blokovaný {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: kontrolujem...", + "losStatusNoData": "LOS: žiadne údaje", + "losStatusSummary": "LOS: {clear}/{total} vymazané, {blocked} blokované, {unknown} neznáme", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Údaje o nadmorskej výške nie sú k dispozícii pre jednu alebo viacero vzoriek.", + "losErrorInvalidInput": "Neplatné body/údaje o nadmorskej výške pre výpočet LOS.", + "losRenameCustomPoint": "Premenovať vlastný bod", + "losPointName": "Názov bodu", + "losShowPanelTooltip": "Zobraziť panel LOS", + "losHidePanelTooltip": "Skryť panel LOS", + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 59b84342..50a90432 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Prikaži vse poti", "settings_clientRepeatFreqWarning": "Za ponovni prenos na brezžični način so potrebne frekvence 433, 869 ali 918 MHz.", "settings_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.", - "settings_clientRepeat": "Neovadno ponavljanje" + "settings_clientRepeat": "Neovadno ponavljanje", + "settings_aboutOpenMeteoAttribution": "Podatki o višini LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Enote", + "appSettings_unitsMetric": "Metrična (m/km)", + "appSettings_unitsImperial": "Imperialno (ft / mi)", + "map_lineOfSight": "Linija vida", + "map_losScreenTitle": "Linija vida", + "losSelectStartEnd": "Izberite začetno in končno vozlišče za LOS.", + "losRunFailed": "Preverjanje vidnega polja ni uspelo: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Počisti vse točke", + "losRunToViewElevationProfile": "Zaženite LOS za ogled višinskega profila", + "losMenuTitle": "LOS meni", + "losMenuSubtitle": "Tapnite vozlišča ali dolgo pritisnite na zemljevid za točke po meri", + "losShowDisplayNodes": "Pokaži prikazna vozlišča", + "losCustomPoints": "Točke po meri", + "losCustomPointLabel": "Po meri {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Točka A", + "losPointB": "Točka B", + "losAntennaA": "Antena A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antena B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Zaženi LOS", + "losNoElevationData": "Ni podatkov o višini", + "losProfileClear": "{distance} {distanceUnit}, čisti LOS, najmanjša razdalja {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blokiral {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: preverjam ...", + "losStatusNoData": "LOS: ni podatkov", + "losStatusSummary": "LOS: {clear}/{total} jasno, {blocked} blokirano, {unknown} neznano", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Podatki o nadmorski višini niso na voljo za enega ali več vzorcev.", + "losErrorInvalidInput": "Neveljavni podatki o točkah/višini za izračun LOS.", + "losRenameCustomPoint": "Preimenujte točko po meri", + "losPointName": "Ime točke", + "losShowPanelTooltip": "Pokaži ploščo LOS", + "losHidePanelTooltip": "Skrij ploščo LOS", + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index fa786f70..260a34b8 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Visa alla vägar", "settings_clientRepeatSubtitle": "Låt enheten repetera nätpaket för andra användare.", "settings_clientRepeat": "Upprepa utan elnät", - "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz." + "settings_clientRepeatFreqWarning": "För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.", + "settings_aboutOpenMeteoAttribution": "LOS-höjddata: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "Enheter", + "appSettings_unitsMetric": "Metriskt (m/km)", + "appSettings_unitsImperial": "Imperialt (ft / mi)", + "map_lineOfSight": "Synlinje", + "map_losScreenTitle": "Synlinje", + "losSelectStartEnd": "Välj start- och slutnoder för LOS.", + "losRunFailed": "Synlinjekontroll misslyckades: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Rensa alla punkter", + "losRunToViewElevationProfile": "Kör LOS för att se höjdprofil", + "losMenuTitle": "LOS-menyn", + "losMenuSubtitle": "Tryck på noder eller tryck länge på kartan för anpassade punkter", + "losShowDisplayNodes": "Visa displaynoder", + "losCustomPoints": "Anpassade poäng", + "losCustomPointLabel": "Anpassad {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Punkt A", + "losPointB": "Punkt B", + "losAntennaA": "Antenn A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Antenn B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Kör LOS", + "losNoElevationData": "Inga höjddata", + "losProfileClear": "{distance} {distanceUnit}, rensa LOS, min clearance {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, blockerad av {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: kollar...", + "losStatusNoData": "LOS: inga data", + "losStatusSummary": "LOS: {clear}/{total} rensa, {blocked} blockerad, {unknown} okänd", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Höjddata är inte tillgänglig för ett eller flera prover.", + "losErrorInvalidInput": "Ogiltiga poäng/höjddata för LOS-beräkning.", + "losRenameCustomPoint": "Byt namn på anpassad punkt", + "losPointName": "Punktnamn", + "losShowPanelTooltip": "Visa LOS-panelen", + "losHidePanelTooltip": "Dölj LOS-panelen", + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3f7b276f..ec414b48 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "Показати всі шляхи", "settings_clientRepeatFreqWarning": "Повтор без підключення до мережі вимагає частоти 433, 869 або 918 МГц.", "settings_clientRepeatSubtitle": "Дозвольте цьому пристрою повторювати пакети даних для інших пристроїв.", - "settings_clientRepeat": "Автономна система" + "settings_clientRepeat": "Автономна система", + "settings_aboutOpenMeteoAttribution": "Дані про висоту LOS: Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "одиниці", + "appSettings_unitsMetric": "Метричний (м / км)", + "appSettings_unitsImperial": "Імперська (ft / mi)", + "map_lineOfSight": "Пряма видимість", + "map_losScreenTitle": "Пряма видимість", + "losSelectStartEnd": "Виберіть початковий і кінцевий вузли для LOS.", + "losRunFailed": "Помилка перевірки прямої видимості: {error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "Очистити всі пункти", + "losRunToViewElevationProfile": "Запустіть LOS, щоб переглянути профіль висоти", + "losMenuTitle": "Меню LOS", + "losMenuSubtitle": "Торкніться вузлів або утримуйте карту, щоб отримати власні точки", + "losShowDisplayNodes": "Показати вузли відображення", + "losCustomPoints": "Користувальницькі точки", + "losCustomPointLabel": "Спеціальний {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "Точка А", + "losPointB": "Точка Б", + "losAntennaA": "Антена A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "Антена B: {value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "Запустіть LOS", + "losNoElevationData": "Немає даних про висоту", + "losProfileClear": "{distance} {distanceUnit}, чистий LOS, мінімальний зазор {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit}, заблоковано {obstruction} {heightUnit}", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "LOS: перевірка...", + "losStatusNoData": "LOS: немає даних", + "losStatusSummary": "LOS: {clear}/{total} очищено, {blocked} заблоковано, {unknown} невідомо", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "Дані про висоту недоступні для одного чи кількох зразків.", + "losErrorInvalidInput": "Недійсні дані про точки/висоту для розрахунку LOS.", + "losRenameCustomPoint": "Перейменуйте спеціальну точку", + "losPointName": "Назва точки", + "losShowPanelTooltip": "Показати панель LOS", + "losHidePanelTooltip": "Приховати панель LOS", + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index bc433927..6b072c9c 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1599,5 +1599,120 @@ "chat_ShowAllPaths": "显示所有路径", "settings_clientRepeat": "离网重复", "settings_clientRepeatSubtitle": "允许此设备重复发送网状数据包给其他设备", - "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。" + "settings_clientRepeatFreqWarning": "离网重复通信需要使用 433、869 或 918 兆赫兹的频率。", + "settings_aboutOpenMeteoAttribution": "LOS 高程数据:Open-Meteo (CC BY 4.0)", + "appSettings_unitsTitle": "单位", + "appSettings_unitsMetric": "公制(米/公里)", + "appSettings_unitsImperial": "英制 (ft / mi)", + "map_lineOfSight": "视线", + "map_losScreenTitle": "视线", + "losSelectStartEnd": "选择 LOS 的起始节点和结束节点。", + "losRunFailed": "视线检查失败:{error}", + "@losRunFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "losClearAllPoints": "清除所有点", + "losRunToViewElevationProfile": "运行 LOS 查看高程剖面", + "losMenuTitle": "服务水平菜单", + "losMenuSubtitle": "点击节点或长按地图以获取自定义点", + "losShowDisplayNodes": "显示显示节点", + "losCustomPoints": "自定义积分", + "losCustomPointLabel": "自定义 {index}", + "@losCustomPointLabel": { + "placeholders": { + "index": { + "type": "int" + } + } + }, + "losPointA": "A点", + "losPointB": "B点", + "losAntennaA": "天线 A: {value} {unit}", + "@losAntennaA": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losAntennaB": "天线 B:{value} {unit}", + "@losAntennaB": { + "placeholders": { + "value": { + "type": "String" + }, + "unit": { + "type": "String" + } + } + }, + "losRun": "运行视距", + "losNoElevationData": "无海拔数据", + "losProfileClear": "{distance} {distanceUnit},清除 LOS,最小间隙 {clearance} {heightUnit}", + "@losProfileClear": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "clearance": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losProfileBlocked": "{distance} {distanceUnit},被 {obstruction} {heightUnit} 阻止", + "@losProfileBlocked": { + "placeholders": { + "distance": { + "type": "String" + }, + "distanceUnit": { + "type": "String" + }, + "obstruction": { + "type": "String" + }, + "heightUnit": { + "type": "String" + } + } + }, + "losStatusChecking": "洛斯:正在检查...", + "losStatusNoData": "LOS:无数据", + "losStatusSummary": "LOS:{clear}/{total} 清除,{blocked} 阻塞,{unknown} 未知", + "@losStatusSummary": { + "placeholders": { + "clear": { + "type": "int" + }, + "total": { + "type": "int" + }, + "blocked": { + "type": "int" + }, + "unknown": { + "type": "int" + } + } + }, + "losErrorElevationUnavailable": "一个或多个样本的海拔数据不可用。", + "losErrorInvalidInput": "用于 LOS 计算的点/高程数据无效。", + "losRenameCustomPoint": "重命名自定义点", + "losPointName": "点名称", + "losShowPanelTooltip": "显示 LOS 面板", + "losHidePanelTooltip": "隐藏 LOS 面板", + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" } diff --git a/lib/main.dart b/lib/main.dart index 8ee0ca47..3650a7e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter/foundation.dart'; import 'l10n/app_localizations.dart'; import 'package:provider/provider.dart'; @@ -47,6 +48,7 @@ void main() async { final notificationService = NotificationService(); await notificationService.initialize(); await backgroundService.initialize(); + _registerThirdPartyLicenses(); // Wire up connector with services connector.initialize( @@ -80,6 +82,27 @@ void main() async { ); } +void _registerThirdPartyLicenses() { + LicenseRegistry.addLicense(() async* { + yield const LicenseEntryWithLineBreaks( + ['Open-Meteo Elevation API Data'], + ''' +Data used by LOS elevation lookups is provided by Open-Meteo. + +Open-Meteo terms and attribution: +https://open-meteo.com/en/terms + +Elevation API: +https://open-meteo.com/en/docs/elevation-api + +Attribution license reference: +Creative Commons Attribution 4.0 International (CC BY 4.0) +https://creativecommons.org/licenses/by/4.0/ +''', + ); + }); +} + class MeshCoreApp extends StatelessWidget { final MeshCoreConnector connector; final MessageRetryService retryService; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 3edb68fa..229a7a64 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -1,3 +1,16 @@ +enum UnitSystem { metric, imperial } + +extension UnitSystemValue on UnitSystem { + String get value { + switch (this) { + case UnitSystem.imperial: + return 'imperial'; + case UnitSystem.metric: + return 'metric'; + } + } +} + class AppSettings { static const Object _unset = Object(); @@ -21,6 +34,7 @@ class AppSettings { final String? languageOverride; // null = system default final bool appDebugLogEnabled; final Map batteryChemistryByDeviceId; + final UnitSystem unitSystem; AppSettings({ this.clearPathOnMaxRetry = false, @@ -43,6 +57,7 @@ class AppSettings { this.languageOverride, this.appDebugLogEnabled = false, Map? batteryChemistryByDeviceId, + this.unitSystem = UnitSystem.metric, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}; Map toJson() { @@ -67,10 +82,18 @@ class AppSettings { 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, + 'unit_system': unitSystem.value, }; } factory AppSettings.fromJson(Map json) { + UnitSystem parseUnitSystem(dynamic value) { + if (value is String && value.toLowerCase() == 'imperial') { + return UnitSystem.imperial; + } + return UnitSystem.metric; + } + return AppSettings( clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false, mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true, @@ -101,6 +124,9 @@ class AppSettings { (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, + unitSystem: parseUnitSystem( + json['unit_system'] ?? json['los_unit_system'], + ), ); } @@ -125,6 +151,7 @@ class AppSettings { Object? languageOverride = _unset, bool? appDebugLogEnabled, Map? batteryChemistryByDeviceId, + UnitSystem? unitSystem, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -154,6 +181,7 @@ class AppSettings { appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, + unitSystem: unitSystem ?? this.unitSystem, ); } } diff --git a/lib/screens/app_debug_log_screen.dart b/lib/screens/app_debug_log_screen.dart index 5372ea83..48770388 100644 --- a/lib/screens/app_debug_log_screen.dart +++ b/lib/screens/app_debug_log_screen.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../services/app_debug_log_service.dart'; +import '../widgets/adaptive_app_bar_title.dart'; class AppDebugLogScreen extends StatelessWidget { const AppDebugLogScreen({super.key}); @@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(context.l10n.debugLog_appTitle), + title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle), centerTitle: true, actions: [ IconButton( diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 4e317335..b309b4db 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -3,8 +3,10 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../models/app_settings.dart'; import '../services/app_settings_service.dart'; import '../services/notification_service.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import 'map_cache_screen.dart'; class AppSettingsScreen extends StatelessWidget { @@ -14,7 +16,7 @@ class AppSettingsScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.appSettings_title), + title: AdaptiveAppBarTitle(context.l10n.appSettings_title), centerTitle: true, ), body: SafeArea( @@ -360,6 +362,18 @@ class AppSettingsScreen extends StatelessWidget { onTap: () => _showTimeFilterDialog(context, settingsService), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.straighten), + title: Text(context.l10n.appSettings_unitsTitle), + subtitle: Text( + settingsService.settings.unitSystem == UnitSystem.imperial + ? context.l10n.appSettings_unitsImperial + : context.l10n.appSettings_unitsMetric, + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => _showUnitsDialog(context, settingsService), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.download_outlined), title: Text(context.l10n.appSettings_offlineMapCache), @@ -706,6 +720,46 @@ class AppSettingsScreen extends StatelessWidget { ); } + void _showUnitsDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.appSettings_unitsTitle), + content: RadioGroup( + groupValue: settingsService.settings.unitSystem, + onChanged: (value) { + if (value != null) { + settingsService.setUnitSystem(value); + Navigator.pop(context); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(context.l10n.appSettings_unitsMetric), + leading: const Radio(value: UnitSystem.metric), + ), + ListTile( + title: Text(context.l10n.appSettings_unitsImperial), + leading: const Radio(value: UnitSystem.imperial), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_close), + ), + ], + ), + ); + } + Widget _buildDebugCard( BuildContext context, AppSettingsService settingsService, diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart index 7cebb761..88f734bc 100644 --- a/lib/screens/ble_debug_log_screen.dart +++ b/lib/screens/ble_debug_log_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import '../l10n/l10n.dart'; import '../services/ble_debug_log_service.dart'; import '../connector/meshcore_protocol.dart'; +import '../widgets/adaptive_app_bar_title.dart'; enum _BleLogView { frames, rawLogRx } @@ -29,7 +30,7 @@ class _BleDebugLogScreenState extends State { : rawEntries.isNotEmpty; return Scaffold( appBar: AppBar( - title: Text(context.l10n.debugLog_bleTitle), + title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle), actions: [ IconButton( tooltip: context.l10n.debugLog_copyLog, diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index e6fcacce..2d1faa3f 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -9,11 +9,14 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../services/map_tile_cache_service.dart'; +import '../services/app_settings_service.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; import '../models/channel_message.dart'; +import '../models/app_settings.dart'; import '../models/contact.dart'; +import '../widgets/adaptive_app_bar_title.dart'; class ChannelMessagePathScreen extends StatelessWidget { final ChannelMessage message; @@ -48,7 +51,7 @@ class ChannelMessagePathScreen extends StatelessWidget { final extraPaths = _otherPaths(primaryPath, message.pathVariants); return Scaffold( appBar: AppBar( - title: Text(l10n.channelPath_title), + title: AdaptiveAppBarTitle(l10n.channelPath_title), actions: [ IconButton( icon: const Icon(Icons.radar_outlined), @@ -297,8 +300,12 @@ class ChannelMessagePathMapScreen extends StatefulWidget { class _ChannelMessagePathMapScreenState extends State { + static const double _labelZoomThreshold = 8.5; + Uint8List? _selectedPath; double _pathDistance = 0.0; + bool _showNodeLabels = true; + bool _didReceivePositionUpdate = false; @override void initState() { @@ -333,6 +340,8 @@ class _ChannelMessagePathMapScreenState Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { + final settings = context.watch().settings; + final isImperial = settings.unitSystem == UnitSystem.imperial; final tileCache = context.read(); final primaryPath = _selectPrimaryPath( widget.message.pathBytes, @@ -393,6 +402,9 @@ class _ChannelMessagePathMapScreenState ? points.first : const LatLng(0, 0); final initialZoom = points.isNotEmpty ? 13.0 : 2.0; + if (!_didReceivePositionUpdate) { + _showNodeLabels = initialZoom >= _labelZoomThreshold; + } final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null; @@ -402,7 +414,9 @@ class _ChannelMessagePathMapScreenState _pathDistance = _getPathDistance(points); return Scaffold( - appBar: AppBar(title: Text(context.l10n.channelPath_mapTitle)), + appBar: AppBar( + title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle), + ), body: SafeArea( top: false, child: Stack( @@ -424,6 +438,17 @@ class _ChannelMessagePathMapScreenState interactionOptions: InteractionOptions( flags: ~InteractiveFlag.rotate, ), + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (!_didReceivePositionUpdate || + shouldShow != _showNodeLabels) { + if (!mounted) return; + setState(() { + _didReceivePositionUpdate = true; + _showNodeLabels = shouldShow; + }); + } + }, ), children: [ TileLayer( @@ -435,7 +460,12 @@ class _ChannelMessagePathMapScreenState ), if (polylines.isNotEmpty) PolylineLayer(polylines: polylines), - MarkerLayer(markers: _buildHopMarkers(hops)), + MarkerLayer( + markers: _buildHopMarkers( + hops, + showLabels: _showNodeLabels, + ), + ), ], ), if (observedPaths.length > 1) @@ -458,7 +488,7 @@ class _ChannelMessagePathMapScreenState ), ), ), - _buildLegendCard(context, hops), + _buildLegendCard(context, hops, isImperial), ], ), ), @@ -530,45 +560,61 @@ class _ChannelMessagePathMapScreenState ); } - List _buildHopMarkers(List<_PathHop> hops) { - return [ - for (final hop in hops) - if (hop.hasLocation) - Marker( - point: hop.position!, - width: 35, - height: 35, - child: Container( - decoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: Text( - hop.index.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, + List _buildHopMarkers( + List<_PathHop> hops, { + required bool showLabels, + }) { + final markers = []; + for (final hop in hops) { + if (!hop.hasLocation) continue; + final point = hop.position!; + markers.add( + Marker( + point: point, + width: 35, + height: 35, + child: Container( + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), ), + ], + ), + alignment: Alignment.center, + child: Text( + hop.index.toString(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, ), ), ), - if (context.read().selfLatitude != null && - context.read().selfLongitude != null) - Marker( - point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, + ), + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: point, + label: hop.contact?.name ?? _formatPrefix(hop.prefix), ), + ); + } + } + + final selfLat = context.read().selfLatitude; + final selfLon = context.read().selfLongitude; + if (selfLat != null && selfLon != null) { + final selfPoint = LatLng(selfLat, selfLon); + markers.add( + Marker( + point: selfPoint, width: 35, height: 35, child: Container( @@ -595,10 +641,63 @@ class _ChannelMessagePathMapScreenState ), ), ), - ]; + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: selfPoint, + label: context.l10n.pathTrace_you, + ), + ); + } + } + + return markers; } - Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) { + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { + return Marker( + point: point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildLegendCard( + BuildContext context, + List<_PathHop> hops, + bool isImperial, + ) { final l10n = context.l10n; final maxHeight = MediaQuery.of(context).size.height * 0.35; final estimatedHeight = 72.0 + (hops.length * 56.0); @@ -617,7 +716,7 @@ class _ChannelMessagePathMapScreenState Padding( padding: const EdgeInsets.all(12), child: Text( - '${l10n.channelPath_repeaterHops} (${(_pathDistance / 1609.34).toStringAsFixed(2)} Miles / ${(_pathDistance / 1000).toStringAsFixed(2)} Km)', + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}', style: const TextStyle(fontWeight: FontWeight.w600), ), ), diff --git a/lib/screens/community_qr_scanner_screen.dart b/lib/screens/community_qr_scanner_screen.dart index a2914a19..9f8602d3 100644 --- a/lib/screens/community_qr_scanner_screen.dart +++ b/lib/screens/community_qr_scanner_screen.dart @@ -6,6 +6,7 @@ import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/qr_scanner_widget.dart'; /// Screen for scanning community QR codes to join communities. @@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.community_scanQr), + title: AdaptiveAppBarTitle(context.l10n.community_scanQr), centerTitle: true, ), body: _isProcessing diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d4701075..a6828dd1 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1170,12 +1170,17 @@ class _ContactTile extends StatelessWidget { backgroundColor: _getTypeColor(contact.type), child: _buildContactAvatar(contact), ), - title: Text(contact.name), + title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(contact.pathLabel), - Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)), + Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis), + Text( + contact.shortPubKeyHex, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), ], ), // Clamp text scaling in trailing section to prevent overflow while @@ -1186,26 +1191,32 @@ class _ContactTile extends StatelessWidget { MediaQuery.textScalerOf(context).scale(1.0).clamp(1.0, 1.3), ), ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (unreadCount > 0) ...[ - UnreadBadge(count: unreadCount), - const SizedBox(height: 4), - ], - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + child: SizedBox( + width: 120, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (unreadCount > 0) ...[ + UnreadBadge(count: unreadCount), + const SizedBox(height: 4), ], - ), - ], + Text( + _formatLastSeen(context, lastSeen), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), + ], + ), ), ), onTap: onTap, diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart new file mode 100644 index 00000000..a2d4b4ac --- /dev/null +++ b/lib/screens/line_of_sight_map_screen.dart @@ -0,0 +1,1005 @@ +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; + +import '../l10n/l10n.dart'; +import '../screens/channels_screen.dart'; +import '../screens/contacts_screen.dart'; +import '../models/app_settings.dart'; +import '../services/app_settings_service.dart'; +import '../services/line_of_sight_service.dart'; +import '../services/map_tile_cache_service.dart'; +import '../utils/route_transitions.dart'; +import '../widgets/app_bar.dart'; +import '../widgets/quick_switch_bar.dart'; + +class LineOfSightEndpoint { + final String label; + final LatLng point; + final Color color; + final IconData icon; + final bool isCustom; + + const LineOfSightEndpoint({ + required this.label, + required this.point, + this.color = Colors.green, + this.icon = Icons.location_on, + this.isCustom = false, + }); +} + +class LineOfSightMapScreen extends StatefulWidget { + final String title; + final List candidates; + + const LineOfSightMapScreen({ + super.key, + required this.title, + required this.candidates, + }); + + @override + State createState() => _LineOfSightMapScreenState(); +} + +class _LineOfSightMapScreenState extends State { + static const String _errorSelectStartEnd = 'los_error_select_start_end'; + static const double _metersToFeet = 3.28084; + static const double _kmToMiles = 0.621371; + static const double _maxAntennaFeet = 400.0; + static const double _maxAntennaMeters = _maxAntennaFeet / _metersToFeet; + static const double _labelZoomThreshold = 8.5; + + final LineOfSightService _lineOfSightService = LineOfSightService(); + + bool _loading = false; + String? _error; + LineOfSightPathResult? _result; + LineOfSightEndpoint? _start; + LineOfSightEndpoint? _end; + final List _customEndpoints = []; + double _startAntennaHeight = 5.0; + double _endAntennaHeight = 5.0; + bool _showHud = true; + bool _menuExpanded = true; + bool _showDisplayNodes = true; + bool _showMarkerLabels = true; + bool _didReceivePositionUpdate = false; + int _losRequestNonce = 0; + + @override + void initState() { + super.initState(); + if (widget.candidates.isNotEmpty) { + _start = widget.candidates.first; + if (widget.candidates.length > 1) { + _end = widget.candidates[1]; + } + } + _runLos(); + } + + @override + void dispose() { + _lineOfSightService.dispose(); + super.dispose(); + } + + Future _runLos() async { + final start = _start; + final end = _end; + final startAntenna = _startAntennaHeight; + final endAntenna = _endAntennaHeight; + final requestId = ++_losRequestNonce; + if (start == null || end == null) { + setState(() { + _result = null; + _error = _errorSelectStartEnd; + }); + return; + } + + setState(() { + _loading = true; + _error = null; + }); + + try { + final result = await _lineOfSightService.analyzePath( + [start.point, end.point], + startAntennaHeightMeters: startAntenna, + endAntennaHeightMeters: endAntenna, + ); + if (!mounted) return; + if (!_isRunRequestCurrent( + requestId: requestId, + start: start, + end: end, + startAntenna: startAntenna, + endAntenna: endAntenna, + )) { + return; + } + setState(() { + _result = result; + }); + } catch (e) { + if (!mounted) return; + if (!_isRunRequestCurrent( + requestId: requestId, + start: start, + end: end, + startAntenna: startAntenna, + endAntenna: endAntenna, + )) { + return; + } + setState(() { + _result = null; + _error = context.l10n.losRunFailed(e.toString()); + }); + } finally { + if (mounted && requestId == _losRequestNonce) { + setState(() { + _loading = false; + }); + } + } + } + + bool _isRunRequestCurrent({ + required int requestId, + required LineOfSightEndpoint start, + required LineOfSightEndpoint end, + required double startAntenna, + required double endAntenna, + }) { + return requestId == _losRequestNonce && + identical(_start, start) && + identical(_end, end) && + _startAntennaHeight == startAntenna && + _endAntennaHeight == endAntenna; + } + + void _selectFromMap(LineOfSightEndpoint endpoint) { + setState(() { + _result = null; + _error = null; + if (_start == null || (_start != null && _end != null)) { + _start = endpoint; + if (_end == endpoint) _end = null; + } else { + _end = endpoint; + if (_start == endpoint) _start = null; + } + }); + + if (_start != null && _end != null) { + _runLos(); + } + } + + void _addCustomPoint(LatLng point) { + final endpoint = LineOfSightEndpoint( + label: context.l10n.losCustomPointLabel(_customEndpoints.length + 1), + point: point, + color: Colors.orange, + icon: Icons.push_pin, + isCustom: true, + ); + setState(() { + _customEndpoints.add(endpoint); + }); + _selectFromMap(endpoint); + } + + List _visibleEndpoints() { + return [if (_showDisplayNodes) ...widget.candidates, ..._customEndpoints]; + } + + bool _hasEndpoint( + List endpoints, + LineOfSightEndpoint? e, + ) { + if (e == null) return false; + return endpoints.any((item) => identical(item, e)); + } + + void _sanitizeSelection() { + final visible = _visibleEndpoints(); + if (!_hasEndpoint(visible, _start)) { + _start = null; + } + if (!_hasEndpoint(visible, _end)) { + _end = null; + } + } + + void _clearAllPoints() { + setState(() { + _customEndpoints.clear(); + _start = null; + _end = null; + _result = null; + _error = _errorSelectStartEnd; + }); + } + + void _deleteCustomPoint(LineOfSightEndpoint endpoint) { + setState(() { + _customEndpoints.removeWhere((e) => identical(e, endpoint)); + if (identical(_start, endpoint)) _start = null; + if (identical(_end, endpoint)) _end = null; + _result = null; + }); + } + + Future _renameCustomPoint(LineOfSightEndpoint endpoint) async { + final controller = TextEditingController(text: endpoint.label); + final newLabel = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(context.l10n.losRenameCustomPoint), + content: TextField( + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: context.l10n.losPointName, + ), + autofocus: true, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () { + final value = controller.text.trim(); + Navigator.pop(dialogContext, value); + }, + child: Text(context.l10n.common_save), + ), + ], + ), + ); + + if (newLabel == null || newLabel.isEmpty) return; + final index = _customEndpoints.indexWhere((e) => identical(e, endpoint)); + if (index < 0) return; + final renamed = LineOfSightEndpoint( + label: newLabel, + point: endpoint.point, + color: endpoint.color, + icon: endpoint.icon, + isCustom: endpoint.isCustom, + ); + setState(() { + _customEndpoints[index] = renamed; + if (identical(_start, endpoint)) _start = renamed; + if (identical(_end, endpoint)) _end = renamed; + }); + } + + @override + Widget build(BuildContext context) { + final settings = context.watch().settings; + final isImperial = settings.unitSystem == UnitSystem.imperial; + final tileCache = context.read(); + final endpoints = _visibleEndpoints(); + final mapPoints = [ + if (_start != null) _start!.point, + if (_end != null) _end!.point, + ]; + final initialCenter = mapPoints.isNotEmpty + ? mapPoints.first + : const LatLng(0, 0); + final bounds = mapPoints.length > 1 + ? LatLngBounds.fromPoints(mapPoints) + : null; + final initialZoom = mapPoints.length > 1 ? 13.0 : 2.0; + if (!_didReceivePositionUpdate) { + _showMarkerLabels = initialZoom >= _labelZoomThreshold; + } + + return Scaffold( + appBar: AppBar( + title: AppBarTitle(widget.title), + centerTitle: true, + actions: [ + IconButton( + icon: _loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.delete_outline), + onPressed: _loading ? null : _clearAllPoints, + tooltip: context.l10n.losClearAllPoints, + ), + ], + ), + body: Stack( + children: [ + FlutterMap( + options: MapOptions( + initialCenter: initialCenter, + initialZoom: initialZoom, + initialCameraFit: bounds == null + ? null + : CameraFit.bounds( + bounds: bounds, + padding: const EdgeInsets.all(64), + maxZoom: 16, + ), + interactionOptions: InteractionOptions( + flags: ~InteractiveFlag.rotate, + ), + onLongPress: (_, point) => _addCustomPoint(point), + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (!_didReceivePositionUpdate || + shouldShow != _showMarkerLabels) { + setState(() { + _didReceivePositionUpdate = true; + _showMarkerLabels = shouldShow; + }); + } + }, + ), + children: [ + TileLayer( + urlTemplate: kMapTileUrlTemplate, + tileProvider: tileCache.tileProvider, + userAgentPackageName: MapTileCacheService.userAgentPackageName, + maxZoom: 19, + ), + if (_result != null && _result!.segments.isNotEmpty) + PolylineLayer(polylines: _buildSegmentPolylines(_result!)), + MarkerLayer(markers: _buildMarkers(endpoints)), + ], + ), + if (_showHud) + Positioned( + left: 12, + right: 12, + top: 12, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.52, + ), + child: _buildControlPanel(isImperial), + ), + ), + if (!_showHud && _result != null && _result!.segments.isNotEmpty) + Positioned( + left: 12, + bottom: 12, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Text( + context.l10n.losElevationAttribution, + style: const TextStyle(fontSize: 10, color: Colors.white), + ), + ), + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + setState(() { + _showHud = !_showHud; + }); + }, + tooltip: _showHud + ? context.l10n.losHidePanelTooltip + : context.l10n.losShowPanelTooltip, + child: Icon(_showHud ? Icons.visibility_off : Icons.tune), + ), + bottomNavigationBar: SafeArea( + top: false, + child: QuickSwitchBar( + selectedIndex: 2, + onDestinationSelected: (index) => _handleQuickSwitch(index, context), + ), + ), + ); + } + + Widget _buildControlPanel(bool isImperial) { + _sanitizeSelection(); + final segment = _primarySegmentResult(); + final endpoints = _visibleEndpoints(); + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + final antennaAMeters = _startAntennaHeight; + final antennaBMeters = _endAntennaHeight; + final antennaADisplay = _toDisplayHeight(antennaAMeters, isImperial); + final antennaBDisplay = _toDisplayHeight(antennaBMeters, isImperial); + final antennaSliderMax = isImperial ? _maxAntennaFeet : _maxAntennaMeters; + final antennaSliderDivisions = isImperial ? 400 : 122; + return Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (segment != null) + SizedBox( + height: 160, + width: double.infinity, + child: CustomPaint( + painter: _LosProfilePainter( + samples: segment.samples, + distanceUnit: distanceUnit, + heightUnit: heightUnit, + badgeTextStyle: + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ) + else + SizedBox( + height: 44, + child: Center( + child: Text( + context.l10n.losRunToViewElevationProfile, + style: const TextStyle(fontSize: 11), + ), + ), + ), + const SizedBox(height: 8), + Text( + segment != null + ? _profileStats(segment, isImperial) + : _statusText(), + style: TextStyle( + fontSize: 12, + color: segment != null + ? (segment.isClear ? Colors.green : Colors.red) + : _statusColor(), + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + context.l10n.losElevationAttribution, + style: TextStyle(fontSize: 10, color: Colors.grey[700]), + ), + const SizedBox(height: 6), + ExpansionTile( + initiallyExpanded: _menuExpanded, + onExpansionChanged: (value) { + setState(() { + _menuExpanded = value; + }); + }, + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + title: Text( + context.l10n.losMenuTitle, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + ), + ), + subtitle: Text( + context.l10n.losMenuSubtitle, + style: const TextStyle(fontSize: 11), + ), + children: [ + SwitchListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text( + context.l10n.losShowDisplayNodes, + style: const TextStyle(fontSize: 12), + ), + value: _showDisplayNodes, + onChanged: (value) { + setState(() { + _showDisplayNodes = value; + _sanitizeSelection(); + _result = null; + }); + }, + ), + if (_customEndpoints.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + context.l10n.losCustomPoints, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + for (final point in _customEndpoints) + ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + title: Text( + point.label, + style: const TextStyle(fontSize: 12), + ), + subtitle: Text( + '${point.point.latitude.toStringAsFixed(5)}, ${point.point.longitude.toStringAsFixed(5)}', + style: const TextStyle(fontSize: 11), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit, size: 18), + onPressed: () => _renameCustomPoint(point), + tooltip: context.l10n.common_edit, + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: () => _deleteCustomPoint(point), + tooltip: context.l10n.common_delete, + ), + ], + ), + ), + ], + const SizedBox(height: 8), + _buildEndpointRow( + label: context.l10n.losPointA, + value: _start, + candidates: endpoints, + onChanged: (value) { + setState(() { + _start = value; + _result = null; + }); + if (_start != null && _end != null) { + _runLos(); + } + }, + ), + const SizedBox(height: 8), + _buildEndpointRow( + label: context.l10n.losPointB, + value: _end, + candidates: endpoints, + onChanged: (value) { + setState(() { + _end = value; + _result = null; + }); + if (_start != null && _end != null) { + _runLos(); + } + }, + ), + const SizedBox(height: 10), + Text( + context.l10n.losAntennaA( + antennaADisplay.toStringAsFixed(1), + heightUnit, + ), + style: const TextStyle(fontSize: 12), + ), + Slider( + value: antennaADisplay, + min: 0, + max: antennaSliderMax, + divisions: antennaSliderDivisions, + onChanged: (value) { + setState(() { + _startAntennaHeight = _toMetersHeight( + value, + isImperial, + ); + }); + }, + ), + Text( + context.l10n.losAntennaB( + antennaBDisplay.toStringAsFixed(1), + heightUnit, + ), + style: const TextStyle(fontSize: 12), + ), + Slider( + value: antennaBDisplay, + min: 0, + max: antennaSliderMax, + divisions: antennaSliderDivisions, + onChanged: (value) { + setState(() { + _endAntennaHeight = _toMetersHeight(value, isImperial); + }); + }, + ), + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: _loading ? null : _runLos, + icon: const Icon(Icons.visibility), + label: Text(context.l10n.losRun), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildEndpointRow({ + required String label, + required LineOfSightEndpoint? value, + required List candidates, + required ValueChanged onChanged, + }) { + return Row( + children: [ + SizedBox( + width: 54, + child: Text(label, style: const TextStyle(fontSize: 12)), + ), + Expanded( + child: DropdownButton( + value: value, + isExpanded: true, + items: candidates + .map( + (e) => DropdownMenuItem( + value: e, + child: Text(e.label, overflow: TextOverflow.ellipsis), + ), + ) + .toList(), + onChanged: onChanged, + ), + ), + ], + ); + } + + LineOfSightResult? _primarySegmentResult() { + if (_result == null || _result!.segments.isEmpty) return null; + return _result!.segments.first.result; + } + + String _profileStats(LineOfSightResult result, bool isImperial) { + final distance = isImperial + ? (result.totalDistanceMeters / 1000.0) * _kmToMiles + : result.totalDistanceMeters / 1000.0; + final distanceUnit = isImperial ? 'mi' : 'km'; + final heightUnit = isImperial ? 'ft' : 'm'; + final minClearance = result.samples.isEmpty + ? 0.0 + : result.samples.map((s) => s.clearanceMeters).reduce(math.min); + final minClearanceDisplay = isImperial + ? minClearance * _metersToFeet + : minClearance; + final maxObstructionDisplay = isImperial + ? result.maxObstructionMeters * _metersToFeet + : result.maxObstructionMeters; + if (!result.hasData) { + return _localizedLosError(result.errorMessage); + } + if (result.isClear) { + return context.l10n.losProfileClear( + distance.toStringAsFixed(1), + distanceUnit, + minClearanceDisplay.toStringAsFixed(1), + heightUnit, + ); + } + return context.l10n.losProfileBlocked( + distance.toStringAsFixed(1), + distanceUnit, + maxObstructionDisplay.toStringAsFixed(1), + heightUnit, + ); + } + + List _buildSegmentPolylines(LineOfSightPathResult result) { + final polylines = []; + for (final segment in result.segments) { + final color = !segment.result.hasData + ? Colors.grey + : (segment.result.isClear ? Colors.green : Colors.red); + polylines.add( + Polyline( + points: [segment.start, segment.end], + strokeWidth: 4, + color: color, + ), + ); + } + return polylines; + } + + List _buildMarkers(List endpoints) { + return [ + for (final endpoint in endpoints) + Marker( + point: endpoint.point, + width: 36, + height: 36, + child: GestureDetector( + onTap: () => _selectFromMap(endpoint), + child: Container( + decoration: BoxDecoration( + color: endpoint.color, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: const [ + BoxShadow(color: Colors.black26, blurRadius: 4), + ], + ), + child: Stack( + children: [ + Center( + child: Icon(endpoint.icon, color: Colors.white, size: 16), + ), + if (endpoint == _start || endpoint == _end) + Positioned( + right: 0, + bottom: 0, + child: Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(7), + border: Border.all(color: Colors.white, width: 1), + ), + alignment: Alignment.center, + child: Text( + endpoint == _start ? 'A' : 'B', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 9, + ), + ), + ), + ), + ], + ), + ), + ), + ), + for (final endpoint in endpoints) + if (_showMarkerLabels) + Marker( + point: endpoint.point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + endpoint.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ), + ), + ), + ), + ), + ]; + } + + String _statusText() { + if (_loading) return context.l10n.losStatusChecking; + if (_error == _errorSelectStartEnd) { + return context.l10n.losSelectStartEnd; + } + if (_error != null) return _error!; + if (_result == null) return context.l10n.losStatusNoData; + final total = _result!.segments.length; + return context.l10n.losStatusSummary( + _result!.clearSegments, + total, + _result!.blockedSegments, + _result!.unknownSegments, + ); + } + + Color _statusColor() { + if (_error != null) return Colors.red; + if (_loading) return Colors.orange; + if (_result == null) return Colors.grey; + if (_result!.blockedSegments > 0) return Colors.red; + if (_result!.clearSegments > 0) return Colors.green; + return Colors.grey; + } + + double _toDisplayHeight(double meters, bool isImperial) { + return isImperial ? meters * _metersToFeet : meters; + } + + double _toMetersHeight(double displayHeight, bool isImperial) { + return isImperial ? displayHeight / _metersToFeet : displayHeight; + } + + String _localizedLosError(String? message) { + if (message == LineOfSightService.errorElevationUnavailable) { + return context.l10n.losErrorElevationUnavailable; + } + if (message == LineOfSightService.errorInvalidInput) { + return context.l10n.losErrorInvalidInput; + } + return context.l10n.losNoElevationData; + } + + void _handleQuickSwitch(int index, BuildContext context) { + if (index == 2) { + Navigator.pop(context); + return; + } + switch (index) { + case 0: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)), + ); + break; + case 1: + Navigator.pushReplacement( + context, + buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)), + ); + break; + } + } +} + +class _LosProfilePainter extends CustomPainter { + final List samples; + final String distanceUnit; + final String heightUnit; + final TextStyle badgeTextStyle; + + const _LosProfilePainter({ + required this.samples, + required this.distanceUnit, + required this.heightUnit, + required this.badgeTextStyle, + }); + + @override + void paint(Canvas canvas, Size size) { + final bg = Paint()..color = const Color(0xFF243A63); + canvas.drawRect(Offset.zero & size, bg); + _drawUnitBadge(canvas, size); + + if (samples.length < 2) return; + + final minY = samples + .map((s) => math.min(s.terrainMeters, s.lineHeightMeters)) + .reduce(math.min); + final maxY = samples + .map((s) => math.max(s.terrainMeters, s.lineHeightMeters)) + .reduce(math.max); + final ySpan = math.max(1.0, maxY - minY); + final maxDist = math.max(1.0, samples.last.distanceMeters); + + Offset mapPoint(double x, double y) { + final px = (x / maxDist) * size.width; + final py = size.height - ((y - minY) / ySpan) * size.height; + return Offset(px, py); + } + + final terrainPath = ui.Path(); + terrainPath.moveTo(0, size.height); + for (final s in samples) { + final p = mapPoint(s.distanceMeters, s.terrainMeters); + terrainPath.lineTo(p.dx, p.dy); + } + terrainPath.lineTo(size.width, size.height); + terrainPath.close(); + + canvas.drawPath(terrainPath, Paint()..color = const Color(0xCC7C6F5D)); + + final terrainLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint(samples[i].distanceMeters, samples[i].terrainMeters); + if (i == 0) { + terrainLine.moveTo(p.dx, p.dy); + } else { + terrainLine.lineTo(p.dx, p.dy); + } + } + canvas.drawPath( + terrainLine, + Paint() + ..color = const Color(0xFF9FE870) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + + final losLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].lineHeightMeters, + ); + if (i == 0) { + losLine.moveTo(p.dx, p.dy); + } else { + losLine.lineTo(p.dx, p.dy); + } + } + canvas.drawPath( + losLine, + Paint() + ..color = const Color(0xFFE0E7FF) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + } + + @override + bool shouldRepaint(covariant _LosProfilePainter oldDelegate) { + return oldDelegate.samples != samples || + oldDelegate.distanceUnit != distanceUnit || + oldDelegate.heightUnit != heightUnit || + oldDelegate.badgeTextStyle != badgeTextStyle; + } + + void _drawUnitBadge(Canvas canvas, Size size) { + final span = TextSpan( + text: '$heightUnit / $distanceUnit', + style: badgeTextStyle, + ); + final painter = TextPainter(text: span, textDirection: TextDirection.ltr) + ..layout(); + painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); + } +} diff --git a/lib/screens/map_cache_screen.dart b/lib/screens/map_cache_screen.dart index 3f611095..1391660e 100644 --- a/lib/screens/map_cache_screen.dart +++ b/lib/screens/map_cache_screen.dart @@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; import '../services/app_settings_service.dart'; import '../services/map_tile_cache_service.dart'; +import '../widgets/adaptive_app_bar_title.dart'; class MapCacheScreen extends StatefulWidget { const MapCacheScreen({super.key}); @@ -224,7 +225,10 @@ class _MapCacheScreenState extends State { : (_completedTiles / _estimatedTiles).clamp(0.0, 1.0).toDouble(); return Scaffold( - appBar: AppBar(title: Text(l10n.mapCache_title), centerTitle: true), + appBar: AppBar( + title: AdaptiveAppBarTitle(l10n.mapCache_title), + centerTitle: true, + ), body: Column( children: [ Expanded( diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 1fad04b6..8c13a717 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -11,6 +11,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; import '../connector/meshcore_protocol.dart'; +import '../models/app_settings.dart'; import '../models/channel.dart'; import '../models/contact.dart'; import '../services/app_settings_service.dart'; @@ -26,6 +27,7 @@ import '../widgets/repeater_login_dialog.dart'; import '../widgets/room_login_dialog.dart'; import 'repeater_hub_screen.dart'; import 'settings_screen.dart'; +import 'line_of_sight_map_screen.dart'; class MapScreen extends StatefulWidget { final LatLng? highlightPosition; @@ -46,6 +48,8 @@ class MapScreen extends StatefulWidget { } class _MapScreenState extends State { + static const double _labelZoomThreshold = 8.5; + final MapController _mapController = MapController(); final MapMarkerService _markerService = MapMarkerService(); final Set _hiddenMarkerIds = {}; @@ -58,6 +62,7 @@ class _MapScreenState extends State { final List _points = []; final List _polylines = []; bool _legendExpanded = false; + bool _showNodeLabels = true; @override void initState() { @@ -247,6 +252,7 @@ class _MapScreenState extends State { // Re center map after removed markers have loaded if (!_hasInitializedMap && _removedMarkersLoaded) { _hasInitializedMap = true; + _showNodeLabels = initialZoom >= _labelZoomThreshold; if (hasMapContent) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -272,6 +278,47 @@ class _MapScreenState extends State { onPressed: () => _startPath(), tooltip: context.l10n.contacts_pathTrace, ), + if (!_isBuildingPathTrace) + IconButton( + icon: const Icon(Icons.visibility), + onPressed: () { + final candidates = []; + if (connector.selfLatitude != null && + connector.selfLongitude != null) { + candidates.add( + LineOfSightEndpoint( + label: context.l10n.pathTrace_you, + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + color: Colors.teal, + icon: Icons.person_pin_circle, + ), + ); + } + for (final c in contactsWithLocation) { + candidates.add( + LineOfSightEndpoint( + label: c.name, + point: LatLng(c.latitude!, c.longitude!), + color: _getNodeColor(c.type), + icon: _getNodeIcon(c.type), + ), + ); + } + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => LineOfSightMapScreen( + title: context.l10n.map_losScreenTitle, + candidates: candidates, + ), + ), + ); + }, + tooltip: context.l10n.map_lineOfSight, + ), PopupMenuButton( itemBuilder: (context) => [ PopupMenuItem( @@ -350,6 +397,14 @@ class _MapScreenState extends State { position: latLng, ); }, + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (shouldShow != _showNodeLabels && mounted) { + setState(() { + _showNodeLabels = shouldShow; + }); + } + }, ), children: [ TileLayer( @@ -374,7 +429,11 @@ class _MapScreenState extends State { size: 34, ), ), - ..._buildMarkers(contactsWithLocation, settings), + ..._buildMarkers( + contactsWithLocation, + settings, + showLabels: _showNodeLabels, + ), ...sharedMarkers.map(_buildSharedMarker), if (connector.selfLatitude != null && connector.selfLongitude != null) @@ -413,6 +472,16 @@ class _MapScreenState extends State { ), ), ), + if (_showNodeLabels && + connector.selfLatitude != null && + connector.selfLongitude != null) + _buildNodeLabelMarker( + point: LatLng( + connector.selfLatitude!, + connector.selfLongitude!, + ), + label: connector.deviceDisplayName, + ), ], ), ], @@ -444,7 +513,11 @@ class _MapScreenState extends State { ); } - List _buildMarkers(List contacts, settings) { + List _buildMarkers( + List contacts, + settings, { + required bool showLabels, + }) { final markers = []; for (final contact in contacts) { @@ -499,11 +572,57 @@ class _MapScreenState extends State { ); markers.add(marker); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: LatLng(contact.latitude!, contact.longitude!), + label: contact.name, + ), + ); + } } return markers; } + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { + return Marker( + point: point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ); + } + Color _getNodeColor(int type) { switch (type) { case advTypeChat: @@ -1519,6 +1638,9 @@ class _MapScreenState extends State { Widget _buildPathTraceOverlay() { final l10n = context.l10n; + final isImperial = + context.read().settings.unitSystem == + UnitSystem.imperial; return Positioned( top: 16, left: 16, @@ -1539,7 +1661,7 @@ class _MapScreenState extends State { const SizedBox(height: 6), if (_pathTrace.isNotEmpty) Text( - "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points))}", + "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", style: TextStyle(fontSize: 12, color: Colors.grey[700]), ), SelectableText( @@ -1549,8 +1671,10 @@ class _MapScreenState extends State { style: TextStyle(fontSize: 18), ), const SizedBox(height: 6), - Row( - mainAxisSize: MainAxisSize.min, + Wrap( + alignment: WrapAlignment.center, + spacing: 8, + runSpacing: 8, children: [ if (_pathTrace.isNotEmpty) ElevatedButton( diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index 8e24bee1..c1f7f447 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -8,7 +8,9 @@ import 'package:latlong2/latlong.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/l10n/l10n.dart'; +import 'package:meshcore_open/models/app_settings.dart'; import 'package:meshcore_open/models/contact.dart'; +import 'package:meshcore_open/services/app_settings_service.dart'; import 'package:meshcore_open/services/map_tile_cache_service.dart'; import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart'; @@ -27,8 +29,11 @@ double getPathDistanceMeters(List points) { return distanceMeters; } -String formatDistance(double distanceMeters) { - return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} Miles / ${(distanceMeters / 1000).toStringAsFixed(2)} Km)'; +String formatDistance(double distanceMeters, {required bool isImperial}) { + if (isImperial) { + return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)'; + } + return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)'; } class PathTraceData { @@ -64,6 +69,8 @@ class PathTraceMapScreen extends StatefulWidget { } class _PathTraceMapScreenState extends State { + static const double _labelZoomThreshold = 8.5; + StreamSubscription? _frameSubscription; Timer? _timeoutTimer; @@ -78,6 +85,7 @@ class _PathTraceMapScreenState extends State { LatLngBounds? _bounds; ValueKey _mapKey = const ValueKey('initial'); double _pathDistanceMeters = 0.0; + bool _showNodeLabels = true; String _formatPathPrefixes(Uint8List pathBytes) { return pathBytes @@ -291,6 +299,8 @@ class _PathTraceMapScreenState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, connector, _) { + final settings = context.watch().settings; + final isImperial = settings.unitSystem == UnitSystem.imperial; final tileCache = context.read(); return Scaffold( @@ -355,7 +365,8 @@ class _PathTraceMapScreenState extends State { ), ), ), - if (_hasData) _buildLegendCard(context, _traceData!), + if (_hasData) + _buildLegendCard(context, _traceData!, isImperial), ], ), ), @@ -364,55 +375,61 @@ class _PathTraceMapScreenState extends State { ); } - List _buildHopMarkers(List pathData) { - return [ - for (final hop in pathData) - if (_traceData!.pathContacts[hop] != null && - _traceData!.pathContacts[hop]!.hasLocation) - Marker( - point: LatLng( - _traceData!.pathContacts[hop]!.latitude!, - _traceData!.pathContacts[hop]!.longitude!, - ), - width: 35, - height: 35, - child: Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.green, - shape: BoxShape.circle, - border: Border.all(color: Colors.white, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.3), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - alignment: Alignment.center, - child: Text( - _traceData!.pathContacts[hop]!.publicKey - .sublist(0, 1) - .map( - (b) => b.toRadixString(16).padLeft(2, '0').toUpperCase(), - ) - .join(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ), - if (context.read().selfLatitude != null && - context.read().selfLongitude != null) + List _buildHopMarkers( + List pathData, { + required bool showLabels, + }) { + final markers = []; + for (final hop in pathData) { + final contact = _traceData!.pathContacts[hop]; + if (contact == null || !contact.hasLocation) continue; + final point = LatLng(contact.latitude!, contact.longitude!); + markers.add( Marker( - point: LatLng( - context.read().selfLatitude!, - context.read().selfLongitude!, + point: point, + width: 35, + height: 35, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + alignment: Alignment.center, + child: Text( + contact.publicKey + .sublist(0, 1) + .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) + .join(), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), ), + ), + ); + if (showLabels) { + markers.add(_buildNodeLabelMarker(point: point, label: contact.name)); + } + } + + final selfLat = context.read().selfLatitude; + final selfLon = context.read().selfLongitude; + if (selfLat != null && selfLon != null) { + final selfPoint = LatLng(selfLat, selfLon); + markers.add( + Marker( + point: selfPoint, width: 35, height: 35, child: Container( @@ -440,7 +457,56 @@ class _PathTraceMapScreenState extends State { ), ), ), - ]; + ); + if (showLabels) { + markers.add( + _buildNodeLabelMarker( + point: selfPoint, + label: context.l10n.pathTrace_you, + ), + ); + } + } + + return markers; + } + + Marker _buildNodeLabelMarker({required LatLng point, required String label}) { + return Marker( + point: point, + width: 120, + height: 24, + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Transform.translate( + offset: const Offset(0, -26), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: SizedBox( + width: 96, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ), + ); } String formatDirectionText(PathTraceData pathTraceData, int index) { @@ -520,6 +586,14 @@ class _PathTraceMapScreenState extends State { ), minZoom: 2.0, maxZoom: 18.0, + onPositionChanged: (camera, hasGesture) { + final shouldShow = camera.zoom >= _labelZoomThreshold; + if (shouldShow != _showNodeLabels && mounted) { + setState(() { + _showNodeLabels = shouldShow; + }); + } + }, ), children: [ TileLayer( @@ -530,12 +604,21 @@ class _PathTraceMapScreenState extends State { ), if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines), if (_traceData!.pathData.isNotEmpty) - MarkerLayer(markers: _buildHopMarkers(_traceData!.pathData)), + MarkerLayer( + markers: _buildHopMarkers( + _traceData!.pathData, + showLabels: _showNodeLabels, + ), + ), ], ); } - Widget _buildLegendCard(BuildContext context, PathTraceData pathTraceData) { + Widget _buildLegendCard( + BuildContext context, + PathTraceData pathTraceData, + bool isImperial, + ) { final l10n = context.l10n; final maxHeight = MediaQuery.of(context).size.height * 0.35; final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0); @@ -554,7 +637,7 @@ class _PathTraceMapScreenState extends State { Padding( padding: const EdgeInsets.all(12), child: Text( - '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters)}', + '${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}', style: const TextStyle(fontWeight: FontWeight.w600), ), ), diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart index 932e29cf..af9d75ef 100644 --- a/lib/screens/scanner_screen.dart +++ b/lib/screens/scanner_screen.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/device_tile.dart'; import 'contacts_screen.dart'; @@ -70,7 +71,7 @@ class _ScannerScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(context.l10n.scanner_title), + title: AdaptiveAppBarTitle(context.l10n.scanner_title), centerTitle: true, automaticallyImplyLeading: false, ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 94d541b0..a198f991 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -8,6 +8,7 @@ import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../widgets/adaptive_app_bar_title.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -41,7 +42,10 @@ class _SettingsScreenState extends State { Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( - appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true), + appBar: AppBar( + title: AdaptiveAppBarTitle(l10n.settings_title), + centerTitle: true, + ), body: SafeArea( top: false, child: Consumer( diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 8770938c..88c204d1 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -5,8 +5,10 @@ import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; import '../models/path_selection.dart'; +import '../models/app_settings.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; @@ -181,6 +183,8 @@ class _TelemetryScreenState extends State { Widget build(BuildContext context) { final l10n = context.l10n; final connector = context.watch(); + final settings = context.watch().settings; + final isImperialUnits = settings.unitSystem == UnitSystem.imperial; final repeater = _resolveRepeater(connector); final isFloodMode = repeater.pathOverride == -1; @@ -307,6 +311,7 @@ class _TelemetryScreenState extends State { entry['values'], l10n.telemetry_channelTitle(entry['channel']), entry['channel'], + isImperialUnits, ), ], ), @@ -319,6 +324,7 @@ class _TelemetryScreenState extends State { Map channelData, String title, int channel, + bool isImperialUnits, ) { final l10n = context.l10n; return Card( @@ -358,12 +364,12 @@ class _TelemetryScreenState extends State { else if (entry.key == 'temperature' && channel == 1) _buildInfoRow( l10n.telemetry_mcuTemperatureLabel, - _temperatureText(entry.value), + _temperatureText(entry.value, isImperialUnits), ) else if (entry.key == 'temperature') _buildInfoRow( l10n.telemetry_temperatureLabel, - _temperatureText(entry.value), + _temperatureText(entry.value, isImperialUnits), ) else if (entry.key == 'current' && channel == 1) _buildInfoRow( @@ -421,13 +427,13 @@ class _TelemetryScreenState extends State { return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); } - String _temperatureText(double? tempC) { + String _temperatureText(double? tempC, bool isImperialUnits) { final l10n = context.l10n; if (tempC == null) return l10n.common_notAvailable; final tempF = (tempC * 9 / 5) + 32; - return l10n.telemetry_temperatureValue( - tempC.toStringAsFixed(1), - tempF.toStringAsFixed(1), - ); + if (isImperialUnits) { + return '${tempF.toStringAsFixed(1)}°F'; + } + return '${tempC.toStringAsFixed(1)}°C'; } } diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index c1e8fc62..a85ab920 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -132,4 +132,14 @@ class AppSettingsService extends ChangeNotifier { _settings.copyWith(batteryChemistryByDeviceId: updated), ); } + + Future setUnitSystem(UnitSystem value) async { + await updateSettings(_settings.copyWith(unitSystem: value)); + } + + Future setLosUnitSystem(String value) async { + await setUnitSystem( + value == 'imperial' ? UnitSystem.imperial : UnitSystem.metric, + ); + } } diff --git a/lib/services/ble_debug_log_service.dart b/lib/services/ble_debug_log_service.dart index 0a9aeaef..bc46b599 100644 --- a/lib/services/ble_debug_log_service.dart +++ b/lib/services/ble_debug_log_service.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import '../connector/meshcore_protocol.dart'; class BleDebugLogEntry { @@ -44,6 +45,7 @@ class BleDebugLogService extends ChangeNotifier { static const int maxEntries = 500; final List _entries = []; final List _rawLogRxEntries = []; + bool _notifyScheduled = false; List get entries => List.unmodifiable(_entries); List get rawLogRxEntries => @@ -78,13 +80,31 @@ class BleDebugLogService extends ChangeNotifier { } } - notifyListeners(); + _notifyListenersSafely(); } void clear() { _entries.clear(); _rawLogRxEntries.clear(); - notifyListeners(); + _notifyListenersSafely(); + } + + void _notifyListenersSafely() { + final phase = SchedulerBinding.instance.schedulerPhase; + final canNotifyNow = + phase == SchedulerPhase.idle || + phase == SchedulerPhase.postFrameCallbacks; + if (canNotifyNow) { + notifyListeners(); + return; + } + + if (_notifyScheduled) return; + _notifyScheduled = true; + SchedulerBinding.instance.addPostFrameCallback((_) { + _notifyScheduled = false; + notifyListeners(); + }); } String _describeFrame( diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart new file mode 100644 index 00000000..e9f9f7b6 --- /dev/null +++ b/lib/services/line_of_sight_service.dart @@ -0,0 +1,406 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:math'; + +import 'package:http/http.dart' as http; +import 'package:latlong2/latlong.dart'; + +typedef ElevationDataSource = + Future> Function(List points); + +class LineOfSightSample { + final double distanceMeters; + final double terrainMeters; + final double lineHeightMeters; + final double clearanceMeters; + + const LineOfSightSample({ + required this.distanceMeters, + required this.terrainMeters, + required this.lineHeightMeters, + required this.clearanceMeters, + }); +} + +class LineOfSightResult { + final bool hasData; + final bool isClear; + final double totalDistanceMeters; + final double maxObstructionMeters; + final double? firstObstructionDistanceMeters; + final List samples; + final String? errorMessage; + + const LineOfSightResult({ + required this.hasData, + required this.isClear, + required this.totalDistanceMeters, + required this.maxObstructionMeters, + required this.firstObstructionDistanceMeters, + required this.samples, + this.errorMessage, + }); + + const LineOfSightResult.error({ + required this.totalDistanceMeters, + required this.errorMessage, + }) : hasData = false, + isClear = false, + maxObstructionMeters = 0, + firstObstructionDistanceMeters = null, + samples = const []; +} + +class LineOfSightPathSegment { + final int index; + final LatLng start; + final LatLng end; + final LineOfSightResult result; + + const LineOfSightPathSegment({ + required this.index, + required this.start, + required this.end, + required this.result, + }); +} + +class LineOfSightPathResult { + final List segments; + final int clearSegments; + final int blockedSegments; + final int unknownSegments; + + const LineOfSightPathResult({ + required this.segments, + required this.clearSegments, + required this.blockedSegments, + required this.unknownSegments, + }); +} + +class LineOfSightService { + static const String errorElevationUnavailable = + 'los_error_elevation_unavailable'; + static const String errorInvalidInput = 'los_error_invalid_input'; + + static const double _earthRadiusMeters = 6371000.0; + static const Distance _distance = Distance(); + static const Duration _cacheTtl = Duration(hours: 24); + static const int _maxFetchAttempts = 4; // initial try + 3 retries + static const Duration _initialBackoff = Duration(milliseconds: 300); + + final http.Client _httpClient; + final bool _ownsHttpClient; + final ElevationDataSource? _elevationDataSource; + final Map _elevationCache = {}; + + LineOfSightService({ + http.Client? httpClient, + ElevationDataSource? elevationDataSource, + }) : _httpClient = httpClient ?? http.Client(), + _ownsHttpClient = httpClient == null, + _elevationDataSource = elevationDataSource; + + Future analyzePath( + List points, { + double startAntennaHeightMeters = 1.5, + double endAntennaHeightMeters = 1.5, + double kFactor = 4.0 / 3.0, + double obstructionToleranceMeters = 0.0, + }) async { + if (points.length < 2) { + return const LineOfSightPathResult( + segments: [], + clearSegments: 0, + blockedSegments: 0, + unknownSegments: 0, + ); + } + + final segments = []; + var clearSegments = 0; + var blockedSegments = 0; + var unknownSegments = 0; + + for (int i = 0; i < points.length - 1; i++) { + final result = await analyzeLink( + points[i], + points[i + 1], + startAntennaHeightMeters: startAntennaHeightMeters, + endAntennaHeightMeters: endAntennaHeightMeters, + kFactor: kFactor, + obstructionToleranceMeters: obstructionToleranceMeters, + ); + segments.add( + LineOfSightPathSegment( + index: i, + start: points[i], + end: points[i + 1], + result: result, + ), + ); + + if (!result.hasData) { + unknownSegments++; + } else if (result.isClear) { + clearSegments++; + } else { + blockedSegments++; + } + } + + return LineOfSightPathResult( + segments: segments, + clearSegments: clearSegments, + blockedSegments: blockedSegments, + unknownSegments: unknownSegments, + ); + } + + Future analyzeLink( + LatLng start, + LatLng end, { + double startAntennaHeightMeters = 1.5, + double endAntennaHeightMeters = 1.5, + double kFactor = 4.0 / 3.0, + double obstructionToleranceMeters = 0.0, + }) async { + final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end); + if (totalDistanceMeters <= 1) { + return LineOfSightResult( + hasData: true, + isClear: true, + totalDistanceMeters: totalDistanceMeters, + maxObstructionMeters: 0, + firstObstructionDistanceMeters: null, + samples: const [], + ); + } + + final samplePoints = _buildSamplePoints(start, end, totalDistanceMeters); + final elevations = await _getElevations(samplePoints); + + if (elevations.any((e) => e == null)) { + return LineOfSightResult.error( + totalDistanceMeters: totalDistanceMeters, + errorMessage: errorElevationUnavailable, + ); + } + + return computeFromElevations( + points: samplePoints, + elevations: elevations.cast(), + startAntennaHeightMeters: startAntennaHeightMeters, + endAntennaHeightMeters: endAntennaHeightMeters, + kFactor: kFactor, + obstructionToleranceMeters: obstructionToleranceMeters, + ); + } + + static LineOfSightResult computeFromElevations({ + required List points, + required List elevations, + double startAntennaHeightMeters = 1.5, + double endAntennaHeightMeters = 1.5, + double kFactor = 4.0 / 3.0, + double obstructionToleranceMeters = 0.0, + }) { + if (points.length < 2 || elevations.length != points.length) { + return const LineOfSightResult.error( + totalDistanceMeters: 0, + errorMessage: errorInvalidInput, + ); + } + + final totalDistanceMeters = _distance.as( + LengthUnit.Meter, + points.first, + points.last, + ); + final effectiveEarthRadius = _earthRadiusMeters * kFactor; + final startLineHeight = elevations.first + startAntennaHeightMeters; + final endLineHeight = elevations.last + endAntennaHeightMeters; + + var maxObstructionMeters = 0.0; + double? firstObstructionDistanceMeters; + final samples = []; + var isClear = true; + + for (int i = 0; i < points.length; i++) { + final fraction = points.length == 1 ? 0.0 : i / (points.length - 1); + final distanceFromStart = totalDistanceMeters * fraction; + final lineHeight = + startLineHeight + (endLineHeight - startLineHeight) * fraction; + + final earthBulge = + (distanceFromStart * (totalDistanceMeters - distanceFromStart)) / + (2 * effectiveEarthRadius); + final terrainHeight = elevations[i] + earthBulge; + final clearance = lineHeight - terrainHeight; + + if (clearance < -obstructionToleranceMeters) { + isClear = false; + final obstruction = -clearance; + if (obstruction > maxObstructionMeters) { + maxObstructionMeters = obstruction; + } + firstObstructionDistanceMeters ??= distanceFromStart; + } + + samples.add( + LineOfSightSample( + distanceMeters: distanceFromStart, + terrainMeters: terrainHeight, + lineHeightMeters: lineHeight, + clearanceMeters: clearance, + ), + ); + } + + return LineOfSightResult( + hasData: true, + isClear: isClear, + totalDistanceMeters: totalDistanceMeters, + maxObstructionMeters: maxObstructionMeters, + firstObstructionDistanceMeters: firstObstructionDistanceMeters, + samples: samples, + ); + } + + List _buildSamplePoints( + LatLng start, + LatLng end, + double distanceMeters, + ) { + final sampleCount = distanceMeters < 2000 + ? 21 + : distanceMeters < 10000 + ? 41 + : 81; + + final points = []; + for (int i = 0; i < sampleCount; i++) { + final t = i / (sampleCount - 1); + points.add( + LatLng( + start.latitude + (end.latitude - start.latitude) * t, + start.longitude + (end.longitude - start.longitude) * t, + ), + ); + } + return points; + } + + Future> _getElevations(List points) async { + final dataSource = _elevationDataSource; + if (dataSource != null) { + return dataSource(points); + } + + final uncached = {}; + final values = List.filled(points.length, null); + for (int i = 0; i < points.length; i++) { + final key = _cacheKey(points[i]); + final cached = _readCachedValue(key); + if (cached != null) { + values[i] = cached; + } else { + uncached[i] = points[i]; + } + } + + if (uncached.isEmpty) return values; + + final latCsv = uncached.values + .map((p) => p.latitude.toStringAsFixed(6)) + .join(','); + final lonCsv = uncached.values + .map((p) => p.longitude.toStringAsFixed(6)) + .join(','); + + final uri = Uri.parse( + 'https://api.open-meteo.com/v1/elevation?latitude=$latCsv&longitude=$lonCsv', + ); + + final response = await _getWithBackoff(uri); + if (response.statusCode != 200) { + return values; + } + + final decoded = jsonDecode(response.body); + if (decoded is! Map) { + return values; + } + final elevations = decoded['elevation']; + if (elevations is! List) { + return values; + } + + final indices = uncached.keys.toList(); + for (int i = 0; i < min(indices.length, elevations.length); i++) { + final value = elevations[i]; + if (value is! num) continue; + final index = indices[i]; + final elevation = value.toDouble(); + values[index] = elevation; + _elevationCache[_cacheKey(points[index])] = _CachedElevation( + value: elevation, + expiresAt: DateTime.now().add(_cacheTtl), + ); + } + return values; + } + + Future _getWithBackoff(Uri uri) async { + var attempt = 0; + Duration backoff = _initialBackoff; + + while (true) { + attempt++; + try { + final response = await _httpClient.get(uri); + if (!_shouldRetryStatus(response.statusCode) || + attempt >= _maxFetchAttempts) { + return response; + } + } catch (_) { + if (attempt >= _maxFetchAttempts) rethrow; + } + + await Future.delayed(backoff); + backoff *= 2; + } + } + + bool _shouldRetryStatus(int statusCode) { + return statusCode == 429 || statusCode >= 500; + } + + double? _readCachedValue(String key) { + final cached = _elevationCache[key]; + if (cached == null) return null; + if (DateTime.now().isAfter(cached.expiresAt)) { + _elevationCache.remove(key); + return null; + } + return cached.value; + } + + String _cacheKey(LatLng point) { + return '${point.latitude.toStringAsFixed(5)},${point.longitude.toStringAsFixed(5)}'; + } + + void dispose() { + if (_ownsHttpClient) { + _httpClient.close(); + } + } +} + +class _CachedElevation { + final double value; + final DateTime expiresAt; + + const _CachedElevation({required this.value, required this.expiresAt}); +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart index fc979c6b..0b59bbcc 100644 --- a/lib/services/notification_service.dart +++ b/lib/services/notification_service.dart @@ -58,11 +58,17 @@ class NotificationService { requestBadgePermission: true, requestSoundPermission: true, ); + const windowsSettings = WindowsInitializationSettings( + appName: 'MeshCore Open', + appUserModelId: 'org.meshcore.open.app', + guid: 'e7ea8f85-72f5-4f36-91f6-038f740ccf86', + ); const initSettings = InitializationSettings( android: androidSettings, iOS: iosSettings, macOS: macSettings, + windows: windowsSettings, ); try { @@ -76,6 +82,13 @@ class NotificationService { } } + Future _ensureInitialized() async { + if (!_isInitialized) { + await initialize(); + } + return _isInitialized; + } + Future requestPermissions() async { if (!_isInitialized) { await initialize(); @@ -114,9 +127,7 @@ class NotificationService { String? contactId, int? badgeCount, }) async { - if (!_isInitialized) { - await initialize(); - } + if (!await _ensureInitialized()) return; final androidDetails = AndroidNotificationDetails( 'messages', @@ -148,13 +159,17 @@ class NotificationService { macOS: macDetails, ); - await _notifications.show( - id: contactId?.hashCode ?? 0, - title: contactName, - body: message, - notificationDetails: notificationDetails, - payload: 'message:$contactId', - ); + try { + await _notifications.show( + id: contactId?.hashCode ?? 0, + title: contactName, + body: message, + notificationDetails: notificationDetails, + payload: 'message:$contactId', + ); + } catch (e) { + debugPrint('Failed to show message notification: $e'); + } } Future _showAdvertNotificationImpl({ @@ -162,9 +177,7 @@ class NotificationService { required String contactType, String? contactId, }) async { - if (!_isInitialized) { - await initialize(); - } + if (!await _ensureInitialized()) return; const androidDetails = AndroidNotificationDetails( 'adverts', @@ -193,13 +206,17 @@ class NotificationService { macOS: macDetails, ); - await _notifications.show( - id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - title: _l10n.notification_newTypeDiscovered(contactType), - body: contactName, - notificationDetails: notificationDetails, - payload: 'advert:$contactId', - ); + try { + await _notifications.show( + id: contactId?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: _l10n.notification_newTypeDiscovered(contactType), + body: contactName, + notificationDetails: notificationDetails, + payload: 'advert:$contactId', + ); + } catch (e) { + debugPrint('Failed to show advert notification: $e'); + } } Future _showChannelMessageNotificationImpl({ @@ -208,9 +225,7 @@ class NotificationService { int? channelIndex, int? badgeCount, }) async { - if (!_isInitialized) { - await initialize(); - } + if (!await _ensureInitialized()) return; final androidDetails = AndroidNotificationDetails( 'channel_messages', @@ -247,13 +262,17 @@ class NotificationService { ? _l10n.notification_receivedNewMessage : preview; - await _notifications.show( - id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, - title: channelName, - body: body, - notificationDetails: notificationDetails, - payload: 'channel:$channelIndex', - ); + try { + await _notifications.show( + id: channelIndex?.hashCode ?? DateTime.now().millisecondsSinceEpoch, + title: channelName, + body: body, + notificationDetails: notificationDetails, + payload: 'channel:$channelIndex', + ); + } catch (e) { + debugPrint('Failed to show channel notification: $e'); + } } /// Returns a privacy-safe identifier for debug logging. @@ -396,35 +415,39 @@ class NotificationService { Future _showNotificationImmediately( _PendingNotification notification, ) async { - switch (notification.type) { - case _NotificationType.message: - await _showMessageNotificationImpl( - contactName: notification.title, - message: notification.body, - contactId: notification.id, - badgeCount: notification.badgeCount, - ); - break; - case _NotificationType.advert: - await _showAdvertNotificationImpl( - contactName: notification.body, - contactType: notification.title, - contactId: notification.id, - ); - break; - case _NotificationType.channelMessage: - await _showChannelMessageNotificationImpl( - channelName: notification.title, - message: notification.body, - channelIndex: int.tryParse(notification.id ?? ''), - badgeCount: notification.badgeCount, - ); - break; + try { + switch (notification.type) { + case _NotificationType.message: + await _showMessageNotificationImpl( + contactName: notification.title, + message: notification.body, + contactId: notification.id, + badgeCount: notification.badgeCount, + ); + break; + case _NotificationType.advert: + await _showAdvertNotificationImpl( + contactName: notification.body, + contactType: notification.title, + contactId: notification.id, + ); + break; + case _NotificationType.channelMessage: + await _showChannelMessageNotificationImpl( + channelName: notification.title, + message: notification.body, + channelIndex: int.tryParse(notification.id ?? ''), + badgeCount: notification.badgeCount, + ); + break; + } + } catch (e) { + debugPrint('Failed to show immediate notification: $e'); } } Future _showBatchSummary(List<_PendingNotification> batch) async { - if (!_isInitialized) await initialize(); + if (!await _ensureInitialized()) return; // Group by type final messages = batch @@ -468,13 +491,17 @@ class NotificationService { const notificationDetails = NotificationDetails(android: androidDetails); - await _notifications.show( - id: 'batch_summary'.hashCode, - title: _l10n.notification_activityTitle, - body: parts.join(', '), - notificationDetails: notificationDetails, - payload: 'batch', - ); + try { + await _notifications.show( + id: 'batch_summary'.hashCode, + title: _l10n.notification_activityTitle, + body: parts.join(', '), + notificationDetails: notificationDetails, + payload: 'batch', + ); + } catch (e) { + debugPrint('Failed to show batch summary notification: $e'); + } } } diff --git a/lib/widgets/adaptive_app_bar_title.dart b/lib/widgets/adaptive_app_bar_title.dart new file mode 100644 index 00000000..12363dd7 --- /dev/null +++ b/lib/widgets/adaptive_app_bar_title.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class AdaptiveAppBarTitle extends StatelessWidget { + final String text; + + const AdaptiveAppBarTitle(this.text, {super.key}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => SizedBox( + width: constraints.maxWidth, + child: FittedBox(fit: BoxFit.scaleDown, child: Text(text, maxLines: 1)), + ), + ); + } +} diff --git a/test/services/line_of_sight_service_test.dart b/test/services/line_of_sight_service_test.dart new file mode 100644 index 00000000..987ee6c5 --- /dev/null +++ b/test/services/line_of_sight_service_test.dart @@ -0,0 +1,72 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meshcore_open/services/line_of_sight_service.dart'; + +void main() { + List makePoints(int count) { + return List.generate(count, (i) => LatLng(0, i * 0.00001)); + } + + test('computeFromElevations reports clear LOS on flat terrain', () { + final points = makePoints(21); + final elevations = List.filled(points.length, 100); + + final result = LineOfSightService.computeFromElevations( + points: points, + elevations: elevations, + startAntennaHeightMeters: 2, + endAntennaHeightMeters: 2, + ); + + expect(result.hasData, isTrue); + expect(result.isClear, isTrue); + expect(result.maxObstructionMeters, equals(0)); + expect(result.firstObstructionDistanceMeters, isNull); + }); + + test( + 'computeFromElevations reports blocked LOS with central obstruction', + () { + final points = makePoints(21); + final elevations = List.filled(points.length, 100); + elevations[10] = 300; + + final result = LineOfSightService.computeFromElevations( + points: points, + elevations: elevations, + startAntennaHeightMeters: 1.5, + endAntennaHeightMeters: 1.5, + ); + + expect(result.hasData, isTrue); + expect(result.isClear, isFalse); + expect(result.maxObstructionMeters, greaterThan(0)); + expect(result.firstObstructionDistanceMeters, isNotNull); + }, + ); + + test('analyzePath summarizes clear and blocked segments', () async { + final service = LineOfSightService( + elevationDataSource: (points) async { + final elevations = List.filled(points.length, 100); + if (points.first.longitude > 0.00005) { + elevations[elevations.length ~/ 2] = 300; + } + return elevations; + }, + ); + + final path = [ + const LatLng(0, 0), + const LatLng(0, 0.0001), + const LatLng(0, 0.0002), + ]; + + final result = await service.analyzePath(path); + + expect(result.segments.length, 2); + expect(result.clearSegments, 1); + expect(result.blockedSegments, 1); + expect(result.unknownSegments, 0); + }); +} From 7acfe47fd78e0b49d718887258d7d767ed13a34b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Fri, 20 Feb 2026 22:09:11 -0800 Subject: [PATCH 38/99] Refactor map legend and filtering logic for contacts with location, to show count of active markers. (#203) --- lib/screens/map_screen.dart | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 8c13a717..ce7a7d1f 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -488,7 +488,8 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) _buildLegend( - contactsWithLocation.length, + contactsWithLocation, + settings, sharedMarkers.length, ), if (_isBuildingPathTrace) _buildPathTraceOverlay(), @@ -524,13 +525,17 @@ class _MapScreenState extends State { if (!contact.hasLocation) continue; // Apply node type filters - if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) { + if (contact.type == advTypeRepeater && + (!settings.mapShowRepeaters && !_isBuildingPathTrace)) { + continue; + } + if (contact.type == advTypeChat && + !(settings.mapShowChatNodes && !_isBuildingPathTrace)) { continue; } - if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue; if (contact.type != advTypeChat && contact.type != advTypeRepeater && - !settings.mapShowOtherNodes) { + (!settings.mapShowOtherNodes && !_isBuildingPathTrace)) { continue; } @@ -653,7 +658,26 @@ class _MapScreenState extends State { } } - Widget _buildLegend(int nodeCount, int markerCount) { + Widget _buildLegend( + List contactsWithLocation, + settings, + int markerCount, + ) { + int nodeCount = 0; + for (final contact in contactsWithLocation) { + // Apply node type filters + if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) { + continue; + } + if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue; + if (contact.type != advTypeChat && + contact.type != advTypeRepeater && + !settings.mapShowOtherNodes) { + continue; + } + nodeCount++; + } + return Positioned( top: 16, right: 16, From 304c3896692f5ad80a4e4a8d904a4fc0f1d7d33c Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Fri, 20 Feb 2026 23:41:20 -0800 Subject: [PATCH 39/99] Refactor label display in Line Of Sight and Map screens for improved alignment and styling (#204) --- lib/screens/line_of_sight_map_screen.dart | 44 +++++++++---------- lib/screens/map_screen.dart | 52 ++++++++++------------- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index a2d4b4ac..b073685a 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -802,29 +802,27 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 2, - ), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - endpoint.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 10, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + endpoint.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index ce7a7d1f..77ec98ce 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -462,13 +462,10 @@ class _MapScreenState extends State { ], ), alignment: Alignment.center, - child: Text( - context.l10n.pathTrace_you, - style: const TextStyle( - color: Colors.black, - fontWeight: FontWeight.bold, - fontSize: 12, - ), + child: const Icon( + Icons.person_pin_circle, + color: Colors.white, + size: 20, ), ), ), @@ -480,7 +477,7 @@ class _MapScreenState extends State { connector.selfLatitude!, connector.selfLongitude!, ), - label: connector.deviceDisplayName, + label: context.l10n.pathTrace_you, ), ], ), @@ -598,27 +595,24 @@ class _MapScreenState extends State { alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), From 061b7156946e51ae3e98acfdb9b85cfdb2ba5bdf Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:54:39 -0500 Subject: [PATCH 40/99] Fix repeater battery % inconsistency and add configurable repeater battery chemistry (#199) * fix(repeater): unify battery percentage math and add repeater chemistry setting - Add shared battery percent utility used by connector, repeater status, and telemetry - Add repeater-specific battery chemistry persistence and service accessors - Add repeater chemistry selector in Repeater Hub - Ensure telemetry and status compute percentages consistently from same chemistry - Add focused battery utility tests Refs #116 Refs #174 * fix: Flutter Analyzer Errors fixed Recent Merge Compatible * Unify repeater battery source across status and telemetry --- lib/connector/meshcore_connector.dart | 60 +++++++++++++++--------- lib/models/app_settings.dart | 18 +++++-- lib/screens/repeater_hub_screen.dart | 62 +++++++++++++++++++++++++ lib/screens/repeater_status_screen.dart | 44 ++++++++++++++---- lib/screens/telemetry_screen.dart | 46 ++++++++++++++---- lib/services/app_settings_service.dart | 25 +++++++--- lib/utils/battery_utils.dart | 26 +++++++++++ test/utils/battery_utils_test.dart | 23 +++++++++ 8 files changed, 253 insertions(+), 51 deletions(-) create mode 100644 lib/utils/battery_utils.dart create mode 100644 test/utils/battery_utils_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 9b256e29..10ee15bf 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -29,6 +29,7 @@ import '../storage/contact_store.dart'; import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; +import '../utils/battery_utils.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -81,6 +82,18 @@ enum MeshCoreConnectionState { disconnecting, } +class RepeaterBatterySnapshot { + final int millivolts; + final DateTime updatedAt; + final String source; + + const RepeaterBatterySnapshot({ + required this.millivolts, + required this.updatedAt, + required this.source, + }); +} + class MeshCoreConnector extends ChangeNotifier { // Message windowing to limit memory usage static const int _messageWindowSize = 200; @@ -187,6 +200,7 @@ class MeshCoreConnector extends ChangeNotifier { final Map _contactSmazEnabled = {}; final Set _knownContactKeys = {}; final Map _contactUnreadCount = {}; + final Map _repeaterBatterySnapshots = {}; bool _unreadStateLoaded = false; final Map _pendingRepeaterAcks = {}; String? _activeContactKey; @@ -254,10 +268,32 @@ class MeshCoreConnector extends ChangeNotifier { : 0; int? get batteryPercent => _batteryMillivolts == null ? null - : _estimateBatteryPercent( + : estimateBatteryPercentFromMillivolts( _batteryMillivolts!, _batteryChemistryForDevice(), ); + RepeaterBatterySnapshot? getRepeaterBatterySnapshot(String contactKeyHex) => + _repeaterBatterySnapshots[contactKeyHex]; + int? getRepeaterBatteryMillivolts(String contactKeyHex) => + _repeaterBatterySnapshots[contactKeyHex]?.millivolts; + + void updateRepeaterBatterySnapshot( + String contactKeyHex, + int millivolts, { + String source = 'unknown', + }) { + if (contactKeyHex.isEmpty || millivolts <= 0) return; + final previous = _repeaterBatterySnapshots[contactKeyHex]; + final snapshot = RepeaterBatterySnapshot( + millivolts: millivolts, + updatedAt: DateTime.now(), + source: source, + ); + _repeaterBatterySnapshots[contactKeyHex] = snapshot; + if (previous?.millivolts != millivolts) { + notifyListeners(); + } + } String _batteryChemistryForDevice() { final deviceId = _device?.remoteId.toString(); @@ -265,27 +301,6 @@ class MeshCoreConnector extends ChangeNotifier { return _appSettingsService!.batteryChemistryForDevice(deviceId); } - int _estimateBatteryPercent(int millivolts, String chemistry) { - final range = _batteryVoltageRange(chemistry); - final minMv = range.$1; - final maxMv = range.$2; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); - } - - (int, int) _batteryVoltageRange(String chemistry) { - switch (chemistry) { - case 'lifepo4': - return (2600, 3650); - case 'lipo': - return (3000, 4200); - case 'nmc': - default: - return (3000, 4200); - } - } - List getMessages(Contact contact) { return _conversations[contact.publicKeyHex] ?? []; } @@ -961,6 +976,7 @@ class MeshCoreConnector extends ChangeNotifier { _clientRepeat = null; _firmwareVerCode = null; _batteryMillivolts = null; + _repeaterBatterySnapshots.clear(); _batteryRequested = false; _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 229a7a64..71cafa03 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -34,6 +34,7 @@ class AppSettings { final String? languageOverride; // null = system default final bool appDebugLogEnabled; final Map batteryChemistryByDeviceId; + final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; AppSettings({ @@ -57,8 +58,10 @@ class AppSettings { this.languageOverride, this.appDebugLogEnabled = false, Map? batteryChemistryByDeviceId, + Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, - }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}; + }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, + batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}; Map toJson() { return { @@ -82,6 +85,7 @@ class AppSettings { 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, + 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, }; } @@ -124,9 +128,12 @@ class AppSettings { (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, - unitSystem: parseUnitSystem( - json['unit_system'] ?? json['los_unit_system'], - ), + batteryChemistryByRepeaterId: + (json['battery_chemistry_by_repeater_id'] as Map?)?.map( + (key, value) => MapEntry(key.toString(), value.toString()), + ) ?? + {}, + unitSystem: parseUnitSystem(json['unit_system']), ); } @@ -151,6 +158,7 @@ class AppSettings { Object? languageOverride = _unset, bool? appDebugLogEnabled, Map? batteryChemistryByDeviceId, + Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, }) { return AppSettings( @@ -181,6 +189,8 @@ class AppSettings { appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, + batteryChemistryByRepeaterId: + batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, ); } diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index a5f503d5..fd2da8e4 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart'; +import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; import '../models/contact.dart'; +import '../services/app_settings_service.dart'; import 'repeater_status_screen.dart'; import 'repeater_cli_screen.dart'; import 'repeater_settings_screen.dart'; @@ -21,6 +23,10 @@ class RepeaterHubScreen extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; + final settingsService = context.watch(); + final chemistry = settingsService.batteryChemistryForRepeater( + repeater.publicKeyHex, + ); return Scaffold( appBar: AppBar( title: Column( @@ -107,6 +113,62 @@ class RepeaterHubScreen extends StatelessWidget { ), ), const SizedBox(height: 24), + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.battery_full), + const SizedBox(width: 10), + Expanded( + child: Text( + l10n.appSettings_batteryChemistry, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + DropdownButtonFormField( + initialValue: chemistry, + isExpanded: true, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + isDense: true, + ), + onChanged: (value) { + if (value == null) return; + settingsService.setBatteryChemistryForRepeater( + repeater.publicKeyHex, + value, + ); + }, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(l10n.appSettings_batteryNmc), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 24), Text( l10n.repeater_managementTools, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 472b0137..95254f43 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -8,7 +8,9 @@ import '../models/contact.dart'; import '../models/path_selection.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; +import '../utils/battery_utils.dart'; import '../widgets/path_management_dialog.dart'; class RepeaterStatusScreen extends StatefulWidget { @@ -179,6 +181,12 @@ class _RepeaterStatusScreenState extends State { _dupDirect = directDups; _dupFlood = floodDups; }); + final connector = Provider.of(context, listen: false); + connector.updateRepeaterBatterySnapshot( + widget.repeater.publicKeyHex, + batteryMv, + source: 'status_binary', + ); _recordStatusResult(true); } @@ -201,6 +209,18 @@ class _RepeaterStatusScreenState extends State { _uptimeSecs = _asInt(data['uptime_secs']); _queueLen = _asInt(data['queue_len']); _debugFlags = _asInt(data['errors']); + final batteryMv = _batteryMv; + if (batteryMv != null) { + final connector = Provider.of( + context, + listen: false, + ); + connector.updateRepeaterBatterySnapshot( + widget.repeater.publicKeyHex, + batteryMv, + source: 'status_text', + ); + } } else if (data.containsKey('noise_floor')) { _noiseFloor = _asInt(data['noise_floor']); _lastRssi = _asInt(data['last_rssi']); @@ -590,18 +610,24 @@ class _RepeaterStatusScreenState extends State { } String _batteryText() { - if (_batteryMv == null) return '—'; - final percent = _batteryPercentFromMv(_batteryMv!); - final volts = (_batteryMv! / 1000.0).toStringAsFixed(2); + final connector = context.watch(); + final batteryMv = + connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? + _batteryMv; + if (batteryMv == null) return '—'; + final percent = estimateBatteryPercentFromMillivolts( + batteryMv, + _batteryChemistry(), + ); + final volts = (batteryMv / 1000.0).toStringAsFixed(2); return '$percent% / ${volts}V'; } - int _batteryPercentFromMv(int millivolts) { - const minMv = 3000; - const maxMv = 4200; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); + String _batteryChemistry() { + final settingsService = context.read(); + return settingsService.batteryChemistryForRepeater( + widget.repeater.publicKeyHex, + ); } String _clockText() { diff --git a/lib/screens/telemetry_screen.dart b/lib/screens/telemetry_screen.dart index 88c204d1..3f95ccdb 100644 --- a/lib/screens/telemetry_screen.dart +++ b/lib/screens/telemetry_screen.dart @@ -12,6 +12,7 @@ import '../services/app_settings_service.dart'; import '../services/repeater_command_service.dart'; import '../widgets/path_management_dialog.dart'; import '../helpers/cayenne_lpp.dart'; +import '../utils/battery_utils.dart'; class TelemetryScreen extends StatefulWidget { final Contact repeater; @@ -74,9 +75,19 @@ class _TelemetryScreenState extends State { } void _handleStatusResponse(Uint8List frame) { + final parsedTelemetry = CayenneLpp.parseByChannel(frame); + final batteryMv = _extractTelemetryBatteryMillivolts(parsedTelemetry); + if (batteryMv != null) { + final connector = Provider.of(context, listen: false); + connector.updateRepeaterBatterySnapshot( + widget.repeater.publicKeyHex, + batteryMv, + source: 'telemetry', + ); + } if (!mounted) return; setState(() { - _parsedTelemetry = CayenneLpp.parseByChannel(frame); + _parsedTelemetry = parsedTelemetry; }); ScaffoldMessenger.of(context).showSnackBar( @@ -411,20 +422,35 @@ class _TelemetryScreenState extends State { ); } - String _batteryText(double? batteryMv) { + int? _extractTelemetryBatteryMillivolts(List> entries) { + for (final entry in entries) { + if (entry['channel'] != 1) continue; + final values = entry['values']; + if (values is! Map) continue; + final voltage = values['voltage']; + if (voltage is num) return (voltage.toDouble() * 1000).round(); + } + return null; + } + + String _batteryText(double? telemetryVolts) { final l10n = context.l10n; + final connector = context.watch(); + final batteryMv = + connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ?? + (telemetryVolts == null ? null : (telemetryVolts * 1000).round()); if (batteryMv == null) return l10n.common_notAvailable; - final percent = _batteryPercentFromMv(batteryMv); - final volts = batteryMv.toStringAsFixed(2); + final chemistry = _batteryChemistry(); + final percent = estimateBatteryPercentFromMillivolts(batteryMv, chemistry); + final volts = (batteryMv / 1000).toStringAsFixed(2); return l10n.telemetry_batteryValue(percent, volts); } - int _batteryPercentFromMv(double millivolts) { - const minMv = 2.800; - const maxMv = 4.200; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); + String _batteryChemistry() { + final settingsService = context.read(); + return settingsService.batteryChemistryForRepeater( + widget.repeater.publicKeyHex, + ); } String _temperatureText(double? tempC, bool isImperialUnits) { diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index a85ab920..e131eb87 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -17,6 +17,12 @@ class AppSettingsService extends ChangeNotifier { return stored ?? 'nmc'; } + String batteryChemistryForRepeater(String repeaterPubKeyHex) { + final stored = _settings.batteryChemistryByRepeaterId[repeaterPubKeyHex]; + if (stored == 'liion') return 'nmc'; + return stored ?? 'nmc'; + } + Future loadSettings() async { final prefs = PrefsManager.instance; final jsonStr = prefs.getString(_settingsKey); @@ -133,13 +139,20 @@ class AppSettingsService extends ChangeNotifier { ); } + Future setBatteryChemistryForRepeater( + String repeaterPubKeyHex, + String chemistry, + ) async { + final updated = Map.from( + _settings.batteryChemistryByRepeaterId, + ); + updated[repeaterPubKeyHex] = chemistry; + await updateSettings( + _settings.copyWith(batteryChemistryByRepeaterId: updated), + ); + } + Future setUnitSystem(UnitSystem value) async { await updateSettings(_settings.copyWith(unitSystem: value)); } - - Future setLosUnitSystem(String value) async { - await setUnitSystem( - value == 'imperial' ? UnitSystem.imperial : UnitSystem.metric, - ); - } } diff --git a/lib/utils/battery_utils.dart b/lib/utils/battery_utils.dart new file mode 100644 index 00000000..2bf4a5df --- /dev/null +++ b/lib/utils/battery_utils.dart @@ -0,0 +1,26 @@ +typedef BatteryVoltageRange = ({int minMv, int maxMv}); + +BatteryVoltageRange batteryVoltageRange(String chemistry) { + switch (chemistry) { + case 'lifepo4': + return (minMv: 2600, maxMv: 3650); + case 'lipo': + return (minMv: 3000, maxMv: 4200); + case 'nmc': + default: + return (minMv: 3000, maxMv: 4200); + } +} + +int estimateBatteryPercentFromMillivolts(int millivolts, String chemistry) { + final range = batteryVoltageRange(chemistry); + if (millivolts <= range.minMv) return 0; + if (millivolts >= range.maxMv) return 100; + return (((millivolts - range.minMv) * 100) / (range.maxMv - range.minMv)) + .round(); +} + +int estimateBatteryPercentFromVolts(double volts, String chemistry) { + final millivolts = (volts * 1000).round(); + return estimateBatteryPercentFromMillivolts(millivolts, chemistry); +} diff --git a/test/utils/battery_utils_test.dart b/test/utils/battery_utils_test.dart new file mode 100644 index 00000000..65dec1ec --- /dev/null +++ b/test/utils/battery_utils_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/utils/battery_utils.dart'; + +void main() { + group('battery utils', () { + test('nmc range maps 3.0V to 0% and 4.2V to 100%', () { + expect(estimateBatteryPercentFromVolts(3.0, 'nmc'), 0); + expect(estimateBatteryPercentFromVolts(4.2, 'nmc'), 100); + }); + + test('lifepo4 range maps 2.6V to 0% and 3.65V to 100%', () { + expect(estimateBatteryPercentFromVolts(2.6, 'lifepo4'), 0); + expect(estimateBatteryPercentFromVolts(3.65, 'lifepo4'), 100); + }); + + test('unknown chemistry falls back to nmc mapping', () { + expect( + estimateBatteryPercentFromMillivolts(3600, 'unknown'), + estimateBatteryPercentFromMillivolts(3600, 'nmc'), + ); + }); + }); +} From b05b62eeee8431ab4c8bea0582fd9eadebec6426 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 21 Feb 2026 14:55:42 -0800 Subject: [PATCH 41/99] Changed all map lables to look the same across all map ui (#206) * Refactor label display in Line Of Sight and Map screens for improved alignment and styling * Refactor label positioning and styling in ChannelMessagePathMap and PathTraceMap screens for improved alignment --- lib/screens/channel_message_path_screen.dart | 39 +++++++++----------- lib/screens/path_trace_map.dart | 39 +++++++++----------- 2 files changed, 36 insertions(+), 42 deletions(-) diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart index 2d1faa3f..44dfe797 100644 --- a/lib/screens/channel_message_path_screen.dart +++ b/lib/screens/channel_message_path_screen.dart @@ -663,27 +663,24 @@ class _ChannelMessagePathMapScreenState alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart index c1f7f447..5f86cc16 100644 --- a/lib/screens/path_trace_map.dart +++ b/lib/screens/path_trace_map.dart @@ -479,27 +479,24 @@ class _PathTraceMapScreenState extends State { alignment: Alignment.topCenter, child: IgnorePointer( child: Transform.translate( - offset: const Offset(0, -26), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: SizedBox( - width: 96, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w500, - ), + offset: const Offset(0, -20), + child: FittedBox( + fit: BoxFit.contain, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w500, ), ), ), From 51d70ce0869e1953eb7d67ed7bf684741fb22d44 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:20:56 -0500 Subject: [PATCH 42/99] fix(appbar): prevent title overflow on narrow widths (#205) Use width-aware layout in AppBarTitle to avoid RenderFlex overflows under tight title constraints and larger text scaling. Hide subtitle and signal indicators progressively when space is limited while preserving normal behavior on wider layouts. --- lib/widgets/app_bar.dart | 69 +++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/lib/widgets/app_bar.dart b/lib/widgets/app_bar.dart index c88a5965..e1cda778 100644 --- a/lib/widgets/app_bar.dart +++ b/lib/widgets/app_bar.dart @@ -14,35 +14,54 @@ class AppBarTitle extends StatelessWidget { @override Widget build(BuildContext context) { final connector = context.watch(); + final selfName = connector.selfName; - return Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - leading ?? const SizedBox.shrink(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, + return LayoutBuilder( + builder: (context, constraints) { + final availableWidth = constraints.hasBoundedWidth + ? constraints.maxWidth + : MediaQuery.sizeOf(context).width; + final compact = availableWidth < 240; + final showSubtitle = + !compact && connector.isConnected && selfName != null; + final showBattery = availableWidth >= 60; + final showSnr = availableWidth >= 110; + final showIndicators = showBattery || showSnr; + + return Row( + mainAxisAlignment: MainAxisAlignment.start, children: [ - Text(title, overflow: TextOverflow.ellipsis), - if (connector.isConnected && connector.selfName != null) - Text( - '(${connector.selfName})', - style: TextStyle(fontSize: 14, color: Colors.grey[600]), - overflow: TextOverflow.ellipsis, + leading ?? const SizedBox.shrink(), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(title, maxLines: 1, overflow: TextOverflow.ellipsis), + if (showSubtitle) + Text( + '($selfName)', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), + ), + if (showIndicators) const SizedBox(width: 6), + if (showIndicators) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (showBattery) BatteryIndicator(connector: connector), + if (showSnr) SNRIndicator(connector: connector), + ], + ), + trailing ?? const SizedBox.shrink(), ], - ), - const SizedBox(width: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BatteryIndicator(connector: connector), - SNRIndicator(connector: connector), - ], - ), - trailing ?? const SizedBox.shrink(), - ], + ); + }, ); } } From 2feff809ff76c18ab08913dfe0ce08c862df7f62 Mon Sep 17 00:00:00 2001 From: Aaron Easterling <111671335+Specter242@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:31:51 -0500 Subject: [PATCH 43/99] Mark pending channel messages sent on RESP_CODE_SENT (#186) * Mark pending channel message sent on RESP_CODE_SENT * Disambiguate RESP_CODE_SENT handling for direct vs channel * Handle channel sent feedback when firmware returns RESP_CODE_OK * Correlate channel OK ACKs and queue reaction channel sends --- lib/connector/meshcore_connector.dart | 166 ++++++++++++++++++++++-- lib/screens/contacts_screen.dart | 9 +- lib/services/message_retry_service.dart | 15 ++- 3 files changed, 175 insertions(+), 15 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 10ee15bf..2fbddc7c 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -114,6 +114,10 @@ class MeshCoreConnector extends ChangeNotifier { final List _channels = []; final Map> _conversations = {}; final Map> _channelMessages = {}; + final List _pendingChannelSentQueue = []; + final List<_PendingCommandAck> _pendingGenericAckQueue = []; + static const String _reactionSendQueuePrefix = '__reaction_send__'; + int _reactionSendQueueSequence = 0; final Set _loadedConversationKeys = {}; final Map> _processedChannelReactions = {}; // channelIndex -> Set of "targetHash_emoji" @@ -988,6 +992,9 @@ class MeshCoreConnector extends ChangeNotifier { _isSyncingChannels = false; _channelSyncInFlight = false; _hasLoadedChannels = false; + _pendingChannelSentQueue.clear(); + _pendingGenericAckQueue.clear(); + _reactionSendQueueSequence = 0; _setState(MeshCoreConnectionState.disconnected); if (!manual) { @@ -995,7 +1002,11 @@ class MeshCoreConnector extends ChangeNotifier { } } - Future sendFrame(Uint8List data) async { + Future sendFrame( + Uint8List data, { + String? channelSendQueueId, + bool expectsGenericAck = false, + }) async { if (!isConnected || _rxCharacteristic == null) { throw Exception("Not connected to a MeshCore device"); } @@ -1014,6 +1025,11 @@ class MeshCoreConnector extends ChangeNotifier { data.toList(), withoutResponse: canWriteWithoutResponse, ); + _trackPendingGenericAck( + data, + channelSendQueueId: channelSendQueueId, + expectsGenericAck: expectsGenericAck, + ); } Future requestBatteryStatus({bool force = false}) async { @@ -1369,7 +1385,13 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); // Send the reaction to the device (don't add as a visible message) - await sendFrame(buildSendChannelTextMsgFrame(channel.index, text)); + final reactionQueueId = _nextReactionSendQueueId(); + _pendingChannelSentQueue.add(reactionQueueId); + await sendFrame( + buildSendChannelTextMsgFrame(channel.index, text), + channelSendQueueId: reactionQueueId, + expectsGenericAck: true, + ); return; } @@ -1379,6 +1401,7 @@ class MeshCoreConnector extends ChangeNotifier { channel.index, ); _addChannelMessage(channel.index, message); + _pendingChannelSentQueue.add(message.messageId); notifyListeners(); final trimmed = text.trim(); @@ -1388,7 +1411,11 @@ class MeshCoreConnector extends ChangeNotifier { (isChannelSmazEnabled(channel.index) && !isStructuredPayload) ? Smaz.encodeIfSmaller(text) : text; - await sendFrame(buildSendChannelTextMsgFrame(channel.index, outboundText)); + await sendFrame( + buildSendChannelTextMsgFrame(channel.index, outboundText), + channelSendQueueId: message.messageId, + expectsGenericAck: true, + ); } Future removeContact(Contact contact) async { @@ -1735,6 +1762,9 @@ class MeshCoreConnector extends ChangeNotifier { debugPrint('RX frame: code=$code len=${frame.length}'); switch (code) { + case respCodeOk: + _handleOk(); + break; case respCodeDeviceInfo: _handleDeviceInfo(frame); break; @@ -1829,6 +1859,17 @@ class MeshCoreConnector extends ChangeNotifier { 'Firmware responded with error code: $errCode', tag: 'Protocol', ); + + if (_pendingGenericAckQueue.isEmpty) { + return; + } + + final failedAck = _pendingGenericAckQueue.removeAt(0); + if (failedAck.commandCode != cmdSendChannelTxtMsg || + failedAck.channelSendQueueId == null) { + return; + } + _pendingChannelSentQueue.remove(failedAck.channelSendQueueId); } void _handlePathUpdated(Uint8List frame) { @@ -2611,8 +2652,22 @@ class MeshCoreConnector extends ChangeNotifier { return; } - if (_retryService != null) { - _retryService!.updateMessageFromSent(ackHash, timeoutMs); + final retryService = _retryService; + if (retryService != null && + retryService.updateMessageFromSent( + ackHash, + timeoutMs, + allowQueueFallback: false, + )) { + return; + } + + if (_markNextPendingChannelMessageSent()) { + return; + } + + if (retryService != null) { + retryService.updateMessageFromSent(ackHash, timeoutMs); } } else { // Fallback to old behavior @@ -2629,6 +2684,64 @@ class MeshCoreConnector extends ChangeNotifier { } } + bool _markNextPendingChannelMessageSent() { + while (_pendingChannelSentQueue.isNotEmpty) { + final queuedMessageId = _pendingChannelSentQueue.removeAt(0); + if (_isReactionSendQueueId(queuedMessageId)) { + return true; + } + if (_markPendingChannelMessageSentById(queuedMessageId)) { + return true; + } + } + return false; + } + + bool _markPendingChannelMessageSentById(String messageId) { + for (final entry in _channelMessages.entries) { + final channelMessages = entry.value; + for (int i = channelMessages.length - 1; i >= 0; i--) { + final message = channelMessages[i]; + if (message.messageId != messageId) { + continue; + } + if (!message.isOutgoing || + message.status != ChannelMessageStatus.pending) { + return false; + } + channelMessages[i] = message.copyWith( + status: ChannelMessageStatus.sent, + ); + _pendingChannelSentQueue.remove(messageId); + unawaited( + _channelMessageStore.saveChannelMessages(entry.key, channelMessages), + ); + notifyListeners(); + return true; + } + } + return false; + } + + void _handleOk() { + if (_pendingGenericAckQueue.isEmpty) { + return; + } + + final pendingAck = _pendingGenericAckQueue.removeAt(0); + if (pendingAck.commandCode != cmdSendChannelTxtMsg || + pendingAck.channelSendQueueId == null) { + return; + } + + final queueId = pendingAck.channelSendQueueId!; + _pendingChannelSentQueue.remove(queueId); + if (_isReactionSendQueueId(queueId)) { + return; + } + _markPendingChannelMessageSentById(queueId); + } + void _handleSendConfirmed(Uint8List frame) { // Frame format from C++: // [0] = PUSH_CODE_SEND_CONFIRMED @@ -3207,18 +3320,22 @@ class MeshCoreConnector extends ChangeNotifier { mergedPathBytes.length, ); final newRepeatCount = existing.repeatCount + 1; + final promotedFromPending = + newRepeatCount == 1 && + existing.status == ChannelMessageStatus.pending; messages[existingIndex] = existing.copyWith( repeatCount: newRepeatCount, pathLength: mergedPathLength, pathBytes: mergedPathBytes, pathVariants: mergedPathVariants, // Mark as sent when first repeat is heard - status: - newRepeatCount == 1 && - existing.status == ChannelMessageStatus.pending + status: promotedFromPending ? ChannelMessageStatus.sent : existing.status, ); + if (promotedFromPending) { + _pendingChannelSentQueue.remove(existing.messageId); + } } else { messages.add(processedMessage); } @@ -3391,11 +3508,37 @@ class MeshCoreConnector extends ChangeNotifier { _queuedMessageSyncInFlight = false; _isSyncingChannels = false; _channelSyncInFlight = false; + _pendingChannelSentQueue.clear(); + _pendingGenericAckQueue.clear(); + _reactionSendQueueSequence = 0; _setState(MeshCoreConnectionState.disconnected); _scheduleReconnect(); } + void _trackPendingGenericAck( + Uint8List data, { + String? channelSendQueueId, + required bool expectsGenericAck, + }) { + if (!expectsGenericAck || data.isEmpty) return; + _pendingGenericAckQueue.add( + _PendingCommandAck( + commandCode: data[0], + channelSendQueueId: channelSendQueueId, + ), + ); + } + + String _nextReactionSendQueueId() { + _reactionSendQueueSequence++; + return '$_reactionSendQueuePrefix$_reactionSendQueueSequence'; + } + + bool _isReactionSendQueueId(String queueId) { + return queueId.startsWith(_reactionSendQueuePrefix); + } + Map _parseKeyValueString(String input) { final result = {}; @@ -3691,3 +3834,10 @@ class _RepeaterAckContext { required this.messageBytes, }); } + +class _PendingCommandAck { + final int commandCode; + final String? channelSendQueueId; + + _PendingCommandAck({required this.commandCode, this.channelSendQueueId}); +} diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index a6828dd1..c3f783c6 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -183,14 +183,17 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); _pendingOperations.add(ContactOperationType.export); - await connector.sendFrame(exportContactFrame); + await connector.sendFrame(exportContactFrame, expectsGenericAck: true); } Future _contactZeroHop(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactZeroHopFrame = buildZeroHopContact(pubKey); _pendingOperations.add(ContactOperationType.zeroHopShare); - await connector.sendFrame(exportContactZeroHopFrame); + await connector.sendFrame( + exportContactZeroHopFrame, + expectsGenericAck: true, + ); } Future _contactImport() async { @@ -217,7 +220,7 @@ class _ContactsScreenState extends State try { final importContactFrame = buildImportContactFrame(hexString); _pendingOperations.add(ContactOperationType.import); - await connector.sendFrame(importContactFrame); + await connector.sendFrame(importContactFrame, expectsGenericAck: true); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/services/message_retry_service.dart b/lib/services/message_retry_service.dart index 9cbd68f7..694a6162 100644 --- a/lib/services/message_retry_service.dart +++ b/lib/services/message_retry_service.dart @@ -234,7 +234,11 @@ class MessageRetryService extends ChangeNotifier { } } - void updateMessageFromSent(Uint8List ackHash, int timeoutMs) { + bool updateMessageFromSent( + Uint8List ackHash, + int timeoutMs, { + bool allowQueueFallback = true, + }) { final ackHashHex = ackHash .map((b) => b.toRadixString(16).padLeft(2, '0')) .join(); @@ -277,7 +281,7 @@ class MessageRetryService extends ChangeNotifier { } // FALLBACK: Old queue-based matching (for messages sent before hash computation was added) - if (messageId == null) { + if (messageId == null && allowQueueFallback) { _debugLogService?.warn( 'RESP_CODE_SENT: ACK hash $ackHashHex not found in hash table, falling back to queue', tag: 'AckHash', @@ -320,7 +324,7 @@ class MessageRetryService extends ChangeNotifier { if (messageId == null || contact == null) { debugPrint('No pending message found for ACK hash: $ackHashHex'); - return; + return false; } // Store the mapping for future lookups (e.g., when ACK arrives) @@ -339,7 +343,7 @@ class MessageRetryService extends ChangeNotifier { 'Message $messageId no longer pending for ACK hash: $ackHashHex', ); _ackHashToMessageId.remove(ackHashHex); - return; + return false; } // Add this ACK hash to the list of expected ACKs for this message (for history) @@ -389,8 +393,11 @@ class MessageRetryService extends ChangeNotifier { _startTimeoutTimer(messageId, actualTimeout); debugPrint('Updated message $messageId with ACK hash: $ackHashHex'); + return true; } + bool get hasPendingMessages => _pendingMessages.isNotEmpty; + void _startTimeoutTimer(String messageId, int timeoutMs) { _timeoutTimers[messageId]?.cancel(); _timeoutTimers[messageId] = Timer(Duration(milliseconds: timeoutMs), () { From 8fe412920453307572275c638c4020a152c874fc Mon Sep 17 00:00:00 2001 From: Specter242 Date: Sat, 21 Feb 2026 21:01:57 -0500 Subject: [PATCH 44/99] Align Android app module to Java 17 and bump wakelock_plus --- android/app/build.gradle.kts | 6 +++--- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e7d2b428..e0a80290 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -19,13 +19,13 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 isCoreLibraryDesugaringEnabled = true } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } defaultConfig { diff --git a/pubspec.yaml b/pubspec.yaml index f5ceaafc..3624b932 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: cached_network_image: ^3.4.1 flutter_cache_manager: ^3.4.1 flutter_foreground_task: ^9.2.0 - wakelock_plus: ^1.2.8 + wakelock_plus: ^1.4.0 characters: ^1.4.0 package_info_plus: ^9.0.0 mobile_scanner: ^7.1.4 # QR/barcode scanning From 7cb4c5a33445a8fe2759edde44c504561f2325fc Mon Sep 17 00:00:00 2001 From: Leah <45321184+ChaoticLeah@users.noreply.github.com> Date: Sun, 22 Feb 2026 08:44:20 +0100 Subject: [PATCH 45/99] Swipe to reply (#160) * Add swipe to reply * format * Cleaned up code * format * remove my gitignore change - ignore this * fix * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor onHorizontalDragStart for readability fixed formating. * Fix swipe end handling in channel chat screen * Refactor swipe gesture handling in chat screen * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update lib/screens/channel_chat_screen.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor swipe handling for reply functionality * Adjust swipe thresholds and logic in chat screen * Conditionally render reply bubble or padding --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Winston Lowe --- lib/screens/channel_chat_screen.dart | 532 ++++++++++++++++++--------- 1 file changed, 367 insertions(+), 165 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 021ad7d8..bf05110e 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -271,193 +272,243 @@ class _ChannelChatScreenState extends State { ? message.pathVariants.first : Uint8List(0)); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Column( - crossAxisAlignment: isOutgoing - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: isOutgoing - ? MainAxisAlignment.end - : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - _buildAvatar(message.senderName), - const SizedBox(width: 8), - ], - Flexible( - child: GestureDetector( - onTap: () => _showMessagePathInfo(message), - onLongPress: () => _showMessageActions(message), - child: Container( - padding: gifId != null - ? const EdgeInsets.all(4) - : const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.65, - ), - decoration: BoxDecoration( - color: isOutgoing - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isOutgoing) ...[ - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - top: 4, - bottom: 4, - ) - : EdgeInsets.zero, - child: Text( - message.senderName, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), + const maxSwipeOffset = 64.0; + const replySwipeThreshold = 64.0; + final messageBody = Column( + crossAxisAlignment: isOutgoing + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: isOutgoing + ? MainAxisAlignment.end + : MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + _buildAvatar(message.senderName), + const SizedBox(width: 8), + ], + Flexible( + child: GestureDetector( + onTap: () => _showMessagePathInfo(message), + onLongPress: () => _showMessageActions(message), + child: Container( + padding: gifId != null + ? const EdgeInsets.all(4) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.65, + ), + decoration: BoxDecoration( + color: isOutgoing + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isOutgoing) ...[ + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + top: 4, + bottom: 4, + ) + : EdgeInsets.zero, + child: Text( + message.senderName, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, ), ), - if (gifId == null) const SizedBox(height: 4), - ], - if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message), - const SizedBox(height: 8), - ], - if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) - else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), - ) - else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + ), + if (gifId == null) const SizedBox(height: 4), + ], + if (message.replyToMessageId != null) ...[ + _buildReplyPreview(message), + const SizedBox(height: 8), + ], + if (poi != null) + _buildPoiMessage(context, poi, isOutgoing) + else if (gifId != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', + ) + else + Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), Text( - _formatTime(message.timestamp), + '${message.repeatCount}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), - Text( - '${message.repeatCount}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], ], - ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], + ], ), - ], - ), + ), + ], ), ), ), - ], - ), - if (message.reactions.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), - child: _buildReactionsDisplay(message), ), ], + ), + if (message.reactions.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: EdgeInsets.only(left: isOutgoing ? 0 : 48), + child: _buildReactionsDisplay(message), + ), ], - ), + ], + ); + + if (!isOutgoing) { + return _SwipeReplyBubble( + maxSwipeOffset: maxSwipeOffset, + replySwipeThreshold: replySwipeThreshold, + onReplyTriggered: () => _setReplyingTo(message), + hintBuilder: ({required isStart}) => + _buildReplySwipeHint(isStart: isStart), + child: messageBody, + ); + } else { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: messageBody, + ); + } + } + + Widget _buildReplySwipeHint({required bool isStart}) { + final colorScheme = Theme.of(context).colorScheme; + final content = Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.reply, color: colorScheme.primary), + const SizedBox(width: 6), + Text( + context.l10n.chat_reply, + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + + return Container( + alignment: isStart ? Alignment.centerLeft : Alignment.centerRight, + padding: const EdgeInsets.symmetric(horizontal: 16), + color: colorScheme.primary.withValues(alpha: 0.08), + child: isStart + ? content + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.l10n.chat_reply, + style: TextStyle( + color: colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 6), + Icon(Icons.reply, color: colorScheme.primary), + ], + ), ); } @@ -1007,6 +1058,157 @@ class _ChannelChatScreenState extends State { } } +class _SwipeReplyBubble extends StatefulWidget { + final double maxSwipeOffset; + final double replySwipeThreshold; + final VoidCallback onReplyTriggered; + final Widget Function({required bool isStart}) hintBuilder; + final Widget child; + + const _SwipeReplyBubble({ + required this.maxSwipeOffset, + required this.replySwipeThreshold, + required this.onReplyTriggered, + required this.hintBuilder, + required this.child, + }); + + @override + State<_SwipeReplyBubble> createState() => _SwipeReplyBubbleState(); +} + +class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> { + Offset? _swipeStartPosition; + double _swipeOffset = 0; + double _maxSwipeDistance = 0; + int? _swipePointerId; + bool _swipeLockedToHorizontal = false; + + void _handleSwipeStart(Offset position) { + _swipeStartPosition = position; + _maxSwipeDistance = 0; + if (_swipeOffset != 0) { + setState(() => _swipeOffset = 0); + } + } + + void _handleSwipePointerDown(PointerDownEvent event) { + _swipePointerId = event.pointer; + _swipeLockedToHorizontal = false; + _handleSwipeStart(event.position); + } + + void _handleSwipePointerMove(PointerMoveEvent event) { + if (_swipePointerId != event.pointer || _swipeStartPosition == null) { + return; + } + + final dx = event.position.dx - _swipeStartPosition!.dx; + + const axisLockThreshold = 12.0; + if (!_swipeLockedToHorizontal) { + if (-dx < axisLockThreshold) { + return; + } + _swipeLockedToHorizontal = true; + } + + _handleSwipeUpdate(event.position); + } + + void _handleSwipeUpdate(Offset position) { + if (_swipeStartPosition == null) return; + + final dx = position.dx - _swipeStartPosition!.dx; + if (dx >= 0) return; + + if (-dx < 6) return; + + if (-dx > _maxSwipeDistance) { + _maxSwipeDistance = -dx; + } + + final double clamped = dx.clamp(-widget.maxSwipeOffset, 0.0).toDouble(); + final adjusted = _applySwipeResistance(clamped, widget.maxSwipeOffset); + if (adjusted != _swipeOffset) { + setState(() => _swipeOffset = adjusted); + } + } + + void _handleSwipePointerUp(Offset position) { + if (_swipeLockedToHorizontal && _swipeStartPosition != null) { + final dx = position.dx - _swipeStartPosition!.dx; + final peak = math.max( + _maxSwipeDistance, + (-dx).clamp(0.0, double.infinity), + ); + if (peak >= widget.replySwipeThreshold) { + widget.onReplyTriggered(); + HapticFeedback.selectionClick(); + } + } + _resetSwipe(); + } + + void _resetSwipe() { + if (_swipeOffset != 0) { + setState(() => _swipeOffset = 0); + } + _swipeStartPosition = null; + _maxSwipeDistance = 0; + _swipePointerId = null; + _swipeLockedToHorizontal = false; + } + + double _applySwipeResistance(double rawOffset, double maxOffset) { + final abs = rawOffset.abs(); + if (abs <= 0) return 0; + final norm = (abs / maxOffset).clamp(0.0, 1.0); + const deadZone = 0.18; + if (norm <= deadZone) { + return rawOffset.sign * maxOffset * (norm * 0.08); + } + final t = ((norm - deadZone) / (1 - deadZone)).clamp(0.0, 1.0); + final curved = t < 0.5 + ? 16 * math.pow(t, 5) + : 1 - math.pow(-2 * t + 2, 5) / 2; + const deadZoneEnd = 0.0144; + return rawOffset.sign * + maxOffset * + (deadZoneEnd + curved * (1 - deadZoneEnd)); + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: _handleSwipePointerDown, + onPointerMove: _handleSwipePointerMove, + onPointerUp: (event) => _handleSwipePointerUp(event.position), + onPointerCancel: (_) => _resetSwipe(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: Opacity( + opacity: _swipeOffset.abs() / widget.maxSwipeOffset, + child: widget.hintBuilder(isStart: false), + ), + ), + AnimatedContainer( + duration: const Duration(milliseconds: 150), + transform: Matrix4.translationValues(_swipeOffset, 0, 0), + curve: Curves.easeOut, + child: widget.child, + ), + ], + ), + ), + ); + } +} + class _PoiInfo { final double lat; final double lon; From b3ad54f2964c498bd6c6ea942b57804a5f109274 Mon Sep 17 00:00:00 2001 From: Krasimir Kazakov Date: Sun, 22 Feb 2026 09:51:48 +0200 Subject: [PATCH 46/99] Added mute channel functionality (#209) --- lib/connector/meshcore_connector.dart | 2 ++ lib/l10n/app_bg.arb | 2 ++ lib/l10n/app_de.arb | 2 ++ lib/l10n/app_en.arb | 2 ++ lib/l10n/app_es.arb | 2 ++ lib/l10n/app_fr.arb | 2 ++ lib/l10n/app_it.arb | 2 ++ lib/l10n/app_localizations.dart | 12 ++++++++++++ lib/l10n/app_localizations_bg.dart | 6 ++++++ lib/l10n/app_localizations_de.dart | 6 ++++++ lib/l10n/app_localizations_en.dart | 6 ++++++ lib/l10n/app_localizations_es.dart | 6 ++++++ lib/l10n/app_localizations_fr.dart | 6 ++++++ lib/l10n/app_localizations_it.dart | 6 ++++++ lib/l10n/app_localizations_nl.dart | 6 ++++++ lib/l10n/app_localizations_pl.dart | 6 ++++++ lib/l10n/app_localizations_pt.dart | 6 ++++++ lib/l10n/app_localizations_ru.dart | 6 ++++++ lib/l10n/app_localizations_sk.dart | 6 ++++++ lib/l10n/app_localizations_sl.dart | 6 ++++++ lib/l10n/app_localizations_sv.dart | 6 ++++++ lib/l10n/app_localizations_uk.dart | 6 ++++++ lib/l10n/app_localizations_zh.dart | 6 ++++++ lib/l10n/app_nl.arb | 2 ++ lib/l10n/app_pl.arb | 2 ++ lib/l10n/app_pt.arb | 2 ++ lib/l10n/app_ru.arb | 2 ++ lib/l10n/app_sk.arb | 2 ++ lib/l10n/app_sl.arb | 2 ++ lib/l10n/app_sv.arb | 2 ++ lib/l10n/app_uk.arb | 2 ++ lib/l10n/app_zh.arb | 2 ++ lib/models/app_settings.dart | 13 ++++++++++++- lib/screens/channels_screen.dart | 24 ++++++++++++++++++++++++ lib/services/app_settings_service.dart | 15 +++++++++++++++ 35 files changed, 185 insertions(+), 1 deletion(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2fbddc7c..afd1626d 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -2529,6 +2529,8 @@ class MeshCoreConnector extends ChangeNotifier { } final label = channelName ?? _channelDisplayName(channelIndex); + if (_appSettingsService!.isChannelMuted(label)) return; + _notificationService.showChannelMessageNotification( channelName: label, message: message.text, diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 5689f951..8609023e 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Публичен канал", "channels_privateChannel": "Частен канал", "channels_editChannel": "Редактирай канал", + "channels_muteChannel": "Заглуши канала", + "channels_unmuteChannel": "Включи известията на канала", "channels_deleteChannel": "Изтрий канала", "channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 22fdf6bd..e5c82f79 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Öffentlicher Kanal", "channels_privateChannel": "Privater Kanal", "channels_editChannel": "Kanal bearbeiten", + "channels_muteChannel": "Kanal stummschalten", + "channels_unmuteChannel": "Kanal Stummschaltung aufheben", "channels_deleteChannel": "Lösche den Kanal", "channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ae245398..67ca72ee 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -352,6 +352,8 @@ "channels_publicChannel": "Public channel", "channels_privateChannel": "Private channel", "channels_editChannel": "Edit channel", + "channels_muteChannel": "Mute channel", + "channels_unmuteChannel": "Unmute channel", "channels_deleteChannel": "Delete channel", "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3a7fe537..483b4d33 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", + "channels_muteChannel": "Silenciar canal", + "channels_unmuteChannel": "Activar canal", "channels_deleteChannel": "Eliminar canal", "channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f962ee5c..e162cdb5 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canal public", "channels_privateChannel": "Canal privé", "channels_editChannel": "Modifier le canal", + "channels_muteChannel": "Désactiver les notifications du canal", + "channels_unmuteChannel": "Réactiver les notifications du canal", "channels_deleteChannel": "Supprimer le canal", "channels_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 61110043..2f8d186f 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canale pubblico", "channels_privateChannel": "Canale privato", "channels_editChannel": "Modifica canale", + "channels_muteChannel": "Silenzia canale", + "channels_unmuteChannel": "Attiva notifiche canale", "channels_deleteChannel": "Elimina canale", "channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 20d04228..e9686cef 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1546,6 +1546,18 @@ abstract class AppLocalizations { /// **'Edit channel'** String get channels_editChannel; + /// No description provided for @channels_muteChannel. + /// + /// In en, this message translates to: + /// **'Mute channel'** + String get channels_muteChannel; + + /// No description provided for @channels_unmuteChannel. + /// + /// In en, this message translates to: + /// **'Unmute channel'** + String get channels_unmuteChannel; + /// No description provided for @channels_deleteChannel. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 9c66ff22..cf4bf7b5 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -798,6 +798,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channels_editChannel => 'Редактирай канал'; + @override + String get channels_muteChannel => 'Заглуши канала'; + + @override + String get channels_unmuteChannel => 'Включи известията на канала'; + @override String get channels_deleteChannel => 'Изтрий канала'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ef7cd9d2..c6a07a4b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -795,6 +795,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_editChannel => 'Kanal bearbeiten'; + @override + String get channels_muteChannel => 'Kanal stummschalten'; + + @override + String get channels_unmuteChannel => 'Kanal Stummschaltung aufheben'; + @override String get channels_deleteChannel => 'Lösche den Kanal'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 7f07e263..254b5f4a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -787,6 +787,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get channels_editChannel => 'Edit channel'; + @override + String get channels_muteChannel => 'Mute channel'; + + @override + String get channels_unmuteChannel => 'Unmute channel'; + @override String get channels_deleteChannel => 'Delete channel'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6409675a..dcde3653 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -796,6 +796,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channels_editChannel => 'Editar canal'; + @override + String get channels_muteChannel => 'Silenciar canal'; + + @override + String get channels_unmuteChannel => 'Activar canal'; + @override String get channels_deleteChannel => 'Eliminar canal'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3536cf56..d572b8f7 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -798,6 +798,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_editChannel => 'Modifier le canal'; + @override + String get channels_muteChannel => 'Désactiver les notifications du canal'; + + @override + String get channels_unmuteChannel => 'Réactiver les notifications du canal'; + @override String get channels_deleteChannel => 'Supprimer le canal'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 521cfb71..d8e27f87 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -794,6 +794,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_editChannel => 'Modifica canale'; + @override + String get channels_muteChannel => 'Silenzia canale'; + + @override + String get channels_unmuteChannel => 'Attiva notifiche canale'; + @override String get channels_deleteChannel => 'Elimina canale'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index a7a4c0b8..0a50e8b0 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -792,6 +792,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get channels_editChannel => 'Kanaal bewerken'; + @override + String get channels_muteChannel => 'Kanaal dempen'; + + @override + String get channels_unmuteChannel => 'Kanaal dempen opheffen'; + @override String get channels_deleteChannel => 'Kanaal verwijderen'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 88154720..31dd8b56 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -797,6 +797,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get channels_editChannel => 'Edytuj kanał'; + @override + String get channels_muteChannel => 'Wycisz kanał'; + + @override + String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału'; + @override String get channels_deleteChannel => 'Usuń kanał'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c7fc7073..5092826f 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -797,6 +797,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channels_editChannel => 'Editar canal'; + @override + String get channels_muteChannel => 'Silenciar canal'; + + @override + String get channels_unmuteChannel => 'Ativar canal'; + @override String get channels_deleteChannel => 'Excluir canal'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 2e992bd1..570b7c84 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -795,6 +795,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get channels_editChannel => 'Изменить канал'; + @override + String get channels_muteChannel => 'Отключить уведомления канала'; + + @override + String get channels_unmuteChannel => 'Включить уведомления канала'; + @override String get channels_deleteChannel => 'Удалить канал'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index a51e0591..8bbb6dea 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -792,6 +792,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channels_editChannel => 'Upraviť kanál'; + @override + String get channels_muteChannel => 'Stlmiť kanál'; + + @override + String get channels_unmuteChannel => 'Zrušiť stlmenie kanála'; + @override String get channels_deleteChannel => 'Odstrániť kanál'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 5ac7e8b4..61e30584 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -790,6 +790,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_editChannel => 'Uredi kanal'; + @override + String get channels_muteChannel => 'Utišaj kanal'; + + @override + String get channels_unmuteChannel => 'Vklopi obvestila kanala'; + @override String get channels_deleteChannel => 'Pošlji kanal'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6d355d91..79b30b88 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -786,6 +786,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get channels_editChannel => 'Redigera kanal'; + @override + String get channels_muteChannel => 'Tysta kanal'; + + @override + String get channels_unmuteChannel => 'Slå på ljud för kanal'; + @override String get channels_deleteChannel => 'Ta bort kanal'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 0f3d550c..f3670021 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -793,6 +793,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channels_editChannel => 'Редагувати канал'; + @override + String get channels_muteChannel => 'Вимкнути сповіщення каналу'; + + @override + String get channels_unmuteChannel => 'Увімкнути сповіщення каналу'; + @override String get channels_deleteChannel => 'Видалити канал'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 36a114a5..7641800d 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -755,6 +755,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get channels_editChannel => '编辑频道'; + @override + String get channels_muteChannel => '静音频道'; + + @override + String get channels_unmuteChannel => '取消静音频道'; + @override String get channels_deleteChannel => '删除频道'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 733e4dce..57b2fdda 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Open kanaal", "channels_privateChannel": "Private kanaal", "channels_editChannel": "Kanaal bewerken", + "channels_muteChannel": "Kanaal dempen", + "channels_unmuteChannel": "Kanaal dempen opheffen", "channels_deleteChannel": "Kanaal verwijderen", "channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 35efee18..3787fa71 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Kanał publiczny", "channels_privateChannel": "Prywatny kanał", "channels_editChannel": "Edytuj kanał", + "channels_muteChannel": "Wycisz kanał", + "channels_unmuteChannel": "Wyłącz wyciszenie kanału", "channels_deleteChannel": "Usuń kanał", "channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fd742d94..7be66945 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Canal público", "channels_privateChannel": "Canal privado", "channels_editChannel": "Editar canal", + "channels_muteChannel": "Silenciar canal", + "channels_unmuteChannel": "Ativar canal", "channels_deleteChannel": "Excluir canal", "channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 04b2e049..26cfce3b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -226,6 +226,8 @@ "channels_publicChannel": "Публичный канал", "channels_privateChannel": "Приватный канал", "channels_editChannel": "Изменить канал", + "channels_muteChannel": "Отключить уведомления канала", + "channels_unmuteChannel": "Включить уведомления канала", "channels_deleteChannel": "Удалить канал", "channels_deleteChannelConfirm": "Удалить \"{name}\"? Это действие нельзя отменить.", "channels_channelDeleted": "Канал \"{name}\" удалён", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 6663094a..8b2cb0ae 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Veľké verejne kanály", "channels_privateChannel": "Osobné kanál", "channels_editChannel": "Upraviť kanál", + "channels_muteChannel": "Stlmiť kanál", + "channels_unmuteChannel": "Zrušiť stlmenie kanála", "channels_deleteChannel": "Odstrániť kanál", "channels_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 50a90432..4d3415d3 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Javni kanal", "channels_privateChannel": "Zasebni kanal", "channels_editChannel": "Uredi kanal", + "channels_muteChannel": "Utišaj kanal", + "channels_unmuteChannel": "Vklopi obvestila kanala", "channels_deleteChannel": "Pošlji kanal", "channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 260a34b8..8c5e399c 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -334,6 +334,8 @@ "channels_publicChannel": "Allmänt kanal", "channels_privateChannel": "Privat kanal", "channels_editChannel": "Redigera kanal", + "channels_muteChannel": "Tysta kanal", + "channels_unmuteChannel": "Slå på ljud för kanal", "channels_deleteChannel": "Ta bort kanal", "channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ec414b48..910f8b0e 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -335,6 +335,8 @@ "channels_publicChannel": "Публічний канал", "channels_privateChannel": "Приватний канал", "channels_editChannel": "Редагувати канал", + "channels_muteChannel": "Вимкнути сповіщення каналу", + "channels_unmuteChannel": "Увімкнути сповіщення каналу", "channels_deleteChannel": "Видалити канал", "channels_deleteChannelConfirm": "Видалити {name}? Це не можна скасувати.", "@channels_deleteChannelConfirm": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6b072c9c..d9efce7f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -342,6 +342,8 @@ "channels_publicChannel": "公共频道", "channels_privateChannel": "私密频道", "channels_editChannel": "编辑频道", + "channels_muteChannel": "静音频道", + "channels_unmuteChannel": "取消静音频道", "channels_deleteChannel": "删除频道", "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 71cafa03..d9504b39 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -36,6 +36,7 @@ class AppSettings { final Map batteryChemistryByDeviceId; final Map batteryChemistryByRepeaterId; final UnitSystem unitSystem; + final Set mutedChannels; AppSettings({ this.clearPathOnMaxRetry = false, @@ -60,8 +61,10 @@ class AppSettings { Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, this.unitSystem = UnitSystem.metric, + Set? mutedChannels, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, - batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}; + batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, + mutedChannels = mutedChannels ?? {}; Map toJson() { return { @@ -87,6 +90,7 @@ class AppSettings { 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, 'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId, 'unit_system': unitSystem.value, + 'muted_channels': mutedChannels.toList(), }; } @@ -134,6 +138,11 @@ class AppSettings { ) ?? {}, unitSystem: parseUnitSystem(json['unit_system']), + mutedChannels: + ((json['muted_channels'] as List?) + ?.map((e) => e.toString()) + .toSet()) ?? + {}, ); } @@ -160,6 +169,7 @@ class AppSettings { Map? batteryChemistryByDeviceId, Map? batteryChemistryByRepeaterId, UnitSystem? unitSystem, + Set? mutedChannels, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -192,6 +202,7 @@ class AppSettings { batteryChemistryByRepeaterId: batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId, unitSystem: unitSystem ?? this.unitSystem, + mutedChannels: mutedChannels ?? this.mutedChannels, ); } } diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 26062dea..994d3e7a 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -9,6 +9,7 @@ import 'package:uuid/uuid.dart'; import '../connector/meshcore_connector.dart'; import '../l10n/l10n.dart'; +import '../services/app_settings_service.dart'; import '../models/channel.dart'; import '../models/community.dart'; import '../storage/community_store.dart'; @@ -477,6 +478,9 @@ class _ChannelsScreenState extends State MeshCoreConnector connector, Channel channel, ) { + final settingsService = context.read(); + final isMuted = settingsService.isChannelMuted(channel.name); + showModalBottomSheet( context: context, builder: (context) => SafeArea( @@ -494,6 +498,26 @@ class _ChannelsScreenState extends State } }, ), + ListTile( + leading: Icon( + isMuted + ? Icons.notifications_outlined + : Icons.notifications_off_outlined, + ), + title: Text( + isMuted + ? context.l10n.channels_unmuteChannel + : context.l10n.channels_muteChannel, + ), + onTap: () async { + Navigator.pop(context); + if (isMuted) { + await settingsService.unmuteChannel(channel.name); + } else { + await settingsService.muteChannel(channel.name); + } + }, + ), ListTile( leading: const Icon(Icons.delete_outline, color: Colors.red), title: Text( diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index e131eb87..e80f903d 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -155,4 +155,19 @@ class AppSettingsService extends ChangeNotifier { Future setUnitSystem(UnitSystem value) async { await updateSettings(_settings.copyWith(unitSystem: value)); } + + bool isChannelMuted(String channelName) { + return _settings.mutedChannels.contains(channelName); + } + + Future muteChannel(String channelName) async { + final updated = Set.from(_settings.mutedChannels)..add(channelName); + await updateSettings(_settings.copyWith(mutedChannels: updated)); + } + + Future unmuteChannel(String channelName) async { + final updated = Set.from(_settings.mutedChannels) + ..remove(channelName); + await updateSettings(_settings.copyWith(mutedChannels: updated)); + } } From 230626938448e6e950312161226d10e65fb1131a Mon Sep 17 00:00:00 2001 From: spfmoby <40357319+spfmoby@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:20:55 +0100 Subject: [PATCH 47/99] Better french translations --- lib/l10n/app_fr.arb | 6 +++--- lib/l10n/app_localizations_fr.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e162cdb5..2d4846c8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -262,7 +262,7 @@ } }, "contacts_manageRepeater": "Gérer le répéteur", - "contacts_roomLogin": "Connexion Salle", + "contacts_roomLogin": "Connexion Room Server", "contacts_openChat": "Ouverture du Chat", "contacts_editGroup": "Modifier le groupe", "contacts_deleteGroup": "Supprimer le groupe", @@ -798,7 +798,7 @@ "dialog_disconnect": "Déconnecter", "dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?", "login_repeaterLogin": "Connexion au répéteur", - "login_roomLogin": "Connexion Salle", + "login_roomLogin": "Connexion Room Server", "login_password": "Mot de passe", "login_enterPassword": "Entrez votre mot de passe", "login_savePassword": "Sauvegarder le mot de passe", @@ -1393,7 +1393,7 @@ "settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)", "settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.", "contacts_manageRoom": "Gérer le Room Server", - "room_management": "Administración del Servidor de Habitación", + "room_management": "Administrattion Room Server", "@community_joinConfirmation": { "placeholders": { "name": { diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d572b8f7..c4e1e273 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -696,7 +696,7 @@ class AppLocalizationsFr extends AppLocalizations { String get contacts_manageRoom => 'Gérer le Room Server'; @override - String get contacts_roomLogin => 'Connexion Salle'; + String get contacts_roomLogin => 'Connexion Room Server'; @override String get contacts_openChat => 'Ouverture du Chat'; @@ -1559,7 +1559,7 @@ class AppLocalizationsFr extends AppLocalizations { String get login_repeaterLogin => 'Connexion au répéteur'; @override - String get login_roomLogin => 'Connexion Salle'; + String get login_roomLogin => 'Connexion Room Server'; @override String get login_password => 'Mot de passe'; @@ -1684,7 +1684,7 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_management => 'Gestion des répéteurs'; @override - String get room_management => 'Administración del Servidor de Habitación'; + String get room_management => 'Administrattion Room Server'; @override String get repeater_managementTools => 'Outils de Gestion'; From 7288f11c88299234c394ca490d6fbc1f40ab96b0 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 06:49:14 -0800 Subject: [PATCH 48/99] add chrome in planning --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bad9b6ca..da92d473 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - ✅ **Android**: Full support (API 21+) - ✅ **iOS**: Full support (iOS 12+) - 🚧 **Desktop**: Limited support (macOS/Linux/Windows) +- 🚧 **Web**: Limited support (Chrome) ### Dependencies From c7b33f1d1b1db1e1babf13192edc72671ae12b61 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 06:51:40 -0800 Subject: [PATCH 49/99] readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da92d473..10fb0a57 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - ✅ **Android**: Full support (API 21+) - ✅ **iOS**: Full support (iOS 12+) - 🚧 **Desktop**: Limited support (macOS/Linux/Windows) -- 🚧 **Web**: Limited support (Chrome) +- 🚧 **Web**: Under construction (Chrome) ### Dependencies From 096e0a4184b5e5e8fb5907ab38cbb13f4e4f4630 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:16:07 -0800 Subject: [PATCH 50/99] fix: return cursor to message window after send --- lib/screens/channel_chat_screen.dart | 1 + lib/screens/chat_screen.dart | 1 + lib/screens/repeater_cli_screen.dart | 1 + 3 files changed, 3 insertions(+) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110e..9f406843 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -935,6 +935,7 @@ class _ChannelChatScreenState extends State { connector.sendChannelMessage(widget.channel, messageText); _textController.clear(); _cancelReply(); + _textFieldFocusNode.requestFocus(); } String _formatTime(DateTime time) { diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0b..ea657ffe 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -429,6 +429,7 @@ class _ChatScreenState extends State { connector.sendMessage(widget.contact, text); _textController.clear(); + _textFieldFocusNode.requestFocus(); } void _showPathHistory(BuildContext context) { diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart index abfb06aa..1c7ff438 100644 --- a/lib/screens/repeater_cli_screen.dart +++ b/lib/screens/repeater_cli_screen.dart @@ -168,6 +168,7 @@ class _RepeaterCliScreenState extends State { _commandController.clear(); _historyIndex = -1; + _commandFocusNode.requestFocus(); // Auto-scroll to bottom Future.delayed(const Duration(milliseconds: 100), () { From 3ca53e967c5d39ef496ec5bbda39ba6ec4ad2784 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 09:20:20 -0800 Subject: [PATCH 51/99] fix: to send giphy --- lib/screens/channel_chat_screen.dart | 59 +++++++++++++++++----------- lib/screens/chat_screen.dart | 55 ++++++++++++++++---------- 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110e..9fecdc76 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -849,30 +849,45 @@ class _ChannelChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + fallbackTextColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0b..c5cd4c63 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -340,28 +340,43 @@ class _ChatScreenState extends State { builder: (context, value, child) { final gifId = _parseGifId(value.text); if (gifId != null) { - return Row( - children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: - colorScheme.surfaceContainerHighest, - fallbackTextColor: colorScheme.onSurface - .withValues(alpha: 0.6), - maxSize: 160, + return Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + (event.logicalKey == LogicalKeyboardKey.enter || + event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + _sendMessage(connector); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: + colorScheme.surfaceContainerHighest, + fallbackTextColor: colorScheme.onSurface + .withValues(alpha: 0.6), + maxSize: 160, + ), ), ), - ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => _textController.clear(), - ), - ], + const SizedBox(width: 8), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + _textController.clear(); + _textFieldFocusNode.requestFocus(); + }, + ), + ], + ), ); } From bf4f52a4e30d2c585361c2865bdecc4c82e5b4d0 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:27:06 -0800 Subject: [PATCH 52/99] hide message tracing --- lib/l10n/app_bg.arb | 4 +- lib/l10n/app_de.arb | 4 +- lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 4 +- lib/l10n/app_fr.arb | 4 +- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 12 ++ lib/l10n/app_localizations_bg.dart | 8 + lib/l10n/app_localizations_de.dart | 8 + lib/l10n/app_localizations_en.dart | 7 + lib/l10n/app_localizations_es.dart | 8 + lib/l10n/app_localizations_fr.dart | 8 + lib/l10n/app_localizations_it.dart | 8 + lib/l10n/app_localizations_nl.dart | 7 + lib/l10n/app_localizations_pl.dart | 7 + lib/l10n/app_localizations_pt.dart | 8 + lib/l10n/app_localizations_ru.dart | 8 + lib/l10n/app_localizations_sk.dart | 7 + lib/l10n/app_localizations_sl.dart | 7 + lib/l10n/app_localizations_sv.dart | 7 + lib/l10n/app_localizations_uk.dart | 8 + lib/l10n/app_localizations_zh.dart | 6 + lib/l10n/app_nl.arb | 4 +- lib/l10n/app_pl.arb | 4 +- lib/l10n/app_pt.arb | 4 +- lib/l10n/app_ru.arb | 4 +- lib/l10n/app_sk.arb | 4 +- lib/l10n/app_sl.arb | 4 +- lib/l10n/app_sv.arb | 4 +- lib/l10n/app_uk.arb | 4 +- lib/l10n/app_zh.arb | 4 +- lib/models/app_settings.dart | 6 + lib/screens/app_settings_screen.dart | 12 ++ lib/screens/channel_chat_screen.dart | 231 ++++++++++++++++--------- lib/screens/chat_screen.dart | 225 +++++++++++++++--------- lib/services/app_settings_service.dart | 4 + macos/Podfile.lock | 17 +- 37 files changed, 491 insertions(+), 186 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023e..e9f46c61 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1554,6 +1554,8 @@ "contacts_clipboardEmpty": "Клипборда е празна.", "contacts_invalidAdvertFormat": "Невалидни данни за контакт", "appSettings_languageRu": "Руски", + "appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения", + "appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения", "contacts_contactImported": "Контактът е импортиран.", "contacts_zeroHopAdvert": "Реклама без скок", "contacts_contactImportFailed": "Контактът не е успешно импортиран.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f79..bdea5749 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1554,6 +1554,8 @@ "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", "appSettings_languageUk": "Ukrainisch", + "appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren", + "appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen", "contacts_contactImported": "Kontakt wurde importiert.", "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", "contacts_zeroHopAdvert": "Zero-Hop-Ankündigung", @@ -1745,4 +1747,4 @@ "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72ee..0e96e468 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -183,6 +183,8 @@ "appSettings_languageBg": "Български", "appSettings_languageRu": "Русский", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Enable Message Tracing", + "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_notifications": "Notifications", "appSettings_enableNotifications": "Enable Notifications", "appSettings_enableNotificationsSubtitle": "Receive notifications for messages and adverts", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d33..99db15dd 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1553,6 +1553,8 @@ "appSettings_languageUk": "Ucraniano", "contacts_clipboardEmpty": "El portapapeles está vacío.", "appSettings_languageRu": "Ruso", + "appSettings_enableMessageTracing": "Habilitar seguimiento de mensajes", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes", "contacts_invalidAdvertFormat": "Datos de contacto no válidos", "contacts_floodAdvert": "Anuncio de inundación", "contacts_contactImported": "El contacto ha sido importado.", @@ -1745,4 +1747,4 @@ "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e162cdb5..8f5a40b6 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1553,6 +1553,8 @@ "contacts_invalidAdvertFormat": "Données de contact non valides", "appSettings_languageUk": "Ukrainien", "appSettings_languageRu": "Russe", + "appSettings_enableMessageTracing": "Activer le traçage des messages", + "appSettings_enableMessageTracingSubtitle": "Afficher les métadonnées détaillées de routage et de synchronisation des messages", "contacts_clipboardEmpty": "Le presse-papiers est vide.", "contacts_contactImported": "Le contact a été importé.", "contacts_floodAdvert": "Annonce à tout le réseau", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186f..fe4bffc4 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1553,6 +1553,8 @@ "appSettings_languageRu": "Russo", "contacts_invalidAdvertFormat": "Dati di contatto non validi", "appSettings_languageUk": "Ucraino", + "appSettings_enableMessageTracing": "Abilita tracciamento messaggi", + "appSettings_enableMessageTracingSubtitle": "Mostra metadati dettagliati su instradamento e tempi per i messaggi", "contacts_zeroHopAdvert": "Annuncio Zero Hop", "contacts_floodAdvert": "Annuncio alluvionale", "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686cef..6097f867 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -970,6 +970,18 @@ abstract class AppLocalizations { /// **'Українська'** String get appSettings_languageUk; + /// No description provided for @appSettings_enableMessageTracing. + /// + /// In en, this message translates to: + /// **'Enable Message Tracing'** + String get appSettings_enableMessageTracing; + + /// No description provided for @appSettings_enableMessageTracingSubtitle. + /// + /// In en, this message translates to: + /// **'Show detailed routing and timing metadata for messages'** + String get appSettings_enableMessageTracingSubtitle; + /// No description provided for @appSettings_notifications. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b5..94f9f7aa 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -466,6 +466,14 @@ class AppLocalizationsBg extends AppLocalizations { @override String get appSettings_languageUk => 'Украински'; + @override + String get appSettings_enableMessageTracing => + 'Разрешаване на проследяване на съобщения'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4b..ba0f5daa 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -460,6 +460,14 @@ class AppLocalizationsDe extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainisch'; + @override + String get appSettings_enableMessageTracing => + 'Nachrichtenverfolgung aktivieren'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen'; + @override String get appSettings_notifications => 'Benachrichtigungen'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4a..ce5b2f05 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -458,6 +458,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => 'Enable Message Tracing'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Show detailed routing and timing metadata for messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde3653..3c7838d1 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -463,6 +463,14 @@ class AppLocalizationsEs extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Habilitar seguimiento de mensajes'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes'; + @override String get appSettings_notifications => 'Notificaciones'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d572b8f7..a8851a24 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -464,6 +464,14 @@ class AppLocalizationsFr extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainien'; + @override + String get appSettings_enableMessageTracing => + 'Activer le traçage des messages'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Afficher les métadonnées détaillées de routage et de synchronisation des messages'; + @override String get appSettings_notifications => 'Notifications'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f87..b3c41e75 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -462,6 +462,14 @@ class AppLocalizationsIt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraino'; + @override + String get appSettings_enableMessageTracing => + 'Abilita tracciamento messaggi'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostra metadati dettagliati su instradamento e tempi per i messaggi'; + @override String get appSettings_notifications => 'Notifiche'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b0..0ff00ada 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -460,6 +460,13 @@ class AppLocalizationsNl extends AppLocalizations { @override String get appSettings_languageUk => 'Oekraïens'; + @override + String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Gedetailleerde routerings- en timing-metadata voor berichten weergeven'; + @override String get appSettings_notifications => 'Notificaties'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b56..d6c1e15d 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -464,6 +464,13 @@ class AppLocalizationsPl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukraińska'; + @override + String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości'; + @override String get appSettings_notifications => 'Powiadomienia'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826f..a300ba93 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -464,6 +464,14 @@ class AppLocalizationsPt extends AppLocalizations { @override String get appSettings_languageUk => 'Ucraniano'; + @override + String get appSettings_enableMessageTracing => + 'Ativar rastreamento de mensagens'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Mostrar metadados detalhados de roteamento e tempo para as mensagens'; + @override String get appSettings_notifications => 'Notificações'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c84..c4c16332 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -462,6 +462,14 @@ class AppLocalizationsRu extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Включить трассировку сообщений'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показывать подробные метаданные о маршрутизации и времени для сообщений'; + @override String get appSettings_notifications => 'Уведомления'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6dea..0df70a68 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -460,6 +460,13 @@ class AppLocalizationsSk extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinská'; + @override + String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Zobraziť podrobné metadáta o smerovaní a časovaní správ'; + @override String get appSettings_notifications => 'Upozornenia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e30584..4be105ec 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -459,6 +459,13 @@ class AppLocalizationsSl extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrajinsko'; + @override + String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil'; + @override String get appSettings_notifications => 'Obvestila'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b88..52fa5318 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -457,6 +457,13 @@ class AppLocalizationsSv extends AppLocalizations { @override String get appSettings_languageUk => 'Ukrainska'; + @override + String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden'; + @override String get appSettings_notifications => 'Meddelanden'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f3670021..4847009e 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -462,6 +462,14 @@ class AppLocalizationsUk extends AppLocalizations { @override String get appSettings_languageUk => 'Українська'; + @override + String get appSettings_enableMessageTracing => + 'Увімкнути відстеження повідомлень'; + + @override + String get appSettings_enableMessageTracingSubtitle => + 'Показувати детальні метадані про маршрутизацію та час для повідомлень'; + @override String get appSettings_notifications => 'Сповіщення'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800d..454f127c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -446,6 +446,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get appSettings_languageUk => '乌克兰'; + @override + String get appSettings_enableMessageTracing => '启用消息追踪'; + + @override + String get appSettings_enableMessageTracingSubtitle => '显示消息的详细路由和时间元数据'; + @override String get appSettings_notifications => '通知'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdda..2f39fdf1 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1557,6 +1557,8 @@ "contacts_floodAdvert": "Overstromingsadvertentie", "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", "appSettings_languageRu": "Russisch", + "appSettings_enableMessageTracing": "Berichttracking inschakelen", + "appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven", "contacts_clipboardEmpty": "Knipbord is leeg.", "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", "contacts_contactImported": "Contact is geïmporteerd.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa71..0432f8f6 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1552,6 +1552,8 @@ "contacts_chatTraceRoute": "Śledź trasę promienia", "appSettings_languageRu": "Rosyjski", "appSettings_languageUk": "Ukraińska", + "appSettings_enableMessageTracing": "Włącz śledzenie wiadomości", + "appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości", "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", "contacts_zeroHopAdvert": "Reklama Zero Hop", "contacts_floodAdvert": "Reklama powodziowa", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be66945..01c5a83c 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", "appSettings_languageRu": "Russo", + "appSettings_enableMessageTracing": "Ativar rastreamento de mensagens", + "appSettings_enableMessageTracingSubtitle": "Mostrar metadados detalhados de roteamento e tempo para as mensagens", "contacts_ShareContact": "Copiar contato para Área de Transferência", "contacts_contactImportFailed": "Contato falhou ao ser importado.", "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3b..b8a20d95 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -796,6 +796,8 @@ "contacts_invalidAdvertFormat": "Недействительные контактные данные", "contacts_zeroHopAdvert": "Реклама Zero Hop", "appSettings_languageUk": "Українська", + "appSettings_enableMessageTracing": "Включить трассировку сообщений", + "appSettings_enableMessageTracingSubtitle": "Показывать подробные метаданные о маршрутизации и времени для сообщений", "contacts_floodAdvert": "Рекламный поток", "contacts_clipboardEmpty": "Буфер обмена пуст.", "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", @@ -957,4 +959,4 @@ "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0ae..32452828 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", "appSettings_languageRu": "Ruština", + "appSettings_enableMessageTracing": "Povoliť sledovanie správ", + "appSettings_enableMessageTracingSubtitle": "Zobraziť podrobné metadáta o smerovaní a časovaní správ", "contacts_addContactFromClipboard": "Pridať kontakt z schránky", "contacts_contactImported": "Kontakt bol importovaný.", "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d3..c560c31f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1552,6 +1552,8 @@ "contacts_pathTraceTo": "Trace route to {name}", "appSettings_languageRu": "Ruščina", "appSettings_languageUk": "Ukrajinsko", + "appSettings_enableMessageTracing": "Omogoči sledenje sporočilom", + "appSettings_enableMessageTracingSubtitle": "Prikaži podrobne metapodatke o usmerjanju in časovnem usklajevanju sporočil", "contacts_contactImported": "Kontakt je bil uvožen.", "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", "contacts_zeroHopAdvert": "Reklama brez posrednikov", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399c..b93c5ca0 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1558,6 +1558,8 @@ "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", "appSettings_languageUk": "Ukrainska", + "appSettings_enableMessageTracing": "Aktivera meddelandespårning", + "appSettings_enableMessageTracingSubtitle": "Visa detaljerade metadata om dirigering och tidsinställningar för meddelanden", "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", "contacts_contactImported": "Kontakt har importerats.", "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0e..235e4ede 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1559,6 +1559,8 @@ "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", "contacts_clipboardEmpty": "Буфер обміну порожній", "appSettings_languageRu": "Російська", + "appSettings_enableMessageTracing": "Увімкнути відстеження повідомлень", + "appSettings_enableMessageTracingSubtitle": "Показувати детальні метадані про маршрутизацію та час для повідомлень", "contacts_ShareContact": "Копіювати контакт у буфер обміну", "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7f..72f48adc 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -176,6 +176,8 @@ "appSettings_languageBg": "保加利亚", "appSettings_languageRu": "俄语", "appSettings_languageUk": "乌克兰", + "appSettings_enableMessageTracing": "启用消息追踪", + "appSettings_enableMessageTracingSubtitle": "显示消息的详细路由和时间元数据", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", @@ -1717,4 +1719,4 @@ "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} +} \ No newline at end of file diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index d9504b39..62ba9ca6 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -22,6 +22,7 @@ class AppSettings { final bool mapKeyPrefixEnabled; final String mapKeyPrefix; final bool mapShowMarkers; + final bool enableMessageTracing; final Map? mapCacheBounds; final int mapCacheMinZoom; final int mapCacheMaxZoom; @@ -47,6 +48,7 @@ class AppSettings { this.mapKeyPrefixEnabled = false, this.mapKeyPrefix = '', this.mapShowMarkers = true, + this.enableMessageTracing = false, this.mapCacheBounds, this.mapCacheMinZoom = 10, this.mapCacheMaxZoom = 15, @@ -76,6 +78,7 @@ class AppSettings { 'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix': mapKeyPrefix, 'map_show_markers': mapShowMarkers, + 'enable_message_tracing': enableMessageTracing, 'map_cache_bounds': mapCacheBounds, 'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_max_zoom': mapCacheMaxZoom, @@ -112,6 +115,7 @@ class AppSettings { mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapShowMarkers: json['map_show_markers'] as bool? ?? true, + enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( (key, value) => MapEntry(key.toString(), (value as num).toDouble()), ), @@ -155,6 +159,7 @@ class AppSettings { bool? mapKeyPrefixEnabled, String? mapKeyPrefix, bool? mapShowMarkers, + bool? enableMessageTracing, Object? mapCacheBounds = _unset, int? mapCacheMinZoom, int? mapCacheMaxZoom, @@ -180,6 +185,7 @@ class AppSettings { mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, + enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, mapCacheBounds: mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map?, diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index b309b4db..a2c920e7 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -82,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget { trailing: const Icon(Icons.chevron_right), onTap: () => _showLanguageDialog(context, settingsService), ), + const Divider(height: 1), + SwitchListTile( + secondary: const Icon(Icons.location_searching), + title: Text(context.l10n.appSettings_enableMessageTracing), + subtitle: Text( + context.l10n.appSettings_enableMessageTracingSubtitle, + ), + value: settingsService.settings.enableMessageTracing, + onChanged: (value) { + settingsService.setEnableMessageTracing(value); + }, + ), ], ), ); diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bf05110e..8bb004e4 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -17,6 +17,7 @@ import '../helpers/utf8_length_limiter.dart'; import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; +import '../services/app_settings_service.dart'; import '../utils/emoji_utils.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; @@ -263,6 +264,8 @@ class _ChannelChatScreenState extends State { } Widget _buildMessageBubble(ChannelMessage message) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final gifId = _parseGifId(message.text); final poi = _parsePoiMessage(message.text); @@ -336,108 +339,166 @@ class _ChannelChatScreenState extends State { if (poi != null) _buildPoiMessage(context, poi, isOutgoing) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: isOutgoing - ? Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface - .withValues(alpha: 0.6), - ), + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: isOutgoing + ? Theme.of(context) + .colorScheme + .onPrimaryContainer + .withValues(alpha: 0.7) + : Theme.of(context).colorScheme.onSurface + .withValues(alpha: 0.6), + ), + ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (displayPath.isNotEmpty) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - 'via ${_formatPathPrefixes(displayPath)}', - style: TextStyle( - fontSize: 11, - color: Colors.grey[600], - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Row( + Row( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), + Flexible( + child: Linkify( + text: message.text, + style: const TextStyle(fontSize: 14), + linkStyle: const TextStyle( + fontSize: 14, + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), + ), + ), + if (!enableTracing && isOutgoing) ...[ + const SizedBox(width: 4), + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Icons.check_circle + : message.status == ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + ? Colors.green + : message.status == ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, + ), + ), + ], + ], + ), + if (enableTracing) ...[ + if (displayPath.isNotEmpty) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + 'via ${_formatPathPrefixes(displayPath)}', style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), - if (message.repeatCount > 0) ...[ - const SizedBox(width: 6), - Icon( - Icons.repeat, - size: 12, - color: Colors.grey[600], - ), - const SizedBox(width: 2), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ Text( - '${message.repeatCount}', + _formatTime(message.timestamp), style: TextStyle( fontSize: 11, color: Colors.grey[600], ), ), + if (message.repeatCount > 0) ...[ + const SizedBox(width: 6), + Icon( + Icons.repeat, + size: 12, + color: Colors.grey[600], + ), + const SizedBox(width: 2), + Text( + '${message.repeatCount}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + ), + ), + ], + if (isOutgoing) ...[ + const SizedBox(width: 4), + Icon( + message.status == ChannelMessageStatus.sent + ? Icons.check + : message.status == + ChannelMessageStatus.pending + ? Icons.schedule + : Icons.error_outline, + size: 14, + color: + message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey[600], + ), + ], ], - if (isOutgoing) ...[ - const SizedBox(width: 4), - Icon( - message.status == ChannelMessageStatus.sent - ? Icons.check - : message.status == - ChannelMessageStatus.pending - ? Icons.schedule - : Icons.error_outline, - size: 14, - color: - message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey[600], - ), - ], - ], + ), ), - ), + ], ], ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index ad897a0b..f97d4bb8 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -20,6 +20,7 @@ import '../models/channel_message.dart'; import '../models/contact.dart'; import '../models/message.dart'; import '../models/path_history.dart'; +import '../services/app_settings_service.dart'; import '../services/path_history_service.dart'; import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; @@ -1172,6 +1173,8 @@ class _MessageBubble extends StatelessWidget { @override Widget build(BuildContext context) { + final settingsService = context.watch(); + final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; final colorScheme = Theme.of(context).colorScheme; final gifId = _parseGifId(message.text); @@ -1251,100 +1254,158 @@ class _MessageBubble extends StatelessWidget { if (poi != null) _buildPoiMessage(context, poi, textColor, metaColor) else if (gifId != null) - ClipRRect( - borderRadius: BorderRadius.circular(12), - child: GifMessage( - url: - 'https://media.giphy.com/media/$gifId/giphy.gif', - backgroundColor: Colors.transparent, - fallbackTextColor: textColor.withValues( - alpha: 0.7, + Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GifMessage( + url: + 'https://media.giphy.com/media/$gifId/giphy.gif', + backgroundColor: Colors.transparent, + fallbackTextColor: textColor.withValues( + alpha: 0.7, + ), + ), ), - ), + if (!enableTracing && isOutgoing) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + shape: BoxShape.circle, + ), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.white70, + ), + ), + ), + ], ) else - Linkify( - text: messageText, - style: TextStyle(color: textColor), - linkStyle: const TextStyle( - color: Colors.green, - decoration: TextDecoration.underline, - ), - options: const LinkifyOptions( - humanize: false, - defaultToHttps: false, - ), - linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), - ), - if (isOutgoing && message.retryCount > 0) ...[ - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.symmetric(horizontal: 8) - : EdgeInsets.zero, - child: Text( - context.l10n.chat_retryCount( - message.retryCount, - 4, - ), - style: TextStyle( - fontSize: 10, - color: metaColor, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - const SizedBox(height: 4), - Padding( - padding: gifId != null - ? const EdgeInsets.only( - left: 8, - right: 8, - bottom: 4, - ) - : EdgeInsets.zero, - child: Wrap( - spacing: 4, - crossAxisAlignment: WrapCrossAlignment.center, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - _formatTime(message.timestamp), - style: TextStyle( - fontSize: 10, - color: metaColor, + Flexible( + child: Linkify( + text: messageText, + style: TextStyle(color: textColor), + linkStyle: const TextStyle( + color: Colors.green, + decoration: TextDecoration.underline, + ), + options: const LinkifyOptions( + humanize: false, + defaultToHttps: false, + ), + linkifiers: const [UrlLinkifier()], + onOpen: (link) => + LinkHandler.handleLinkTap(context, link.url), ), ), - if (isOutgoing) ...[ + if (!enableTracing && isOutgoing) ...[ const SizedBox(width: 4), - _buildStatusIcon(metaColor), - ], - if (message.tripTimeMs != null && - message.status == - MessageStatus.delivered) ...[ - const SizedBox(width: 4), - Icon( - Icons.speed, - size: 10, - color: isOutgoing - ? metaColor - : Colors.green[700], - ), - Text( - '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', - style: TextStyle( - fontSize: 9, - color: isOutgoing - ? metaColor - : Colors.green[700], + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Icon( + (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Icons.check_circle + : message.status == MessageStatus.failed + ? Icons.cancel + : Icons.cloud, + size: 14, + color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + ? Colors.green + : message.status == MessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], ], ), - ), + if (enableTracing) ...[ + if (isOutgoing && message.retryCount > 0) ...[ + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.symmetric(horizontal: 8) + : EdgeInsets.zero, + child: Text( + context.l10n.chat_retryCount( + message.retryCount, + 4, + ), + style: TextStyle( + fontSize: 10, + color: metaColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + const SizedBox(height: 4), + Padding( + padding: gifId != null + ? const EdgeInsets.only( + left: 8, + right: 8, + bottom: 4, + ) + : EdgeInsets.zero, + child: Wrap( + spacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + _formatTime(message.timestamp), + style: TextStyle( + fontSize: 10, + color: metaColor, + ), + ), + if (isOutgoing) ...[ + const SizedBox(width: 4), + _buildStatusIcon(metaColor), + ], + if (message.tripTimeMs != null && + message.status == + MessageStatus.delivered) ...[ + const SizedBox(width: 4), + Icon( + Icons.speed, + size: 10, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + Text( + '${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s', + style: TextStyle( + fontSize: 9, + color: isOutgoing + ? metaColor + : Colors.green[700], + ), + ), + ], + ], + ), + ), + ], ], ), ), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index e80f903d..eacf26f9 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier { await updateSettings(_settings.copyWith(mapShowMarkers: value)); } + Future setEnableMessageTracing(bool value) async { + await updateSettings(_settings.copyWith(enableMessageTracing: value)); + } + Future setMapCacheBounds(Map? value) async { await updateSettings(_settings.copyWith(mapCacheBounds: value)); } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 65fed266..8224cfba 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -5,10 +5,13 @@ PODS: - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - mobile_scanner (6.0.2): + - mobile_scanner (7.0.0): + - Flutter - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -24,8 +27,9 @@ DEPENDENCIES: - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -39,9 +43,11 @@ EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral mobile_scanner: - :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/macos + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin sqflite_darwin: @@ -53,10 +59,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3 - flutter_local_notifications: 13862b132e32eb858dea558a86d45d08daeacfe7 + flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 + share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd From 35498c1b9046a958206c174e582760fa31bd67cf Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:31:56 -0800 Subject: [PATCH 53/99] formatting fix --- lib/screens/channel_chat_screen.dart | 52 ++++++++++++++++++---------- lib/screens/chat_screen.dart | 50 +++++++++++++++++--------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 8bb004e4..9b9de350 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -367,17 +367,24 @@ class _ChannelChatScreenState extends State { shape: BoxShape.circle, ), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -402,8 +409,10 @@ class _ChannelChatScreenState extends State { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -411,17 +420,24 @@ class _ChannelChatScreenState extends State { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Icons.check_circle - : message.status == ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + ChannelMessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty) + color: + (message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty) ? Colors.green - : message.status == ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + : message.status == + ChannelMessageStatus.failed + ? Colors.red + : Colors.grey, ), ), ], diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index f97d4bb8..32a7882b 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1274,21 +1274,30 @@ class _MessageBubble extends StatelessWidget { child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), + color: Colors.black.withValues( + alpha: 0.3, + ), shape: BoxShape.circle, ), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + : message.status == + MessageStatus.failed + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.white70, + : message.status == + MessageStatus.failed + ? Colors.red + : Colors.white70, ), ), ), @@ -1312,8 +1321,10 @@ class _MessageBubble extends StatelessWidget { defaultToHttps: false, ), linkifiers: const [UrlLinkifier()], - onOpen: (link) => - LinkHandler.handleLinkTap(context, link.url), + onOpen: (link) => LinkHandler.handleLinkTap( + context, + link.url, + ), ), ), if (!enableTracing && isOutgoing) ...[ @@ -1321,17 +1332,22 @@ class _MessageBubble extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 2), child: Icon( - (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Icons.check_circle : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, + ? Icons.cancel + : Icons.cloud, size: 14, - color: (message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty) + color: + (message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty) ? Colors.green : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + ? Colors.red + : Colors.grey, ), ), ], From d269e181c38d01c017d6bbeb9098bf4e3b6d7164 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Sun, 22 Feb 2026 11:34:18 -0800 Subject: [PATCH 54/99] formatting fix --- lib/screens/channel_chat_screen.dart | 10 ++++++---- lib/screens/chat_screen.dart | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9fecdc76..5173852f 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -854,7 +854,8 @@ class _ChannelChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(); return KeyEventResult.handled; } @@ -871,9 +872,10 @@ class _ChannelChatScreenState extends State { backgroundColor: Theme.of( context, ).colorScheme.surfaceContainerHighest, - fallbackTextColor: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), + fallbackTextColor: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), maxSize: 160, ), ), diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index c5cd4c63..fb31c00e 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -345,7 +345,8 @@ class _ChatScreenState extends State { onKeyEvent: (node, event) { if (event is KeyDownEvent && (event.logicalKey == LogicalKeyboardKey.enter || - event.logicalKey == LogicalKeyboardKey.numpadEnter)) { + event.logicalKey == + LogicalKeyboardKey.numpadEnter)) { _sendMessage(connector); return KeyEventResult.handled; } From 332bb5ef3afcb64eef89ceccf143ac5ac2570391 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:06:08 -0500 Subject: [PATCH 55/99] Updated PR and Added snackbar Translations --- .local-agent/AGENTS.local.md | 48 +++++++++++++++++++++++++ .local-agent/memory.local.md | 6 ++++ lib/l10n/app_en.arb | 8 +++++ lib/l10n/app_localizations.dart | 6 ++++ lib/l10n/app_localizations_bg.dart | 5 +++ lib/l10n/app_localizations_de.dart | 5 +++ lib/l10n/app_localizations_en.dart | 5 +++ lib/l10n/app_localizations_es.dart | 5 +++ lib/l10n/app_localizations_fr.dart | 5 +++ lib/l10n/app_localizations_it.dart | 5 +++ lib/l10n/app_localizations_nl.dart | 5 +++ lib/l10n/app_localizations_pl.dart | 5 +++ lib/l10n/app_localizations_pt.dart | 5 +++ lib/l10n/app_localizations_ru.dart | 5 +++ lib/l10n/app_localizations_sk.dart | 5 +++ lib/l10n/app_localizations_sl.dart | 5 +++ lib/l10n/app_localizations_sv.dart | 5 +++ lib/l10n/app_localizations_uk.dart | 5 +++ lib/l10n/app_localizations_zh.dart | 5 +++ lib/screens/channels_screen.dart | 16 +++------ untranslated.json | 58 +++++++++++++++++++++++++++++- 21 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 .local-agent/AGENTS.local.md create mode 100644 .local-agent/memory.local.md diff --git a/.local-agent/AGENTS.local.md b/.local-agent/AGENTS.local.md new file mode 100644 index 00000000..2c61ef8b --- /dev/null +++ b/.local-agent/AGENTS.local.md @@ -0,0 +1,48 @@ +# Local Agent Operating Rules (Untracked) + +This file is NOT version-controlled. +It overrides default agent behavior for this workstation only. + +--- + +## Core Behavior + +- Always search the codebase before editing. +- Produce a short plan before modifying BLE or protocol logic. +- Never modify BLE frame structure or command codes without explicit approval. +- After editing connector code, re-check command/response mappings. +- Never perform destructive operations (delete files, mass refactor) without confirmation. + +--- + +## Protocol Discipline + +- maxFrameSize must remain 172 unless explicitly instructed. +- Identity hash size is 1 byte (PATH_HASH_SIZE). +- Companion radio formats must not change silently. +- Command codes and response codes must remain backward-compatible. + +--- + +## Coding Discipline + +- Keep modifications minimal. +- Prefer refactoring over rewriting. +- Follow existing Flutter patterns (StatelessWidget + Consumer). +- Avoid premature abstraction. +- Explain what changed and why. + +--- + +## Learning Mode + +When discovering: +- a working build command +- a protocol quirk +- a confirmed packet layout rule + +Append a concise bullet to: + +.local-agent/memory.local.md + +Keep memory under 15 bullets max. diff --git a/.local-agent/memory.local.md b/.local-agent/memory.local.md new file mode 100644 index 00000000..a714fbcc --- /dev/null +++ b/.local-agent/memory.local.md @@ -0,0 +1,6 @@ +\# Local Learned Patterns (Machine-Specific) + +(empty) + + + diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72ee..6af00bc5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -363,6 +363,14 @@ } } }, + "channels_channelDeleteFailed": "Failed to delete channel \"{name}\"", + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_channelDeleted": "Channel \"{name}\" deleted", "@channels_channelDeleted": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686cef..80308439 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1570,6 +1570,12 @@ abstract class AppLocalizations { /// **'Delete \"{name}\"? This cannot be undone.'** String channels_deleteChannelConfirm(String name); + /// No description provided for @channels_channelDeleteFailed. + /// + /// In en, this message translates to: + /// **'Failed to delete channel \"{name}\"'** + String channels_channelDeleteFailed(String name); + /// No description provided for @channels_channelDeleted. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b5..25b06312 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -812,6 +812,11 @@ class AppLocalizationsBg extends AppLocalizations { return 'Изтрий \"$name\"? Това не може да бъде отменено.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Каналът \"$name\" е изтрит'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4b..9ba8409c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -809,6 +809,11 @@ class AppLocalizationsDe extends AppLocalizations { return 'Löschen von \"$name\"? Dies kann nicht rückgängig gemacht werden.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanal \"$name\" gelöscht'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4a..146edb82 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -801,6 +801,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'Delete \"$name\"? This cannot be undone.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Channel \"$name\" deleted'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde3653..6c9a517c 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -810,6 +810,11 @@ class AppLocalizationsEs extends AppLocalizations { return 'Eliminar \"$name\"? Esto no se puede deshacer.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Canal \"$name\" eliminado'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c4e1e273..3b99772f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -812,6 +812,11 @@ class AppLocalizationsFr extends AppLocalizations { return 'Supprimer $name? Cela ne peut pas être annulé.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Le canal \"$name\" a été supprimé'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f87..7447fad4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -808,6 +808,11 @@ class AppLocalizationsIt extends AppLocalizations { return 'Eliminare \"$name\"? Non può essere annullato.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Canale \"$name\" eliminato'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b0..6e4a2af5 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -806,6 +806,11 @@ class AppLocalizationsNl extends AppLocalizations { return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanaal \"$name\" verwijderd'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b56..dee97c0c 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -811,6 +811,11 @@ class AppLocalizationsPl extends AppLocalizations { return 'Usuń \"$name\"? Nie można tego cofnąć.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanał \"$name\" usunięto'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826f..22c5a2e8 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -811,6 +811,11 @@ class AppLocalizationsPt extends AppLocalizations { return 'Excluir \"$name\"? Não pode ser desfeito.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Canal \"$name\" excluído'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c84..5afbd871 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -809,6 +809,11 @@ class AppLocalizationsRu extends AppLocalizations { return 'Удалить \"$name\"? Это действие нельзя отменить.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Канал \"$name\" удалён'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6dea..44f5cbfd 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -806,6 +806,11 @@ class AppLocalizationsSk extends AppLocalizations { return 'Odstrániť \"$name\"? To sa nedá zrušiť.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanál \"$name\" bol odstránený'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e30584..455d5aa7 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -804,6 +804,11 @@ class AppLocalizationsSl extends AppLocalizations { return 'Izbrišem \"$name\"? To se ne da povrniti.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanal \"$name\" izbrisan.'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b88..6f94870e 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -800,6 +800,11 @@ class AppLocalizationsSv extends AppLocalizations { return 'Radera \"$name\"? Detta kan inte ångras.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Kanalen \"$name\" raderad'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f3670021..1d6cf230 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -807,6 +807,11 @@ class AppLocalizationsUk extends AppLocalizations { return 'Видалити $name? Це не можна скасувати.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return 'Канал «$name» видалено'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800d..b297297b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -769,6 +769,11 @@ class AppLocalizationsZh extends AppLocalizations { return 'Delete \"$name\"? This cannot be undone.'; } + @override + String channels_channelDeleteFailed(String name) { + return 'Failed to delete channel \"$name\"'; + } + @override String channels_channelDeleted(String name) { return '删除频道 \"$name\"'; diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 892ac636..d2a2e3de 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1524,18 +1524,14 @@ class _ChannelsScreenState extends State try { await connector.deleteChannel(channel.index); - channelMessageStore.clearChannelMessages( - channel.index, - ); + channelMessageStore.clearChannelMessages(channel.index); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.l10n.channels_channelDeleted( - channel.name, - ), + context.l10n.channels_channelDeleted(channel.name), ), ), ); @@ -1545,17 +1541,13 @@ class _ChannelsScreenState extends State ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - context.l10n.channels_channelDeleteFailed( - channel.name, - ), + context.l10n.channels_channelDeleteFailed(channel.name), ), ), ); // Preserve existing logging (if it was there) - debugPrint( - 'Failed to delete channel: $e\n$st', - ); + debugPrint('Failed to delete channel: $e\n$st'); } }, child: Text( diff --git a/untranslated.json b/untranslated.json index 9e26dfee..d68e7535 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,57 @@ -{} \ No newline at end of file +{ + "bg": [ + "channels_channelDeleteFailed" + ], + + "de": [ + "channels_channelDeleteFailed" + ], + + "es": [ + "channels_channelDeleteFailed" + ], + + "fr": [ + "channels_channelDeleteFailed" + ], + + "it": [ + "channels_channelDeleteFailed" + ], + + "nl": [ + "channels_channelDeleteFailed" + ], + + "pl": [ + "channels_channelDeleteFailed" + ], + + "pt": [ + "channels_channelDeleteFailed" + ], + + "ru": [ + "channels_channelDeleteFailed" + ], + + "sk": [ + "channels_channelDeleteFailed" + ], + + "sl": [ + "channels_channelDeleteFailed" + ], + + "sv": [ + "channels_channelDeleteFailed" + ], + + "uk": [ + "channels_channelDeleteFailed" + ], + + "zh": [ + "channels_channelDeleteFailed" + ] +} From ab76a52d71cf1d476b43cab72b5a5b05522f2a86 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:07:19 -0500 Subject: [PATCH 56/99] Delete .local-agent/AGENTS.local.md --- .local-agent/AGENTS.local.md | 48 ------------------------------------ 1 file changed, 48 deletions(-) delete mode 100644 .local-agent/AGENTS.local.md diff --git a/.local-agent/AGENTS.local.md b/.local-agent/AGENTS.local.md deleted file mode 100644 index 2c61ef8b..00000000 --- a/.local-agent/AGENTS.local.md +++ /dev/null @@ -1,48 +0,0 @@ -# Local Agent Operating Rules (Untracked) - -This file is NOT version-controlled. -It overrides default agent behavior for this workstation only. - ---- - -## Core Behavior - -- Always search the codebase before editing. -- Produce a short plan before modifying BLE or protocol logic. -- Never modify BLE frame structure or command codes without explicit approval. -- After editing connector code, re-check command/response mappings. -- Never perform destructive operations (delete files, mass refactor) without confirmation. - ---- - -## Protocol Discipline - -- maxFrameSize must remain 172 unless explicitly instructed. -- Identity hash size is 1 byte (PATH_HASH_SIZE). -- Companion radio formats must not change silently. -- Command codes and response codes must remain backward-compatible. - ---- - -## Coding Discipline - -- Keep modifications minimal. -- Prefer refactoring over rewriting. -- Follow existing Flutter patterns (StatelessWidget + Consumer). -- Avoid premature abstraction. -- Explain what changed and why. - ---- - -## Learning Mode - -When discovering: -- a working build command -- a protocol quirk -- a confirmed packet layout rule - -Append a concise bullet to: - -.local-agent/memory.local.md - -Keep memory under 15 bullets max. From f4dd76a4591b4062930cdcd3bee654a4f1d017bc Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:07:32 -0500 Subject: [PATCH 57/99] Delete .local-agent/memory.local.md --- .local-agent/memory.local.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .local-agent/memory.local.md diff --git a/.local-agent/memory.local.md b/.local-agent/memory.local.md deleted file mode 100644 index a714fbcc..00000000 --- a/.local-agent/memory.local.md +++ /dev/null @@ -1,6 +0,0 @@ -\# Local Learned Patterns (Machine-Specific) - -(empty) - - - From 47044ae14e9f8ae2f35edc087d84f866d9ae0858 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:37:10 -0500 Subject: [PATCH 58/99] fix(l10n): add channels_channelDeleteFailed with proper placeholder typing and translations --- lib/l10n/app_bg.arb | 4 ++- lib/l10n/app_de.arb | 4 ++- lib/l10n/app_es.arb | 4 ++- lib/l10n/app_fr.arb | 4 ++- lib/l10n/app_it.arb | 4 ++- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 4 ++- lib/l10n/app_pl.arb | 4 ++- lib/l10n/app_pt.arb | 4 ++- lib/l10n/app_ru.arb | 4 ++- lib/l10n/app_sk.arb | 4 ++- lib/l10n/app_sl.arb | 4 ++- lib/l10n/app_sv.arb | 4 ++- lib/l10n/app_uk.arb | 4 ++- lib/l10n/app_zh.arb | 4 ++- pubspec.lock | 20 +++++------ untranslated.json | 58 +----------------------------- 30 files changed, 67 insertions(+), 95 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023e..6725c245 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "bg", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f79..dbd06060 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "de", "appTitle": "MeshCore Open", "nav_contacts": "Kontakte", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d33..52e7c254 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "es", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2d4846c8..0e96ba84 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "fr", "appTitle": "MeshCore Open", "nav_contacts": "Contacts", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186f..421fcb07 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "it", "appTitle": "MeshCore Open", "nav_contacts": "Contatti", diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 25b06312..3876b3c0 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -814,7 +814,7 @@ class AppLocalizationsBg extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Неуспешно изтриване на канала \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 9ba8409c..96cfee13 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -811,7 +811,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kanal $name konnte nicht gelöscht werden'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 6c9a517c..d0e50281 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -812,7 +812,7 @@ class AppLocalizationsEs extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'No se pudo eliminar el canal \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3b99772f..6ad03350 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -814,7 +814,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Échec de la suppression de la chaîne \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 7447fad4..b53acb50 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -810,7 +810,7 @@ class AppLocalizationsIt extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Impossibile eliminare il canale \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 6e4a2af5..fa684e2a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -808,7 +808,7 @@ class AppLocalizationsNl extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kan kanaal $name niet verwijderen'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index dee97c0c..b62d4b68 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -813,7 +813,7 @@ class AppLocalizationsPl extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Nie udało się usunąć kanału \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 22c5a2e8..34713e6d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -813,7 +813,7 @@ class AppLocalizationsPt extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Falha ao excluir o canal \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5afbd871..f0de018f 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -811,7 +811,7 @@ class AppLocalizationsRu extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Не удалось удалить канал $name.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 44f5cbfd..b826056d 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -808,7 +808,7 @@ class AppLocalizationsSk extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kanál \"$name\" sa nepodarilo odstrániť'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 455d5aa7..5f2d1a48 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -806,7 +806,7 @@ class AppLocalizationsSl extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Kanala $name ni bilo mogoče izbrisati'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6f94870e..6cf18b30 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -802,7 +802,7 @@ class AppLocalizationsSv extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Det gick inte att ta bort kanalen \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1d6cf230..7869afa6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -809,7 +809,7 @@ class AppLocalizationsUk extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return 'Не вдалося видалити канал \"$name\"'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b297297b..ac612632 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -771,7 +771,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String channels_channelDeleteFailed(String name) { - return 'Failed to delete channel \"$name\"'; + return '无法删除频道 \"$name\"'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdda..f1586460 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "nl", "appTitle": "MeshCore Open", "nav_contacts": "Contacten", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa71..2eb67dde 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pl", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be66945..4b947ad8 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pt", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3b..f9ab2d35 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "ru", "appTitle": "MeshCore Open", "nav_contacts": "Контакты", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0ae..e9653ec6 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sk", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d3..384f669d 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sl", "appTitle": "MeshCore Open", "nav_contacts": "Stiki", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399c..39422fce 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sv", "appTitle": "MeshCore Open", "nav_contacts": "Kontakter", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0e..bb0dd926 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "uk", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7f..6bbfe487 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,4 +1,6 @@ -{ +{ + "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", + "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "zh", "appTitle": "MeshCore Open", "nav_contacts": "联系方式", diff --git a/pubspec.lock b/pubspec.lock index ed84c404..09e93017 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -497,26 +497,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -910,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: diff --git a/untranslated.json b/untranslated.json index d68e7535..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,57 +1 @@ -{ - "bg": [ - "channels_channelDeleteFailed" - ], - - "de": [ - "channels_channelDeleteFailed" - ], - - "es": [ - "channels_channelDeleteFailed" - ], - - "fr": [ - "channels_channelDeleteFailed" - ], - - "it": [ - "channels_channelDeleteFailed" - ], - - "nl": [ - "channels_channelDeleteFailed" - ], - - "pl": [ - "channels_channelDeleteFailed" - ], - - "pt": [ - "channels_channelDeleteFailed" - ], - - "ru": [ - "channels_channelDeleteFailed" - ], - - "sk": [ - "channels_channelDeleteFailed" - ], - - "sl": [ - "channels_channelDeleteFailed" - ], - - "sv": [ - "channels_channelDeleteFailed" - ], - - "uk": [ - "channels_channelDeleteFailed" - ], - - "zh": [ - "channels_channelDeleteFailed" - ] -} +{} \ No newline at end of file From f3db63ceeaf590c8042a75999356b6a5be51c1af Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:37:58 -0500 Subject: [PATCH 59/99] Delete pubspec.lock --- pubspec.lock | 1095 -------------------------------------------------- 1 file changed, 1095 deletions(-) delete mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 09e93017..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,1095 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" - url: "https://pub.dev" - source: hosted - version: "4.0.7" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - bluez: - dependency: transitive - description: - name: bluez - sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" - url: "https://pub.dev" - source: hosted - version: "0.8.3" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - characters: - dependency: "direct main" - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" - source: hosted - version: "0.3.5+2" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - dart_earcut: - dependency: transitive - description: - name: dart_earcut - sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dart_polylabel2: - dependency: transitive - description: - name: dart_polylabel2 - sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" - source: hosted - version: "0.7.12" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blue_plus: - dependency: "direct main" - description: - name: flutter_blue_plus - sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - flutter_blue_plus_android: - dependency: transitive - description: - name: flutter_blue_plus_android - sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_darwin: - dependency: transitive - description: - name: flutter_blue_plus_darwin - sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" - url: "https://pub.dev" - source: hosted - version: "8.1.1" - flutter_blue_plus_linux: - dependency: transitive - description: - name: flutter_blue_plus_linux - sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_platform_interface: - dependency: transitive - description: - name: flutter_blue_plus_platform_interface - sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_web: - dependency: transitive - description: - name: flutter_blue_plus_web - sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_winrt: - dependency: transitive - description: - name: flutter_blue_plus_winrt - sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 - url: "https://pub.dev" - source: hosted - version: "0.0.18" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_foreground_task: - dependency: "direct main" - description: - name: flutter_foreground_task - sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" - url: "https://pub.dev" - source: hosted - version: "9.2.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" - source: hosted - version: "0.14.4" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" - url: "https://pub.dev" - source: hosted - version: "20.1.0" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a - url: "https://pub.dev" - source: hosted - version: "2.0.1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_map: - dependency: "direct main" - description: - name: flutter_map - sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" - url: "https://pub.dev" - source: hosted - version: "8.2.2" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - gpx: - dependency: "direct main" - description: - name: gpx - sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e - url: "https://pub.dev" - source: hosted - version: "2.3.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" - url: "https://pub.dev" - source: hosted - version: "4.7.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" - url: "https://pub.dev" - source: hosted - version: "4.10.0" - latlong2: - dependency: "direct main" - description: - name: latlong2 - sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" - url: "https://pub.dev" - source: hosted - version: "0.9.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - lists: - dependency: transitive - description: - name: lists - sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - logger: - dependency: transitive - description: - name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 - url: "https://pub.dev" - source: hosted - version: "2.6.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mgrs_dart: - dependency: transitive - description: - name: mgrs_dart - sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 - url: "https://pub.dev" - source: hosted - version: "7.1.4" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" - url: "https://pub.dev" - source: hosted - version: "7.0.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: "direct main" - description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - posix: - dependency: transitive - description: - name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" - url: "https://pub.dev" - source: hosted - version: "6.0.3" - proj4dart: - dependency: transitive - description: - name: proj4dart - sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e - url: "https://pub.dev" - source: hosted - version: "2.1.0" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - qr_flutter: - dependency: "direct main" - description: - name: qr_flutter - sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f - url: "https://pub.dev" - source: hosted - version: "2.4.20" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - timezone: - dependency: transitive - description: - name: timezone - sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - unicode: - dependency: transitive - description: - name: unicode - sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 - url: "https://pub.dev" - source: hosted - version: "4.5.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - wkt_parser: - dependency: transitive - description: - name: wkt_parser - sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" From 298951f8bc39951f7fe6ed612ca7bb356111429b Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:43:37 -0500 Subject: [PATCH 60/99] bring up to current main --- pubspec.lock | 1095 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1095 insertions(+) create mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 00000000..492eb4d4 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1095 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.dev" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + bluez: + dependency: transitive + description: + name: bluez + sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" + url: "https://pub.dev" + source: hosted + version: "0.8.3" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: "direct main" + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.dev" + source: hosted + version: "0.7.12" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_blue_plus_android: + dependency: transitive + description: + name: flutter_blue_plus_android + sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_darwin: + dependency: transitive + description: + name: flutter_blue_plus_darwin + sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" + url: "https://pub.dev" + source: hosted + version: "8.1.1" + flutter_blue_plus_linux: + dependency: transitive + description: + name: flutter_blue_plus_linux + sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_platform_interface: + dependency: transitive + description: + name: flutter_blue_plus_platform_interface + sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_web: + dependency: transitive + description: + name: flutter_blue_plus_web + sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" + url: "https://pub.dev" + source: hosted + version: "8.1.0" + flutter_blue_plus_winrt: + dependency: transitive + description: + name: flutter_blue_plus_winrt + sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 + url: "https://pub.dev" + source: hosted + version: "0.0.18" + flutter_cache_manager: + dependency: "direct main" + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_foreground_task: + dependency: "direct main" + description: + name: flutter_foreground_task + sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" + url: "https://pub.dev" + source: hosted + version: "9.2.0" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" + url: "https://pub.dev" + source: hosted + version: "0.14.4" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" + url: "https://pub.dev" + source: hosted + version: "20.1.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + gpx: + dependency: "direct main" + description: + name: gpx + sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e + url: "https://pub.dev" + source: hosted + version: "2.3.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + url: "https://pub.dev" + source: hosted + version: "4.7.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + url: "https://pub.dev" + source: hosted + version: "4.10.0" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 + url: "https://pub.dev" + source: hosted + version: "7.1.4" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.dev" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: "direct main" + description: + name: pointycastle + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.dev" + source: hosted + version: "6.0.3" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.dev" + source: hosted + version: "6.1.5+1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.dev" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f + url: "https://pub.dev" + source: hosted + version: "2.4.20" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + url: "https://pub.dev" + source: hosted + version: "0.7.9" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" \ No newline at end of file From 0f2d18d6fa797f7d7ed9bb05af4fd3a0f67cc810 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:39:52 -0500 Subject: [PATCH 61/99] feat: add custom los icon --- lib/icons/los_icon.dart | 26 ++++++++++ lib/screens/line_of_sight_map_screen.dart | 3 +- lib/screens/map_screen.dart | 3 +- pubspec.lock | 60 +++++++++++++++++++---- pubspec.yaml | 1 + 5 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 lib/icons/los_icon.dart diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart new file mode 100644 index 00000000..309cc7d7 --- /dev/null +++ b/lib/icons/los_icon.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class LosIcon extends StatelessWidget { + final double size; + final Color? color; + + const LosIcon({super.key, this.size = 24, this.color}); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; + return SvgPicture.string( + _losSvg, + width: size, + height: size, + colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), + ); + } +} + +const String _losSvg = ''' + + + +'''; diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685a..dfda1c18 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -16,6 +16,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; class LineOfSightEndpoint { final String label; @@ -642,7 +643,7 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _loading ? null : _runLos, - icon: const Icon(Icons.visibility), + icon: const LosIcon(), label: Text(context.l10n.losRun), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 77ec98ce..b688a30f 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -20,6 +20,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -280,7 +281,7 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) IconButton( - icon: const Icon(Icons.visibility), + icon: const LosIcon(), onPressed: () { final candidates = []; if (connector.selfLatitude != null && diff --git a/pubspec.lock b/pubspec.lock index ed84c404..d9d480f6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -497,26 +505,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mgrs_dart: dependency: transitive description: @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -910,10 +926,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" timezone: dependency: transitive description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3624b932..54e36487 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + flutter_svg: ^2.0.10 dev_dependencies: flutter_test: From 08edd2696ee33a8376d2fb244d3bc6cf0ca6e560 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:47:49 -0500 Subject: [PATCH 62/99] Revert "feat: add custom los icon" This reverts commit 0f2d18d6fa797f7d7ed9bb05af4fd3a0f67cc810. --- lib/icons/los_icon.dart | 26 ---------- lib/screens/line_of_sight_map_screen.dart | 3 +- lib/screens/map_screen.dart | 3 +- pubspec.lock | 60 ++++------------------- pubspec.yaml | 1 - 5 files changed, 12 insertions(+), 81 deletions(-) delete mode 100644 lib/icons/los_icon.dart diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart deleted file mode 100644 index 309cc7d7..00000000 --- a/lib/icons/los_icon.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class LosIcon extends StatelessWidget { - final double size; - final Color? color; - - const LosIcon({super.key, this.size = 24, this.color}); - - @override - Widget build(BuildContext context) { - final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; - return SvgPicture.string( - _losSvg, - width: size, - height: size, - colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), - ); - } -} - -const String _losSvg = ''' - - - -'''; diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index dfda1c18..b073685a 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -16,7 +16,6 @@ import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; -import '../icons/los_icon.dart'; class LineOfSightEndpoint { final String label; @@ -643,7 +642,7 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _loading ? null : _runLos, - icon: const LosIcon(), + icon: const Icon(Icons.visibility), label: Text(context.l10n.losRun), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b688a30f..77ec98ce 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -20,7 +20,6 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; -import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -281,7 +280,7 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) IconButton( - icon: const LosIcon(), + icon: const Icon(Icons.visibility), onPressed: () { final candidates = []; if (connector.selfLatitude != null && diff --git a/pubspec.lock b/pubspec.lock index d9d480f6..ed84c404 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -347,14 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -505,26 +497,26 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -605,14 +597,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: "direct main" description: @@ -926,10 +910,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: transitive description: @@ -1026,30 +1010,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 54e36487..3624b932 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,6 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 - flutter_svg: ^2.0.10 dev_dependencies: flutter_test: From aaf79c90c91a987527d811a6dfe22142c3d07724 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:01:13 -0500 Subject: [PATCH 63/99] feat: show los elevation icon --- lib/icons/los_icon.dart | 80 +++++++++++++++++++++++ lib/screens/line_of_sight_map_screen.dart | 3 +- lib/screens/map_screen.dart | 3 +- 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 lib/icons/los_icon.dart diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart new file mode 100644 index 00000000..86bef060 --- /dev/null +++ b/lib/icons/los_icon.dart @@ -0,0 +1,80 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +class LosIcon extends StatelessWidget { + final double size; + final Color? color; + + const LosIcon({ + super.key, + this.size = 24, + this.color, + }); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; + final canvasSize = size; + + return SizedBox( + width: canvasSize, + height: canvasSize, + child: CustomPaint( + painter: _LosIconPainter(iconColor), + ), + ); + } +} + +class _LosIconPainter extends CustomPainter { + final Color color; + + _LosIconPainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.fill; + + final path = Path() + ..moveTo(82, -120) + ..relativeLineTo(258, -360) + ..relativeLineTo(202, 0) + ..relativeLineTo(298, -348) + ..relativeLineTo(0, 708) + ..lineTo(82, -120) + ..close() + ..moveTo(152, -353) + ..relativeLineTo(-64, -46) + ..relativeLineTo(172, -241) + ..relativeLineTo(202, 0) + ..relativeLineTo(188, -219) + ..relativeLineTo(60, 52) + ..relativeLineTo(-212, 247) + ..lineTo(300, -560) + ..lineTo(152, -353) + ..close() + ..moveTo(238, -200) + ..relativeLineTo(522, 0) + ..relativeLineTo(0, -412) + ..lineTo(578, -400) + ..lineTo(380, -400) + ..lineTo(238, -200) + ..close(); + + final scale = math.min(size.width, size.height) / 960; + + canvas.save(); + canvas.translate(0, 960); + canvas.scale(scale, scale); + canvas.drawPath(path, paint); + canvas.restore(); + } + + @override + bool shouldRepaint(covariant _LosIconPainter oldDelegate) { + return oldDelegate.color != color; + } +} diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685a..dfda1c18 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -16,6 +16,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; class LineOfSightEndpoint { final String label; @@ -642,7 +643,7 @@ class _LineOfSightMapScreenState extends State { alignment: Alignment.centerRight, child: ElevatedButton.icon( onPressed: _loading ? null : _runLos, - icon: const Icon(Icons.visibility), + icon: const LosIcon(), label: Text(context.l10n.losRun), ), ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 77ec98ce..b688a30f 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -20,6 +20,7 @@ import '../services/map_tile_cache_service.dart'; import '../utils/contact_search.dart'; import '../utils/route_transitions.dart'; import '../widgets/quick_switch_bar.dart'; +import '../icons/los_icon.dart'; import 'channels_screen.dart'; import 'chat_screen.dart'; import 'contacts_screen.dart'; @@ -280,7 +281,7 @@ class _MapScreenState extends State { ), if (!_isBuildingPathTrace) IconButton( - icon: const Icon(Icons.visibility), + icon: const LosIcon(), onPressed: () { final candidates = []; if (connector.selfLatitude != null && From 9bcb8b9ca67d6478dff1ca3fcf3b6481bca5d1dd Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:36:49 -0500 Subject: [PATCH 64/99] feat: render los elevation via svg --- assets/icons/los_elevation.svg | 5 +++ lib/icons/los_icon.dart | 68 +++------------------------------- pubspec.lock | 68 +++++++++++++++++++++++++++------- pubspec.yaml | 2 + 4 files changed, 67 insertions(+), 76 deletions(-) create mode 100644 assets/icons/los_elevation.svg diff --git a/assets/icons/los_elevation.svg b/assets/icons/los_elevation.svg new file mode 100644 index 00000000..78c7a1b8 --- /dev/null +++ b/assets/icons/los_elevation.svg @@ -0,0 +1,5 @@ + + + diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart index 86bef060..fef6a451 100644 --- a/lib/icons/los_icon.dart +++ b/lib/icons/los_icon.dart @@ -1,6 +1,5 @@ -import 'dart:math' as math; - import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; class LosIcon extends StatelessWidget { final double size; @@ -15,66 +14,11 @@ class LosIcon extends StatelessWidget { @override Widget build(BuildContext context) { final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; - final canvasSize = size; - - return SizedBox( - width: canvasSize, - height: canvasSize, - child: CustomPaint( - painter: _LosIconPainter(iconColor), - ), + return SvgPicture.asset( + 'assets/icons/los_elevation.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), ); } } - -class _LosIconPainter extends CustomPainter { - final Color color; - - _LosIconPainter(this.color); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - final path = Path() - ..moveTo(82, -120) - ..relativeLineTo(258, -360) - ..relativeLineTo(202, 0) - ..relativeLineTo(298, -348) - ..relativeLineTo(0, 708) - ..lineTo(82, -120) - ..close() - ..moveTo(152, -353) - ..relativeLineTo(-64, -46) - ..relativeLineTo(172, -241) - ..relativeLineTo(202, 0) - ..relativeLineTo(188, -219) - ..relativeLineTo(60, 52) - ..relativeLineTo(-212, 247) - ..lineTo(300, -560) - ..lineTo(152, -353) - ..close() - ..moveTo(238, -200) - ..relativeLineTo(522, 0) - ..relativeLineTo(0, -412) - ..lineTo(578, -400) - ..lineTo(380, -400) - ..lineTo(238, -200) - ..close(); - - final scale = math.min(size.width, size.height) / 960; - - canvas.save(); - canvas.translate(0, 960); - canvas.scale(scale, scale); - canvas.drawPath(path, paint); - canvas.restore(); - } - - @override - bool shouldRepaint(covariant _LosIconPainter oldDelegate) { - return oldDelegate.color != color; - } -} diff --git a/pubspec.lock b/pubspec.lock index ed84c404..266bfb7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: archive - sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.9" args: dependency: transitive description: @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -401,10 +409,10 @@ packages: dependency: transitive description: name: image - sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.8.0" intl: dependency: "direct main" description: @@ -417,10 +425,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.11.0" latlong2: dependency: "direct main" description: @@ -537,10 +545,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: c6184bf2913dd66be244108c9c27ca04b01caf726321c44b0e7a7a1e32d41044 + sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce url: "https://pub.dev" source: hosted - version: "7.1.4" + version: "7.2.0" native_toolchain_c: dependency: transitive description: @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -649,10 +665,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -681,10 +697,10 @@ packages: dependency: transitive description: name: posix - sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.5.0" proj4dart: dependency: transitive description: @@ -1006,10 +1022,34 @@ packages: dependency: "direct main" description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3624b932..3dc40d51 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + flutter_svg: ^2.0.10 dev_dependencies: flutter_test: @@ -87,6 +88,7 @@ flutter: assets: - assets/images/ + - assets/icons/los_elevation.svg flutter_launcher_icons: android: true From bd27c90216c70eadec03e375d3de9aeb60537626 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:42:36 -0500 Subject: [PATCH 65/99] feat: render los elevation via material symbol --- assets/icons/los_elevation.svg | 5 ---- lib/icons/los_icon.dart | 19 +++++++++----- pubspec.lock | 48 ++++++---------------------------- pubspec.yaml | 3 +-- 4 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 assets/icons/los_elevation.svg diff --git a/assets/icons/los_elevation.svg b/assets/icons/los_elevation.svg deleted file mode 100644 index 78c7a1b8..00000000 --- a/assets/icons/los_elevation.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart index fef6a451..43c6cdc7 100644 --- a/lib/icons/los_icon.dart +++ b/lib/icons/los_icon.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:material_symbols_icons/material_symbols_icons.dart'; class LosIcon extends StatelessWidget { final double size; @@ -13,12 +13,17 @@ class LosIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final iconColor = color ?? IconTheme.of(context).color ?? Colors.black; - return SvgPicture.asset( - 'assets/icons/los_elevation.svg', - width: size, - height: size, - colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), + final theme = Theme.of(context); + final iconTheme = IconTheme.of(context); + final iconColor = color ?? + iconTheme.color ?? + theme.iconTheme.color ?? + theme.colorScheme.onSurface; + + return Icon( + Symbols.elevation, + size: size, + color: iconColor, ); } } diff --git a/pubspec.lock b/pubspec.lock index 266bfb7d..9605e966 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,14 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -517,6 +509,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.0" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 + url: "https://pub.dev" + source: hosted + version: "4.2906.0" meta: dependency: transitive description: @@ -605,14 +605,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1026,30 +1018,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.3" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3dc40d51..7d6b5c2f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 - flutter_svg: ^2.0.10 + material_symbols_icons: ^4.2906.0 dev_dependencies: flutter_test: @@ -88,7 +88,6 @@ flutter: assets: - assets/images/ - - assets/icons/los_elevation.svg flutter_launcher_icons: android: true From 1f816f7e087d5c85be2c763d22bf19a4a11951ab Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:06:25 -0500 Subject: [PATCH 66/99] ran dart format . on libs/icons/los_icon.dart --- lib/icons/los_icon.dart | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/icons/los_icon.dart b/lib/icons/los_icon.dart index 43c6cdc7..58d75b00 100644 --- a/lib/icons/los_icon.dart +++ b/lib/icons/los_icon.dart @@ -5,25 +5,18 @@ class LosIcon extends StatelessWidget { final double size; final Color? color; - const LosIcon({ - super.key, - this.size = 24, - this.color, - }); + const LosIcon({super.key, this.size = 24, this.color}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final iconTheme = IconTheme.of(context); - final iconColor = color ?? + final iconColor = + color ?? iconTheme.color ?? theme.iconTheme.color ?? theme.colorScheme.onSurface; - return Icon( - Symbols.elevation, - size: size, - color: iconColor, - ); + return Icon(Symbols.elevation, size: size, color: iconColor); } } From 2bdd9d35cc8a1eee151bb2d353df845087c72841 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:53:41 -0500 Subject: [PATCH 67/99] feat: show radio horizon on los profile --- lib/screens/line_of_sight_map_screen.dart | 154 +++++++++++++++++++++- lib/services/line_of_sight_service.dart | 4 + 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index b073685a..101f0138 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -943,7 +943,10 @@ class _LosProfilePainter extends CustomPainter { terrainPath.lineTo(size.width, size.height); terrainPath.close(); - canvas.drawPath(terrainPath, Paint()..color = const Color(0xCC7C6F5D)); + const terrainFillColor = Color(0xCC7C6F5D); + const terrainLineColor = Color(0xFF9FE870); + const losLineColor = Color(0xFFE0E7FF); + canvas.drawPath(terrainPath, Paint()..color = terrainFillColor); final terrainLine = ui.Path(); for (int i = 0; i < samples.length; i++) { @@ -957,7 +960,7 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( terrainLine, Paint() - ..color = const Color(0xFF9FE870) + ..color = terrainLineColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); @@ -977,10 +980,64 @@ class _LosProfilePainter extends CustomPainter { canvas.drawPath( losLine, Paint() - ..color = const Color(0xFFE0E7FF) + ..color = losLineColor ..style = PaintingStyle.stroke ..strokeWidth = 2, ); + + final horizonLine = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].radioHorizonMeters, + ); + if (i == 0) { + horizonLine.moveTo(p.dx, p.dy); + } else { + horizonLine.lineTo(p.dx, p.dy); + } + } + const horizonLineColor = Color(0xFF4BC0FF); + final horizonPaint = Paint() + ..color = horizonLineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + canvas.drawPath(horizonLine, horizonPaint); + + final capPath = ui.Path(); + for (int i = 0; i < samples.length; i++) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].radioHorizonMeters, + ); + if (i == 0) { + capPath.moveTo(p.dx, p.dy); + } else { + capPath.lineTo(p.dx, p.dy); + } + } + for (int i = samples.length - 1; i >= 0; i--) { + final p = mapPoint( + samples[i].distanceMeters, + samples[i].lineHeightMeters, + ); + capPath.lineTo(p.dx, p.dy); + } + capPath.close(); + const horizonFillColor = Color(0x404BC0FF); + canvas.drawPath( + capPath, + Paint() + ..color = horizonFillColor + ..style = PaintingStyle.fill, + ); + + _drawLegend( + canvas, + horizonLineColor, + losLineColor, + terrainLineColor, + ); } @override @@ -1000,4 +1057,95 @@ class _LosProfilePainter extends CustomPainter { ..layout(); painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); } + + void _drawLegend( + Canvas canvas, + Color horizonColor, + Color losColor, + Color terrainColor, + ) { + const legendX = 8.0; + const legendY = 8.0; + const swatchSize = 10.0; + const swatchTextGap = 6.0; + const entrySpacing = 4.0; + const legendPadding = 6.0; + + final entries = [ + _LegendEntry('Terrain', terrainColor), + _LegendEntry('LOS beam', losColor), + _LegendEntry('Radio horizon', horizonColor), + ]; + + final textStyle = badgeTextStyle.copyWith( + fontSize: 10, + fontWeight: FontWeight.w500, + ); + + final painters = entries.map((entry) { + final painter = TextPainter( + text: TextSpan(text: entry.label, style: textStyle), + textDirection: TextDirection.ltr, + )..layout(); + return painter; + }).toList(); + + final maxTextWidth = painters.map((p) => p.width).fold( + 0, + math.max, + ); + + final legendWidth = + legendPadding * 2 + swatchSize + swatchTextGap + maxTextWidth; + + final legendHeight = legendPadding * 2 + + entries.length * swatchSize + + (entries.length - 1) * entrySpacing; + + final legendRect = RRect.fromLTRBR( + legendX, + legendY, + legendX + legendWidth, + legendY + legendHeight, + const Radius.circular(10), + ); + + canvas.drawRRect( + legendRect, + Paint()..color = const Color.fromARGB(90, 0, 0, 0), + ); + + var yOffset = legendY + legendPadding; + for (int i = 0; i < entries.length; i++) { + final entry = entries[i]; + final painter = painters[i]; + final swatchRect = Rect.fromLTWH( + legendX + legendPadding, + yOffset, + swatchSize, + swatchSize, + ); + canvas.drawRect( + swatchRect, + Paint()..color = entry.color, + ); + + painter.paint( + canvas, + Offset( + swatchRect.right + swatchTextGap, + yOffset + (swatchSize - painter.height) / 2, + ), + ); + + yOffset += swatchSize + entrySpacing; + } + } +} + +class _LegendEntry { + final String label; + final Color color; + + const _LegendEntry(this.label, this.color); } diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index e9f9f7b6..b73ab51d 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -12,12 +12,14 @@ class LineOfSightSample { final double distanceMeters; final double terrainMeters; final double lineHeightMeters; + final double radioHorizonMeters; final double clearanceMeters; const LineOfSightSample({ required this.distanceMeters, required this.terrainMeters, required this.lineHeightMeters, + required this.radioHorizonMeters, required this.clearanceMeters, }); } @@ -238,6 +240,7 @@ class LineOfSightService { (2 * effectiveEarthRadius); final terrainHeight = elevations[i] + earthBulge; final clearance = lineHeight - terrainHeight; + final radioHorizonHeight = lineHeight - earthBulge; if (clearance < -obstructionToleranceMeters) { isClear = false; @@ -253,6 +256,7 @@ class LineOfSightService { distanceMeters: distanceFromStart, terrainMeters: terrainHeight, lineHeightMeters: lineHeight, + radioHorizonMeters: radioHorizonHeight, clearanceMeters: clearance, ), ); From fc55fb98ce8211166bf3e44a4285b884f43e22e8 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:42:58 -0500 Subject: [PATCH 68/99] Document LOS frequency and k-factor math Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact --- lib/screens/line_of_sight_map_screen.dart | 152 ++++++++++++++---- lib/services/line_of_sight_service.dart | 47 +++++- test/services/line_of_sight_service_test.dart | 2 + 3 files changed, 166 insertions(+), 35 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 101f0138..5eb532b8 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -14,6 +14,7 @@ import '../services/app_settings_service.dart'; import '../services/line_of_sight_service.dart'; import '../services/map_tile_cache_service.dart'; import '../utils/route_transitions.dart'; +import '../connector/meshcore_connector.dart'; import '../widgets/app_bar.dart'; import '../widgets/quick_switch_bar.dart'; @@ -110,10 +111,13 @@ class _LineOfSightMapScreenState extends State { }); try { + final connector = context.read(); + final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], startAntennaHeightMeters: startAntenna, endAntennaHeightMeters: endAntenna, + frequencyMHz: frequencyMHz, ); if (!mounted) return; if (!_isRunRequestCurrent( @@ -424,6 +428,12 @@ class _LineOfSightMapScreenState extends State { Widget _buildControlPanel(bool isImperial) { _sanitizeSelection(); final segment = _primarySegmentResult(); + final connector = context.watch(); + final reportedFrequencyMHz = _normalizeFrequencyMHz( + connector.currentFreqHz, + ); + final displayFrequencyMHz = segment?.frequencyMHz ?? reportedFrequencyMHz; + final kFactorUsed = segment?.usedKFactor; final endpoints = _visibleEndpoints(); final distanceUnit = isImperial ? 'mi' : 'km'; final heightUnit = isImperial ? 'ft' : 'm'; @@ -488,6 +498,52 @@ class _LineOfSightMapScreenState extends State { ), ), const SizedBox(height: 4), + if (displayFrequencyMHz != null) + Padding( + padding: const EdgeInsets.only(top: 2, bottom: 4), + child: Row( + children: [ + Text( + 'Frequency', + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Text( + '${displayFrequencyMHz.toStringAsFixed(3)} MHz', + style: TextStyle(fontSize: 11, color: Colors.grey[700]), + ), + if (kFactorUsed != null) ...[ + const SizedBox(width: 8), + Text( + 'k=${kFactorUsed.toStringAsFixed(3)}', + style: TextStyle( + fontSize: 11, + color: Colors.grey[700], + ), + ), + const SizedBox(width: 4), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + icon: const Icon(Icons.info_outline, size: 16), + color: Colors.grey[600], + tooltip: 'View calculation details', + onPressed: () { + _showFrequencyInfoDialog( + context, + displayFrequencyMHz, + kFactorUsed, + ); + }, + ), + ], + ], + ), + ), Text( context.l10n.losElevationAttribution, style: TextStyle(fontSize: 10, color: Colors.grey[700]), @@ -896,6 +952,56 @@ class _LineOfSightMapScreenState extends State { break; } } + + void _showFrequencyInfoDialog( + BuildContext context, + double frequencyMHz, + double kFactor, + ) { + final baselineFreq = LineOfSightService.baselineFrequencyMHz; + final baselineK = LineOfSightService.baselineKFactor; + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Radio horizon calculation'), + content: Text.rich( + TextSpan( + children: [ + TextSpan( + text: + 'Starting from k=$baselineK at ${baselineFreq.toStringAsFixed(3)} MHz, ', + ), + const TextSpan(text: 'the calculation multiplies the offset by '), + TextSpan( + text: + '0.15 × (frequency − ${baselineFreq.toStringAsFixed(3)}) / ${baselineFreq.toStringAsFixed(3)} ', + ), + TextSpan( + text: + 'to get k ≈ ${kFactor.toStringAsFixed(3)} for the current ${frequencyMHz.toStringAsFixed(3)} MHz band, ', + ), + const TextSpan( + text: 'which defines the curved radio horizon cap.', + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(context.l10n.common_ok), + ), + ], + ), + ); + } + + double? _normalizeFrequencyMHz(int? frequencyHz) { + if (frequencyHz == null || frequencyHz <= 0) return null; + if (frequencyHz >= 1000000) return frequencyHz / 1e6; + if (frequencyHz >= 1000) return frequencyHz / 1e3; + return frequencyHz.toDouble(); + } } class _LosProfilePainter extends CustomPainter { @@ -985,30 +1091,32 @@ class _LosProfilePainter extends CustomPainter { ..strokeWidth = 2, ); - final horizonLine = ui.Path(); + const refractedLineColor = Color(0xFFFFD57F); + final refractedLine = ui.Path(); for (int i = 0; i < samples.length; i++) { final p = mapPoint( samples[i].distanceMeters, - samples[i].radioHorizonMeters, + samples[i].refractedHeightMeters, ); if (i == 0) { - horizonLine.moveTo(p.dx, p.dy); + refractedLine.moveTo(p.dx, p.dy); } else { - horizonLine.lineTo(p.dx, p.dy); + refractedLine.lineTo(p.dx, p.dy); } } - const horizonLineColor = Color(0xFF4BC0FF); - final horizonPaint = Paint() - ..color = horizonLineColor - ..style = PaintingStyle.stroke - ..strokeWidth = 1.5; - canvas.drawPath(horizonLine, horizonPaint); + canvas.drawPath( + refractedLine, + Paint() + ..color = refractedLineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + ); final capPath = ui.Path(); for (int i = 0; i < samples.length; i++) { final p = mapPoint( samples[i].distanceMeters, - samples[i].radioHorizonMeters, + samples[i].refractedHeightMeters, ); if (i == 0) { capPath.moveTo(p.dx, p.dy); @@ -1024,7 +1132,7 @@ class _LosProfilePainter extends CustomPainter { capPath.lineTo(p.dx, p.dy); } capPath.close(); - const horizonFillColor = Color(0x404BC0FF); + const horizonFillColor = Color(0x40FFD57F); canvas.drawPath( capPath, Paint() @@ -1032,12 +1140,7 @@ class _LosProfilePainter extends CustomPainter { ..style = PaintingStyle.fill, ); - _drawLegend( - canvas, - horizonLineColor, - losLineColor, - terrainLineColor, - ); + _drawLegend(canvas, refractedLineColor, losLineColor, terrainLineColor); } @override @@ -1090,15 +1193,13 @@ class _LosProfilePainter extends CustomPainter { return painter; }).toList(); - final maxTextWidth = painters.map((p) => p.width).fold( - 0, - math.max, - ); + final maxTextWidth = painters.map((p) => p.width).fold(0, math.max); final legendWidth = legendPadding * 2 + swatchSize + swatchTextGap + maxTextWidth; - final legendHeight = legendPadding * 2 + + final legendHeight = + legendPadding * 2 + entries.length * swatchSize + (entries.length - 1) * entrySpacing; @@ -1125,10 +1226,7 @@ class _LosProfilePainter extends CustomPainter { swatchSize, swatchSize, ); - canvas.drawRect( - swatchRect, - Paint()..color = entry.color, - ); + canvas.drawRect(swatchRect, Paint()..color = entry.color); painter.paint( canvas, diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index b73ab51d..14d8fc6f 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -12,14 +12,14 @@ class LineOfSightSample { final double distanceMeters; final double terrainMeters; final double lineHeightMeters; - final double radioHorizonMeters; + final double refractedHeightMeters; final double clearanceMeters; const LineOfSightSample({ required this.distanceMeters, required this.terrainMeters, required this.lineHeightMeters, - required this.radioHorizonMeters, + required this.refractedHeightMeters, required this.clearanceMeters, }); } @@ -32,6 +32,8 @@ class LineOfSightResult { final double? firstObstructionDistanceMeters; final List samples; final String? errorMessage; + final double usedKFactor; + final double? frequencyMHz; const LineOfSightResult({ required this.hasData, @@ -40,6 +42,8 @@ class LineOfSightResult { required this.maxObstructionMeters, required this.firstObstructionDistanceMeters, required this.samples, + required this.usedKFactor, + this.frequencyMHz, this.errorMessage, }); @@ -50,7 +54,9 @@ class LineOfSightResult { isClear = false, maxObstructionMeters = 0, firstObstructionDistanceMeters = null, - samples = const []; + samples = const [], + usedKFactor = 4.0 / 3.0, + frequencyMHz = null; } class LineOfSightPathSegment { @@ -91,6 +97,11 @@ class LineOfSightService { static const Duration _cacheTtl = Duration(hours: 24); static const int _maxFetchAttempts = 4; // initial try + 3 retries static const Duration _initialBackoff = Duration(milliseconds: 300); + static const double _baselineFrequencyMHz = 915.0; + static const double _baselineKFactor = 4.0 / 3.0; + + static double get baselineFrequencyMHz => _baselineFrequencyMHz; + static double get baselineKFactor => _baselineKFactor; final http.Client _httpClient; final bool _ownsHttpClient; @@ -108,7 +119,7 @@ class LineOfSightService { List points, { double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) async { if (points.length < 2) { @@ -125,6 +136,7 @@ class LineOfSightService { var blockedSegments = 0; var unknownSegments = 0; + final kFactor = _kFactorForFrequency(frequencyMHz); for (int i = 0; i < points.length - 1; i++) { final result = await analyzeLink( points[i], @@ -132,6 +144,7 @@ class LineOfSightService { startAntennaHeightMeters: startAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters, kFactor: kFactor, + frequencyMHz: frequencyMHz, obstructionToleranceMeters: obstructionToleranceMeters, ); segments.add( @@ -165,7 +178,8 @@ class LineOfSightService { LatLng end, { double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + required double kFactor, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) async { final totalDistanceMeters = _distance.as(LengthUnit.Meter, start, end); @@ -177,6 +191,8 @@ class LineOfSightService { maxObstructionMeters: 0, firstObstructionDistanceMeters: null, samples: const [], + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -205,7 +221,8 @@ class LineOfSightService { required List elevations, double startAntennaHeightMeters = 1.5, double endAntennaHeightMeters = 1.5, - double kFactor = 4.0 / 3.0, + required double kFactor, + double? frequencyMHz, double obstructionToleranceMeters = 0.0, }) { if (points.length < 2 || elevations.length != points.length) { @@ -240,7 +257,10 @@ class LineOfSightService { (2 * effectiveEarthRadius); final terrainHeight = elevations[i] + earthBulge; final clearance = lineHeight - terrainHeight; - final radioHorizonHeight = lineHeight - earthBulge; + final unrefBulge = + (distanceFromStart * (totalDistanceMeters - distanceFromStart)) / + (2 * _earthRadiusMeters); + final refractedHeight = lineHeight + (unrefBulge - earthBulge); if (clearance < -obstructionToleranceMeters) { isClear = false; @@ -256,7 +276,7 @@ class LineOfSightService { distanceMeters: distanceFromStart, terrainMeters: terrainHeight, lineHeightMeters: lineHeight, - radioHorizonMeters: radioHorizonHeight, + refractedHeightMeters: refractedHeight, clearanceMeters: clearance, ), ); @@ -269,9 +289,20 @@ class LineOfSightService { maxObstructionMeters: maxObstructionMeters, firstObstructionDistanceMeters: firstObstructionDistanceMeters, samples: samples, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } + static double _kFactorForFrequency(double? frequencyMHz) { + if (frequencyMHz == null) return _baselineKFactor; + final delta = + (frequencyMHz - _baselineFrequencyMHz) / _baselineFrequencyMHz; + final adjustment = delta * 0.15; + final scaled = _baselineKFactor * (1 + adjustment); + return scaled.clamp(1.1, 1.6).toDouble(); + } + List _buildSamplePoints( LatLng start, LatLng end, diff --git a/test/services/line_of_sight_service_test.dart b/test/services/line_of_sight_service_test.dart index 987ee6c5..267a70ba 100644 --- a/test/services/line_of_sight_service_test.dart +++ b/test/services/line_of_sight_service_test.dart @@ -16,6 +16,7 @@ void main() { elevations: elevations, startAntennaHeightMeters: 2, endAntennaHeightMeters: 2, + kFactor: 4.0 / 3.0, ); expect(result.hasData, isTrue); @@ -36,6 +37,7 @@ void main() { elevations: elevations, startAntennaHeightMeters: 1.5, endAntennaHeightMeters: 1.5, + kFactor: 4.0 / 3.0, ); expect(result.hasData, isTrue); From 677b25908ade7edc8b1e84487b74393be7e3388c Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:42:58 -0500 Subject: [PATCH 69/99] Document LOS frequency and k-factor math Show the connector frequency right next to the Frequency label, display the derived k value, and keep the info dialog tied to the exact --- lib/l10n/app_bg.arb | 18 +++++++- lib/l10n/app_de.arb | 18 +++++++- lib/l10n/app_en.arb | 16 +++++++ lib/l10n/app_es.arb | 18 +++++++- lib/l10n/app_fr.arb | 18 +++++++- lib/l10n/app_it.arb | 18 +++++++- lib/l10n/app_localizations.dart | 47 ++++++++++++++++++++ lib/l10n/app_localizations_bg.dart | 29 +++++++++++++ lib/l10n/app_localizations_de.dart | 28 ++++++++++++ lib/l10n/app_localizations_en.dart | 28 ++++++++++++ lib/l10n/app_localizations_es.dart | 28 ++++++++++++ lib/l10n/app_localizations_fr.dart | 28 ++++++++++++ lib/l10n/app_localizations_it.dart | 28 ++++++++++++ lib/l10n/app_localizations_nl.dart | 28 ++++++++++++ lib/l10n/app_localizations_pl.dart | 28 ++++++++++++ lib/l10n/app_localizations_pt.dart | 28 ++++++++++++ lib/l10n/app_localizations_ru.dart | 28 ++++++++++++ lib/l10n/app_localizations_sk.dart | 28 ++++++++++++ lib/l10n/app_localizations_sl.dart | 28 ++++++++++++ lib/l10n/app_localizations_sv.dart | 28 ++++++++++++ lib/l10n/app_localizations_uk.dart | 28 ++++++++++++ lib/l10n/app_localizations_zh.dart | 28 ++++++++++++ lib/l10n/app_nl.arb | 18 +++++++- lib/l10n/app_pl.arb | 18 +++++++- lib/l10n/app_pt.arb | 18 +++++++- lib/l10n/app_ru.arb | 18 +++++++- lib/l10n/app_sk.arb | 18 +++++++- lib/l10n/app_sl.arb | 18 +++++++- lib/l10n/app_sv.arb | 18 +++++++- lib/l10n/app_uk.arb | 18 +++++++- lib/l10n/app_zh.arb | 18 +++++++- lib/screens/line_of_sight_map_screen.dart | 52 +++++++++++------------ 32 files changed, 747 insertions(+), 41 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 8609023e..dc0ca4e8 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1716,5 +1716,21 @@ "losPointName": "Име на точката", "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "LOS лъч", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на подробностите за изчислението", + "losFrequencyDialogTitle": "Изчисление на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението умножава 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, за да достигне k approx {kFactor} за текущата лента {frequencyMHz} MHz, която определя извитата граница на радиохоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5c82f79..25c899c9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1744,5 +1744,21 @@ "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Funkhorizont", + "losLegendLosBeam": "LOS-Strahl", + "losLegendTerrain": "Gelände", + "losFrequencyLabel": "Frequenz", + "losFrequencyInfoTooltip": "Berechnungsdetails anzeigen", + "losFrequencyDialogTitle": "Funkhorizont-Berechnung", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz multipliziert die Berechnung 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, um k approx {kFactor} für das aktuelle {frequencyMHz}-MHz-Band zu erreichen, das die gekrümmte Funkhorizont-Grenze definiert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 67ca72ee..3f89e48a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1665,6 +1665,22 @@ "losShowPanelTooltip": "Show LOS panel", "losHidePanelTooltip": "Hide LOS panel", "losElevationAttribution": "Elevation data: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radio horizon", + "losLegendLosBeam": "LOS beam", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Frequency", + "losFrequencyInfoTooltip": "View calculation details", + "losFrequencyDialogTitle": "Radio horizon calculation", + "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + }, "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 483b4d33..0616454c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1744,5 +1744,21 @@ "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Haz LOS", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frecuencia", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "Partiendo de k={baselineK} a {baselineFreq} MHz, el cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para llegar a k approx {kFactor} para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte radioeléctrico.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2d4846c8..cfdd3ba8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizon radio", + "losLegendLosBeam": "Faisceau LOS", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "En partant de k={baselineK} à {baselineFreq} MHz, le calcul multiplie 0.15 × (frequency − {baselineFreq}) / {baselineFreq} pour atteindre k approx {kFactor} pour la bande actuelle de {frequencyMHz} MHz, qui définit la limite courbe de l’horizon radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 2f8d186f..427eb0a5 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nome del punto", "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", - "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Orizzonte radio", + "losLegendLosBeam": "Raggio LOS", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequenza", + "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo moltiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} per raggiungere k approx {kFactor} per la banda corrente di {frequencyMHz} MHz, che definisce il limite curvo dell’orizzonte radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e9686cef..015dccaf 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4992,6 +4992,53 @@ abstract class AppLocalizations { /// **'Elevation data: Open-Meteo (CC BY 4.0)'** String get losElevationAttribution; + /// No description provided for @losLegendRadioHorizon. + /// + /// In en, this message translates to: + /// **'Radio horizon'** + String get losLegendRadioHorizon; + + /// No description provided for @losLegendLosBeam. + /// + /// In en, this message translates to: + /// **'LOS beam'** + String get losLegendLosBeam; + + /// No description provided for @losLegendTerrain. + /// + /// In en, this message translates to: + /// **'Terrain'** + String get losLegendTerrain; + + /// No description provided for @losFrequencyLabel. + /// + /// In en, this message translates to: + /// **'Frequency'** + String get losFrequencyLabel; + + /// No description provided for @losFrequencyInfoTooltip. + /// + /// In en, this message translates to: + /// **'View calculation details'** + String get losFrequencyInfoTooltip; + + /// No description provided for @losFrequencyDialogTitle. + /// + /// In en, this message translates to: + /// **'Radio horizon calculation'** + String get losFrequencyDialogTitle; + + /// Explain how the calculation uses the baseline frequency and derived k-factor. + /// + /// In en, this message translates to: + /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ); + /// No description provided for @contacts_pathTrace. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index cf4bf7b5..68ed8e54 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2859,6 +2859,35 @@ class AppLocalizationsBg extends AppLocalizations { String get losElevationAttribution => 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радиохоризонт'; + + @override + String get losLegendLosBeam => 'LOS лъч'; + + @override + String get losLegendTerrain => 'Терен'; + + @override + String get losFrequencyLabel => 'Честота'; + + @override + String get losFrequencyInfoTooltip => + 'Преглед на подробностите за изчислението'; + + @override + String get losFrequencyDialogTitle => 'Изчисление на радиохоризонта'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението умножава 0.15 × (frequency − $baselineFreq) / $baselineFreq, за да достигне k approx $kFactor за текущата лента $frequencyMHz MHz, която определя извитата граница на радиохоризонта.'; + } + @override String get contacts_pathTrace => 'Пътен проследяване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index c6a07a4b..1c97a4c6 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2865,6 +2865,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Funkhorizont'; + + @override + String get losLegendLosBeam => 'LOS-Strahl'; + + @override + String get losLegendTerrain => 'Gelände'; + + @override + String get losFrequencyLabel => 'Frequenz'; + + @override + String get losFrequencyInfoTooltip => 'Berechnungsdetails anzeigen'; + + @override + String get losFrequencyDialogTitle => 'Funkhorizont-Berechnung'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz multipliziert die Berechnung 0.15 × (frequency − $baselineFreq) / $baselineFreq, um k approx $kFactor für das aktuelle $frequencyMHz-MHz-Band zu erreichen, das die gekrümmte Funkhorizont-Grenze definiert.'; + } + @override String get contacts_pathTrace => 'Pfadverfolgung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 254b5f4a..c5ef344d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2817,6 +2817,34 @@ class AppLocalizationsEn extends AppLocalizations { String get losElevationAttribution => 'Elevation data: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radio horizon'; + + @override + String get losLegendLosBeam => 'LOS beam'; + + @override + String get losLegendTerrain => 'Terrain'; + + @override + String get losFrequencyLabel => 'Frequency'; + + @override + String get losFrequencyInfoTooltip => 'View calculation details'; + + @override + String get losFrequencyDialogTitle => 'Radio horizon calculation'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + } + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index dcde3653..33db8722 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2859,6 +2859,34 @@ class AppLocalizationsEs extends AppLocalizations { String get losElevationAttribution => 'Datos de elevación: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; + + @override + String get losLegendLosBeam => 'Haz LOS'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frecuencia'; + + @override + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; + + @override + String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partiendo de k=$baselineK a $baselineFreq MHz, el cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para llegar a k approx $kFactor para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte radioeléctrico.'; + } + @override String get contacts_pathTrace => 'Rastreo de caminos'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c4e1e273..fc059a9c 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2874,6 +2874,34 @@ class AppLocalizationsFr extends AppLocalizations { String get losElevationAttribution => 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizon radio'; + + @override + String get losLegendLosBeam => 'Faisceau LOS'; + + @override + String get losLegendTerrain => 'Terrain'; + + @override + String get losFrequencyLabel => 'Fréquence'; + + @override + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; + + @override + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'En partant de k=$baselineK à $baselineFreq MHz, le calcul multiplie 0.15 × (frequency − $baselineFreq) / $baselineFreq pour atteindre k approx $kFactor pour la bande actuelle de $frequencyMHz MHz, qui définit la limite courbe de l’horizon radio.'; + } + @override String get contacts_pathTrace => 'Traçage de chemin'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index d8e27f87..123d194c 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2859,6 +2859,34 @@ class AppLocalizationsIt extends AppLocalizations { String get losElevationAttribution => 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Orizzonte radio'; + + @override + String get losLegendLosBeam => 'Raggio LOS'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frequenza'; + + @override + String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; + + @override + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo moltiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq per raggiungere k approx $kFactor per la banda corrente di $frequencyMHz MHz, che definisce il limite curvo dell’orizzonte radio.'; + } + @override String get contacts_pathTrace => 'Traccia Percorso'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 0a50e8b0..3c6d5c45 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2850,6 +2850,34 @@ class AppLocalizationsNl extends AppLocalizations { String get losElevationAttribution => 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radiohorizon'; + + @override + String get losLegendLosBeam => 'LOS-straal'; + + @override + String get losLegendTerrain => 'Terrein'; + + @override + String get losFrequencyLabel => 'Frequentie'; + + @override + String get losFrequencyInfoTooltip => 'Berekeningsdetails bekijken'; + + @override + String get losFrequencyDialogTitle => 'Radiohorizon-berekening'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Uitgaande van k=$baselineK bij $baselineFreq MHz vermenigvuldigt de berekening 0.15 × (frequency − $baselineFreq) / $baselineFreq om k approx $kFactor te bereiken voor de huidige $frequencyMHz-MHz-band, die de gebogen radiohorizon-limiet definieert.'; + } + @override String get contacts_pathTrace => 'Pad Traceren'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 31dd8b56..02a6a111 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2856,6 +2856,34 @@ class AppLocalizationsPl extends AppLocalizations { String get losElevationAttribution => 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horyzont radiowy'; + + @override + String get losLegendLosBeam => 'Wiązka LOS'; + + @override + String get losLegendTerrain => 'Teren'; + + @override + String get losFrequencyLabel => 'Częstotliwość'; + + @override + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczeń'; + + @override + String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Wychodząc od k=$baselineK przy $baselineFreq MHz, obliczenie mnoży 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby osiągnąć k approx $kFactor dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywioną granicę horyzontu radiowego.'; + } + @override String get contacts_pathTrace => 'Śledzenie Ścieżek'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5092826f..7cb09865 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2858,6 +2858,34 @@ class AppLocalizationsPt extends AppLocalizations { String get losElevationAttribution => 'Dados de elevação: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Horizonte de rádio'; + + @override + String get losLegendLosBeam => 'Feixe LOS'; + + @override + String get losLegendTerrain => 'Terreno'; + + @override + String get losFrequencyLabel => 'Frequência'; + + @override + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; + + @override + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Partindo de k=$baselineK a $baselineFreq MHz, o cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para chegar a k approx $kFactor para a banda atual de $frequencyMHz MHz, que define o limite curvo do horizonte de rádio.'; + } + @override String get contacts_pathTrace => 'Traçado de Caminho'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 570b7c84..6e35b434 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2861,6 +2861,34 @@ class AppLocalizationsRu extends AppLocalizations { String get losElevationAttribution => 'Данные о высоте: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радиогоризонт'; + + @override + String get losLegendLosBeam => 'Луч LOS'; + + @override + String get losLegendTerrain => 'Рельеф'; + + @override + String get losFrequencyLabel => 'Частота'; + + @override + String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; + + @override + String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Исходя из k=$baselineK при $baselineFreq MHz, расчёт умножает 0.15 × (frequency − $baselineFreq) / $baselineFreq, чтобы получить k approx $kFactor для текущего диапазона $frequencyMHz MHz, который определяет изогнутую границу радиогоризонта.'; + } + @override String get contacts_pathTrace => 'Трассировка пути'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 8bbb6dea..041e7fda 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2844,6 +2844,34 @@ class AppLocalizationsSk extends AppLocalizations { String get losElevationAttribution => 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Rádiový horizont'; + + @override + String get losLegendLosBeam => 'LOS lúč'; + + @override + String get losLegendTerrain => 'Terén'; + + @override + String get losFrequencyLabel => 'Frekvencia'; + + @override + String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; + + @override + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Vychádzajúc z k=$baselineK pri $baselineFreq MHz výpočet násobí 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby dosiahol k approx $kFactor pre aktuálne pásmo $frequencyMHz MHz, ktoré definuje zakrivenú hranicu rádiového horizontu.'; + } + @override String get contacts_pathTrace => 'Sledovanie lúčov'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 61e30584..0a465335 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2847,6 +2847,34 @@ class AppLocalizationsSl extends AppLocalizations { String get losElevationAttribution => 'Podatki o višini: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radijski horizont'; + + @override + String get losLegendLosBeam => 'LOS žarek'; + + @override + String get losLegendTerrain => 'Teren'; + + @override + String get losFrequencyLabel => 'Frekvenca'; + + @override + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; + + @override + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Izhajajoč iz k=$baselineK pri $baselineFreq MHz izračun množi 0.15 × (frequency − $baselineFreq) / $baselineFreq, da doseže k approx $kFactor za trenutno $frequencyMHz-MHz območje, ki določa ukrivljeno mejo radijskega horizonta.'; + } + @override String get contacts_pathTrace => 'Sledenje poti'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 79b30b88..0fb8e701 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2830,6 +2830,34 @@ class AppLocalizationsSv extends AppLocalizations { @override String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Radiohorisont'; + + @override + String get losLegendLosBeam => 'LOS-stråle'; + + @override + String get losLegendTerrain => 'Terräng'; + + @override + String get losFrequencyLabel => 'Frekvens'; + + @override + String get losFrequencyInfoTooltip => 'Visa beräkningsdetaljer'; + + @override + String get losFrequencyDialogTitle => 'Beräkning av radiohorisont'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Med start från k=$baselineK vid $baselineFreq MHz multiplicerar beräkningen 0.15 × (frequency − $baselineFreq) / $baselineFreq för att nå k approx $kFactor för det aktuella $frequencyMHz-MHz-bandet, vilket definierar den krökta radiohorisontgränsen.'; + } + @override String get contacts_pathTrace => 'Path Trace'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index f3670021..e81f8e11 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2869,6 +2869,34 @@ class AppLocalizationsUk extends AppLocalizations { String get losElevationAttribution => 'Дані про висоту: Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => 'Радіогоризонт'; + + @override + String get losLegendLosBeam => 'Промінь LOS'; + + @override + String get losLegendTerrain => 'Рельєф'; + + @override + String get losFrequencyLabel => 'Частота'; + + @override + String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; + + @override + String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return 'Виходячи з k=$baselineK при $baselineFreq MHz, розрахунок множить 0.15 × (frequency − $baselineFreq) / $baselineFreq, щоб отримати k approx $kFactor для поточного діапазону $frequencyMHz MHz, який визначає вигнуту межу радіогоризонту.'; + } + @override String get contacts_pathTrace => 'Трасування шляхів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7641800d..a7f4a8ae 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2709,6 +2709,34 @@ class AppLocalizationsZh extends AppLocalizations { @override String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; + @override + String get losLegendRadioHorizon => '无线电地平线'; + + @override + String get losLegendLosBeam => 'LOS 波束'; + + @override + String get losLegendTerrain => '地形'; + + @override + String get losFrequencyLabel => '频率'; + + @override + String get losFrequencyInfoTooltip => '查看计算详情'; + + @override + String get losFrequencyDialogTitle => '无线电地平线计算'; + + @override + String losFrequencyDialogDescription( + double baselineK, + double baselineFreq, + double frequencyMHz, + double kFactor, + ) { + return '从 k=$baselineK($baselineFreq MHz)开始,计算将 0.15 × (frequency − $baselineFreq) / $baselineFreq,以得到当前 $frequencyMHz MHz 频段的 k approx $kFactor,从而定义弯曲的无线电地平线边界。'; + } + @override String get contacts_pathTrace => '路径追踪'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 57b2fdda..3855e149 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1716,5 +1716,21 @@ "losPointName": "Puntnaam", "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", - "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorizon", + "losLegendLosBeam": "LOS-straal", + "losLegendTerrain": "Terrein", + "losFrequencyLabel": "Frequentie", + "losFrequencyInfoTooltip": "Berekeningsdetails bekijken", + "losFrequencyDialogTitle": "Radiohorizon-berekening", + "losFrequencyDialogDescription": "Uitgaande van k={baselineK} bij {baselineFreq} MHz vermenigvuldigt de berekening 0.15 × (frequency − {baselineFreq}) / {baselineFreq} om k approx {kFactor} te bereiken voor de huidige {frequencyMHz}-MHz-band, die de gebogen radiohorizon-limiet definieert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3787fa71..dde149ce 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nazwa punktu", "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horyzont radiowy", + "losLegendLosBeam": "Wiązka LOS", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Częstotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczeń", + "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", + "losFrequencyDialogDescription": "Wychodząc od k={baselineK} przy {baselineFreq} MHz, obliczenie mnoży 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby osiągnąć k approx {kFactor} dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywioną granicę horyzontu radiowego.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7be66945..7edff74b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1716,5 +1716,21 @@ "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", + "losLegendLosBeam": "Feixe LOS", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Partindo de k={baselineK} a {baselineFreq} MHz, o cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para chegar a k approx {kFactor} para a banda atual de {frequencyMHz} MHz, que define o limite curvo do horizonte de rádio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 26cfce3b..139074b1 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -956,5 +956,21 @@ "losPointName": "Имя точки", "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Луч LOS", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Исходя из k={baselineK} при {baselineFreq} MHz, расчёт умножает 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, чтобы получить k approx {kFactor} для текущего диапазона {frequencyMHz} MHz, который определяет изогнутую границу радиогоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 8b2cb0ae..89a1b0df 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1716,5 +1716,21 @@ "losPointName": "Názov bodu", "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "LOS lúč", + "losLegendTerrain": "Terén", + "losFrequencyLabel": "Frekvencia", + "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Vychádzajúc z k={baselineK} pri {baselineFreq} MHz výpočet násobí 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby dosiahol k approx {kFactor} pre aktuálne pásmo {frequencyMHz} MHz, ktoré definuje zakrivenú hranicu rádiového horizontu.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 4d3415d3..2fe86e09 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1716,5 +1716,21 @@ "losPointName": "Ime točke", "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radijski horizont", + "losLegendLosBeam": "LOS žarek", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Frekvenca", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "Izhajajoč iz k={baselineK} pri {baselineFreq} MHz izračun množi 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, da doseže k approx {kFactor} za trenutno {frequencyMHz}-MHz območje, ki določa ukrivljeno mejo radijskega horizonta.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 8c5e399c..2625fb2a 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1716,5 +1716,21 @@ "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorisont", + "losLegendLosBeam": "LOS-stråle", + "losLegendTerrain": "Terräng", + "losFrequencyLabel": "Frekvens", + "losFrequencyInfoTooltip": "Visa beräkningsdetaljer", + "losFrequencyDialogTitle": "Beräkning av radiohorisont", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz multiplicerar beräkningen 0.15 × (frequency − {baselineFreq}) / {baselineFreq} för att nå k approx {kFactor} för det aktuella {frequencyMHz}-MHz-bandet, vilket definierar den krökta radiohorisontgränsen.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 910f8b0e..8c28f192 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1716,5 +1716,21 @@ "losPointName": "Назва точки", "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Промінь LOS", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Виходячи з k={baselineK} при {baselineFreq} MHz, розрахунок множить 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, щоб отримати k approx {kFactor} для поточного діапазону {frequencyMHz} MHz, який визначає вигнуту межу радіогоризонту.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index d9efce7f..bd8067ff 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1716,5 +1716,21 @@ "losPointName": "点名称", "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "LOS 波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 k={baselineK}({baselineFreq} MHz)开始,计算将 0.15 × (frequency − {baselineFreq}) / {baselineFreq},以得到当前 {frequencyMHz} MHz 频段的 k approx {kFactor},从而定义弯曲的无线电地平线边界。", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } } diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 5eb532b8..72c22324 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -471,6 +471,9 @@ class _LineOfSightMapScreenState extends State { fontSize: 10, fontWeight: FontWeight.w600, ), + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, ), ), ) @@ -504,7 +507,7 @@ class _LineOfSightMapScreenState extends State { child: Row( children: [ Text( - 'Frequency', + context.l10n.losFrequencyLabel, style: TextStyle( fontSize: 11, color: Colors.grey[700], @@ -531,7 +534,7 @@ class _LineOfSightMapScreenState extends State { constraints: const BoxConstraints(), icon: const Icon(Icons.info_outline, size: 16), color: Colors.grey[600], - tooltip: 'View calculation details', + tooltip: context.l10n.losFrequencyInfoTooltip, onPressed: () { _showFrequencyInfoDialog( context, @@ -963,27 +966,13 @@ class _LineOfSightMapScreenState extends State { showDialog( context: context, builder: (dialogContext) => AlertDialog( - title: const Text('Radio horizon calculation'), - content: Text.rich( - TextSpan( - children: [ - TextSpan( - text: - 'Starting from k=$baselineK at ${baselineFreq.toStringAsFixed(3)} MHz, ', - ), - const TextSpan(text: 'the calculation multiplies the offset by '), - TextSpan( - text: - '0.15 × (frequency − ${baselineFreq.toStringAsFixed(3)}) / ${baselineFreq.toStringAsFixed(3)} ', - ), - TextSpan( - text: - 'to get k ≈ ${kFactor.toStringAsFixed(3)} for the current ${frequencyMHz.toStringAsFixed(3)} MHz band, ', - ), - const TextSpan( - text: 'which defines the curved radio horizon cap.', - ), - ], + title: Text(context.l10n.losFrequencyDialogTitle), + content: Text( + context.l10n.losFrequencyDialogDescription( + baselineK, + baselineFreq, + frequencyMHz, + kFactor, ), ), actions: [ @@ -1009,12 +998,18 @@ class _LosProfilePainter extends CustomPainter { final String distanceUnit; final String heightUnit; final TextStyle badgeTextStyle; + final String terrainLabel; + final String losBeamLabel; + final String radioHorizonLabel; const _LosProfilePainter({ required this.samples, required this.distanceUnit, required this.heightUnit, required this.badgeTextStyle, + required this.terrainLabel, + required this.losBeamLabel, + required this.radioHorizonLabel, }); @override @@ -1148,7 +1143,10 @@ class _LosProfilePainter extends CustomPainter { return oldDelegate.samples != samples || oldDelegate.distanceUnit != distanceUnit || oldDelegate.heightUnit != heightUnit || - oldDelegate.badgeTextStyle != badgeTextStyle; + oldDelegate.badgeTextStyle != badgeTextStyle || + oldDelegate.terrainLabel != terrainLabel || + oldDelegate.losBeamLabel != losBeamLabel || + oldDelegate.radioHorizonLabel != radioHorizonLabel; } void _drawUnitBadge(Canvas canvas, Size size) { @@ -1175,9 +1173,9 @@ class _LosProfilePainter extends CustomPainter { const legendPadding = 6.0; final entries = [ - _LegendEntry('Terrain', terrainColor), - _LegendEntry('LOS beam', losColor), - _LegendEntry('Radio horizon', horizonColor), + _LegendEntry(terrainLabel, terrainColor), + _LegendEntry(losBeamLabel, losColor), + _LegendEntry(radioHorizonLabel, horizonColor), ]; final textStyle = badgeTextStyle.copyWith( From 7465e81996271830ba460b77cd4cfdf4ce6b47bb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 03:31:01 -0800 Subject: [PATCH 70/99] add done_all icon --- assets/icons/done_all.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 assets/icons/done_all.svg diff --git a/assets/icons/done_all.svg b/assets/icons/done_all.svg new file mode 100644 index 00000000..bfeeec0d --- /dev/null +++ b/assets/icons/done_all.svg @@ -0,0 +1 @@ + \ No newline at end of file From 173fdf7168e8cdd1ccf06cfe08bb4d8abbb71bd0 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:09:27 -0800 Subject: [PATCH 71/99] chat fixes --- lib/screens/channel_chat_screen.dart | 104 +++++++++++++++------------ lib/screens/chat_screen.dart | 94 ++++++++++++------------ lib/widgets/message_status_icon.dart | 36 ++++++++++ pubspec.lock | 40 +++++++++++ pubspec.yaml | 3 + 5 files changed, 187 insertions(+), 90 deletions(-) create mode 100644 lib/widgets/message_status_icon.dart diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9b9de350..a6354c3a 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -23,6 +24,7 @@ import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; import '../widgets/gif_picker.dart'; +import '../widgets/message_status_icon.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -337,7 +339,23 @@ class _ChannelChatScreenState extends State { const SizedBox(height: 8), ], if (poi != null) - _buildPoiMessage(context, poi, isOutgoing) + _buildPoiMessage( + context, + poi, + isOutgoing, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: message.status == + ChannelMessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -358,33 +376,31 @@ class _ChannelChatScreenState extends State { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.3), - shape: BoxShape.circle, + color: isOutgoing + ? Theme.of( + context, + ).colorScheme.primaryContainer + : Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(8), + ), ), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ), @@ -419,25 +435,14 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Icons.check_circle - : message.status == - ChannelMessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - ChannelMessageStatus.sent && - displayPath.isNotEmpty) - ? Colors.green - : message.status == - ChannelMessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + ChannelMessageStatus.sent && + displayPath.isNotEmpty, + isFailed: + message.status == + ChannelMessageStatus.failed, ), ), ], @@ -727,7 +732,12 @@ class _ChannelChatScreenState extends State { return _PoiInfo(lat: lat, lon: lon, label: label); } - Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) { + Widget _buildPoiMessage( + BuildContext context, + _PoiInfo poi, + bool isOutgoing, { + Widget? trailing, + }) { final colorScheme = Theme.of(context).colorScheme; final textColor = isOutgoing ? colorScheme.onPrimaryContainer @@ -773,6 +783,10 @@ class _ChannelChatScreenState extends State { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 32a7882b..bfbd88f7 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; @@ -13,6 +14,7 @@ import 'package:latlong2/latlong.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../helpers/reaction_helper.dart'; +import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; import '../helpers/link_handler.dart'; import '../helpers/utf8_length_limiter.dart'; @@ -1252,7 +1254,24 @@ class _MessageBubble extends StatelessWidget { if (gifId == null) const SizedBox(height: 4), ], if (poi != null) - _buildPoiMessage(context, poi, textColor, metaColor) + _buildPoiMessage( + context, + poi, + textColor, + metaColor, + trailing: (!enableTracing && isOutgoing) + ? Padding( + padding: const EdgeInsets.only(bottom: 2), + child: MessageStatusIcon( + isAcked: message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: message.status == + MessageStatus.failed, + ), + ) + : null, + ) else if (gifId != null) Stack( children: [ @@ -1269,35 +1288,25 @@ class _MessageBubble extends StatelessWidget { ), if (!enableTracing && isOutgoing) Positioned( - top: 4, - right: 4, + top: 0, + right: 0, child: Container( - padding: const EdgeInsets.all(2), + padding: const EdgeInsets.all(3), decoration: BoxDecoration( - color: Colors.black.withValues( - alpha: 0.3, + color: bubbleColor, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(10), + topRight: Radius.circular(12), ), - shape: BoxShape.circle, ), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == - MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == - MessageStatus.failed - ? Colors.red - : Colors.white70, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == + MessageStatus.failed, ), ), ), @@ -1331,23 +1340,13 @@ class _MessageBubble extends StatelessWidget { const SizedBox(width: 4), Padding( padding: const EdgeInsets.only(bottom: 2), - child: Icon( - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Icons.check_circle - : message.status == MessageStatus.failed - ? Icons.cancel - : Icons.cloud, - size: 14, - color: - (message.status == - MessageStatus.delivered && - message.pathBytes.isNotEmpty) - ? Colors.green - : message.status == MessageStatus.failed - ? Colors.red - : Colors.grey, + child: MessageStatusIcon( + isAcked: + message.status == + MessageStatus.delivered && + message.pathBytes.isNotEmpty, + isFailed: + message.status == MessageStatus.failed, ), ), ], @@ -1464,8 +1463,9 @@ class _MessageBubble extends StatelessWidget { BuildContext context, _PoiInfo poi, Color textColor, - Color metaColor, - ) { + Color metaColor, { + Widget? trailing, + }) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -1502,6 +1502,10 @@ class _MessageBubble extends StatelessWidget { ], ), ), + if (trailing != null) ...[ + const SizedBox(width: 4), + trailing, + ], ], ); } diff --git a/lib/widgets/message_status_icon.dart b/lib/widgets/message_status_icon.dart new file mode 100644 index 00000000..0689f0b5 --- /dev/null +++ b/lib/widgets/message_status_icon.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class MessageStatusIcon extends StatelessWidget { + final bool isAcked; + final bool isFailed; + final double size; + + const MessageStatusIcon({ + super.key, + required this.isAcked, + this.isFailed = false, + this.size = 14, + }); + + @override + Widget build(BuildContext context) { + if (isFailed) { + return Icon(Icons.cancel, size: size, color: Colors.red); + } + + final Color color; + if (isAcked) { + color = Colors.green; + } else { + color = Colors.grey; + } + + return SvgPicture.asset( + 'assets/icons/done_all.svg', + width: size, + height: size, + colorFilter: ColorFilter.mode(color, BlendMode.srcIn), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index f6958384..98304337 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f5ceaafc..dcca7f88 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,8 @@ dependencies: gpx: ^2.3.0 path_provider: ^2.1.5 share_plus: ^12.0.1 + web: ^1.1.1 + flutter_svg: ^2.0.10+1 dev_dependencies: flutter_test: @@ -87,6 +89,7 @@ flutter: assets: - assets/images/ + - assets/icons/ flutter_launcher_icons: android: true From 3730b2a6c2e0f8890ca58c13c6cbbe3988fc316d Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:13:38 -0800 Subject: [PATCH 72/99] formatting --- lib/screens/channel_chat_screen.dart | 11 +++++------ lib/screens/chat_screen.dart | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index a6354c3a..937fa875 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -347,10 +347,12 @@ class _ChannelChatScreenState extends State { ? Padding( padding: const EdgeInsets.only(bottom: 2), child: MessageStatusIcon( - isAcked: message.status == + isAcked: + message.status == ChannelMessageStatus.sent && displayPath.isNotEmpty, - isFailed: message.status == + isFailed: + message.status == ChannelMessageStatus.failed, ), ) @@ -783,10 +785,7 @@ class _ChannelChatScreenState extends State { ], ), ), - if (trailing != null) ...[ - const SizedBox(width: 4), - trailing, - ], + if (trailing != null) ...[const SizedBox(width: 4), trailing], ], ); } diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index bfbd88f7..180f8135 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -1263,10 +1263,12 @@ class _MessageBubble extends StatelessWidget { ? Padding( padding: const EdgeInsets.only(bottom: 2), child: MessageStatusIcon( - isAcked: message.status == + isAcked: + message.status == MessageStatus.delivered && message.pathBytes.isNotEmpty, - isFailed: message.status == + isFailed: + message.status == MessageStatus.failed, ), ) @@ -1502,10 +1504,7 @@ class _MessageBubble extends StatelessWidget { ], ), ), - if (trailing != null) ...[ - const SizedBox(width: 4), - trailing, - ], + if (trailing != null) ...[const SizedBox(width: 4), trailing], ], ); } From bf5fadd15eea2f101c52fe4a55946dca6748a6a4 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:13:52 -0800 Subject: [PATCH 73/99] revert lockfile --- pubspec.lock | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 98304337..f6958384 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,14 +347,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -605,14 +597,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1026,30 +1010,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" vector_math: dependency: transitive description: From 88f8066ed3fe4e904f4fefd2ad5f529e8673dae3 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Mon, 23 Feb 2026 04:53:01 -0800 Subject: [PATCH 74/99] code formatting --- lib/screens/channel_chat_screen.dart | 1 - lib/screens/chat_screen.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 937fa875..9df91c3b 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 180f8135..3556d6de 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:provider/provider.dart'; From 16b2c249830aa5692b7decc82980a4990cb3dcd4 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:18:42 -0500 Subject: [PATCH 75/99] Propagate LOS frequency data and clamp bounds --- lib/screens/line_of_sight_map_screen.dart | 14 ++++++-- lib/services/line_of_sight_service.dart | 1 + pubspec.lock | 42 ++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 72c22324..c3fe4763 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -1021,10 +1021,20 @@ class _LosProfilePainter extends CustomPainter { if (samples.length < 2) return; final minY = samples - .map((s) => math.min(s.terrainMeters, s.lineHeightMeters)) + .map( + (s) => math.min( + math.min(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) .reduce(math.min); final maxY = samples - .map((s) => math.max(s.terrainMeters, s.lineHeightMeters)) + .map( + (s) => math.max( + math.max(s.terrainMeters, s.lineHeightMeters), + s.refractedHeightMeters, + ), + ) .reduce(math.max); final ySpan = math.max(1.0, maxY - minY); final maxDist = math.max(1.0, samples.last.distanceMeters); diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index 14d8fc6f..61e9e27f 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -212,6 +212,7 @@ class LineOfSightService { startAntennaHeightMeters: startAntennaHeightMeters, endAntennaHeightMeters: endAntennaHeightMeters, kFactor: kFactor, + frequencyMHz: frequencyMHz, obstructionToleranceMeters: obstructionToleranceMeters, ); } diff --git a/pubspec.lock b/pubspec.lock index ed84c404..22cad80e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -347,6 +347,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + url: "https://pub.dev" + source: hosted + version: "2.2.3" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -1010,6 +1026,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.2" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + url: "https://pub.dev" + source: hosted + version: "1.1.19" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + url: "https://pub.dev" + source: hosted + version: "1.2.0" vector_math: dependency: transitive description: @@ -1043,7 +1083,7 @@ packages: source: hosted version: "1.3.0" web: - dependency: transitive + dependency: "direct main" description: name: web sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" From c0516a475d24bbc6391470b74b3b0994713023b9 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:36:10 -0500 Subject: [PATCH 76/99] fix: extend los profile edges --- lib/screens/line_of_sight_map_screen.dart | 219 +++++++++++++--------- 1 file changed, 129 insertions(+), 90 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index c3fe4763..785dfe5f 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -487,6 +487,14 @@ class _LineOfSightMapScreenState extends State { ), ), ), + if (segment != null) ...[ + const SizedBox(height: 8), + _LosLegend( + terrainLabel: context.l10n.losLegendTerrain, + losBeamLabel: context.l10n.losLegendLosBeam, + radioHorizonLabel: context.l10n.losLegendRadioHorizon, + ), + ], const SizedBox(height: 8), Text( segment != null @@ -1038,36 +1046,85 @@ class _LosProfilePainter extends CustomPainter { .reduce(math.max); final ySpan = math.max(1.0, maxY - minY); final maxDist = math.max(1.0, samples.last.distanceMeters); + const horizontalPadding = 12.0; + const verticalPadding = 12.0; + final chartWidth = math.max(1.0, size.width - horizontalPadding * 2); + final chartHeight = math.max(1.0, size.height - verticalPadding * 2); Offset mapPoint(double x, double y) { - final px = (x / maxDist) * size.width; - final py = size.height - ((y - minY) / ySpan) * size.height; + final px = horizontalPadding + (x / maxDist) * chartWidth; + final py = + size.height - verticalPadding - ((y - minY) / ySpan) * chartHeight; return Offset(px, py); } - final terrainPath = ui.Path(); - terrainPath.moveTo(0, size.height); - for (final s in samples) { - final p = mapPoint(s.distanceMeters, s.terrainMeters); + final firstTerrainPoint = mapPoint( + samples.first.distanceMeters, + samples.first.terrainMeters, + ); + final lastTerrainPoint = mapPoint( + samples.last.distanceMeters, + samples.last.terrainMeters, + ); + + double distanceForCanvasX(double x) => + ((x - horizontalPadding) / chartWidth) * maxDist; + + double elevationToPixel(double elevation) => + size.height - + verticalPadding - + ((elevation - minY) / ySpan) * chartHeight; + + double extrapolateTerrain(double distance, bool isLeft) { + final samplesForSlope = isLeft + ? samples.sublist(0, math.min(2, samples.length)) + : samples.sublist(samples.length - math.min(2, samples.length)); + if (samplesForSlope.length < 2) { + return samplesForSlope.first.terrainMeters; + } + final a = samplesForSlope.first; + final b = samplesForSlope.last; + final dx = b.distanceMeters - a.distanceMeters; + if (dx.abs() < 1e-6) return a.terrainMeters; + final slope = (b.terrainMeters - a.terrainMeters) / dx; + return a.terrainMeters + slope * (distance - a.distanceMeters); + } + + final leftDistance = distanceForCanvasX(0.0); + final rightDistance = distanceForCanvasX(size.width); + final leftEdgeTerrain = extrapolateTerrain(leftDistance, true); + final rightEdgeTerrain = extrapolateTerrain(rightDistance, false); + final leftEdgePoint = Offset(0.0, elevationToPixel(leftEdgeTerrain)); + final rightEdgePoint = Offset( + size.width, + elevationToPixel(rightEdgeTerrain), + ); + + final terrainPath = ui.Path() + ..moveTo(0, size.height) + ..lineTo(leftEdgePoint.dx, leftEdgePoint.dy) + ..lineTo(firstTerrainPoint.dx, firstTerrainPoint.dy); + for (final sample in samples) { + final p = mapPoint(sample.distanceMeters, sample.terrainMeters); terrainPath.lineTo(p.dx, p.dy); } - terrainPath.lineTo(size.width, size.height); - terrainPath.close(); + terrainPath + ..lineTo(lastTerrainPoint.dx, lastTerrainPoint.dy) + ..lineTo(rightEdgePoint.dx, rightEdgePoint.dy) + ..lineTo(size.width, size.height) + ..close(); const terrainFillColor = Color(0xCC7C6F5D); const terrainLineColor = Color(0xFF9FE870); const losLineColor = Color(0xFFE0E7FF); canvas.drawPath(terrainPath, Paint()..color = terrainFillColor); - final terrainLine = ui.Path(); - for (int i = 0; i < samples.length; i++) { - final p = mapPoint(samples[i].distanceMeters, samples[i].terrainMeters); - if (i == 0) { - terrainLine.moveTo(p.dx, p.dy); - } else { - terrainLine.lineTo(p.dx, p.dy); - } + final terrainLine = ui.Path()..moveTo(leftEdgePoint.dx, leftEdgePoint.dy); + for (final sample in samples) { + final p = mapPoint(sample.distanceMeters, sample.terrainMeters); + terrainLine.lineTo(p.dx, p.dy); } + terrainLine.lineTo(rightEdgePoint.dx, rightEdgePoint.dy); canvas.drawPath( terrainLine, Paint() @@ -1144,8 +1201,6 @@ class _LosProfilePainter extends CustomPainter { ..color = horizonFillColor ..style = PaintingStyle.fill, ); - - _drawLegend(canvas, refractedLineColor, losLineColor, terrainLineColor); } @override @@ -1168,84 +1223,68 @@ class _LosProfilePainter extends CustomPainter { ..layout(); painter.paint(canvas, Offset(size.width - painter.width - 8, 8)); } +} - void _drawLegend( - Canvas canvas, - Color horizonColor, - Color losColor, - Color terrainColor, - ) { - const legendX = 8.0; - const legendY = 8.0; - const swatchSize = 10.0; - const swatchTextGap = 6.0; - const entrySpacing = 4.0; - const legendPadding = 6.0; +class _LosLegend extends StatelessWidget { + static const _terrainColor = Color(0xFF9FE870); + static const _losColor = Color(0xFFE0E7FF); + static const _radioColor = Color(0xFFFFD57F); + + final String terrainLabel; + final String losBeamLabel; + final String radioHorizonLabel; + + const _LosLegend({ + required this.terrainLabel, + required this.losBeamLabel, + required this.radioHorizonLabel, + }); + + @override + Widget build(BuildContext context) { + final textStyle = + Theme.of(context).textTheme.labelSmall?.copyWith( + color: Colors.white70, + fontSize: 11, + fontWeight: FontWeight.w500, + ) ?? + const TextStyle( + color: Colors.white70, + fontSize: 11, + fontWeight: FontWeight.w500, + ); final entries = [ - _LegendEntry(terrainLabel, terrainColor), - _LegendEntry(losBeamLabel, losColor), - _LegendEntry(radioHorizonLabel, horizonColor), + _LegendEntry(terrainLabel, _terrainColor), + _LegendEntry(losBeamLabel, _losColor), + _LegendEntry(radioHorizonLabel, _radioColor), ]; - final textStyle = badgeTextStyle.copyWith( - fontSize: 10, - fontWeight: FontWeight.w500, + const swatchSize = 10.0; + + return Wrap( + spacing: 16, + runSpacing: 6, + children: entries + .map( + (entry) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: swatchSize, + height: swatchSize, + decoration: BoxDecoration( + color: entry.color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 6), + Text(entry.label, style: textStyle), + ], + ), + ) + .toList(), ); - - final painters = entries.map((entry) { - final painter = TextPainter( - text: TextSpan(text: entry.label, style: textStyle), - textDirection: TextDirection.ltr, - )..layout(); - return painter; - }).toList(); - - final maxTextWidth = painters.map((p) => p.width).fold(0, math.max); - - final legendWidth = - legendPadding * 2 + swatchSize + swatchTextGap + maxTextWidth; - - final legendHeight = - legendPadding * 2 + - entries.length * swatchSize + - (entries.length - 1) * entrySpacing; - - final legendRect = RRect.fromLTRBR( - legendX, - legendY, - legendX + legendWidth, - legendY + legendHeight, - const Radius.circular(10), - ); - - canvas.drawRRect( - legendRect, - Paint()..color = const Color.fromARGB(90, 0, 0, 0), - ); - - var yOffset = legendY + legendPadding; - for (int i = 0; i < entries.length; i++) { - final entry = entries[i]; - final painter = painters[i]; - final swatchRect = Rect.fromLTWH( - legendX + legendPadding, - yOffset, - swatchSize, - swatchSize, - ); - canvas.drawRect(swatchRect, Paint()..color = entry.color); - - painter.paint( - canvas, - Offset( - swatchRect.right + swatchTextGap, - yOffset + (swatchSize - painter.height) / 2, - ), - ); - - yOffset += swatchSize + entrySpacing; - } } } From ec14870aeda7607b2d097224922c23b85e195474 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:42:30 -0500 Subject: [PATCH 77/99] Update after upstream merged other commits --- lib/l10n/app_bg.arb | 20 +++- lib/l10n/app_de.arb | 20 +++- lib/l10n/app_es.arb | 20 +++- lib/l10n/app_fr.arb | 20 +++- lib/l10n/app_it.arb | 20 +++- lib/l10n/app_localizations_bg.dart | 14 +-- lib/l10n/app_localizations_de.dart | 14 +-- lib/l10n/app_localizations_es.dart | 14 +-- lib/l10n/app_localizations_fr.dart | 14 +-- lib/l10n/app_localizations_it.dart | 14 +-- lib/l10n/app_localizations_nl.dart | 14 +-- lib/l10n/app_localizations_pl.dart | 14 +-- lib/l10n/app_localizations_pt.dart | 14 +-- lib/l10n/app_localizations_ru.dart | 14 +-- lib/l10n/app_localizations_sk.dart | 14 +-- lib/l10n/app_localizations_sl.dart | 14 +-- lib/l10n/app_localizations_sv.dart | 14 +-- lib/l10n/app_localizations_uk.dart | 14 +-- lib/l10n/app_localizations_zh.dart | 14 +-- lib/l10n/app_nl.arb | 20 +++- lib/l10n/app_pl.arb | 20 +++- lib/l10n/app_pt.arb | 20 +++- lib/l10n/app_ru.arb | 20 +++- lib/l10n/app_sk.arb | 20 +++- lib/l10n/app_sl.arb | 20 +++- lib/l10n/app_sv.arb | 20 +++- lib/l10n/app_uk.arb | 20 +++- lib/l10n/app_zh.arb | 20 +++- untranslated.json | 142 +---------------------------- 29 files changed, 351 insertions(+), 267 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index e9f46c61..c2451019 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1718,5 +1718,21 @@ "losPointName": "Име на точката", "losShowPanelTooltip": "Показване на LOS панел", "losHidePanelTooltip": "Скриване на LOS панела", - "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Данни за надморска височина: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиохоризонт", + "losLegendLosBeam": "Линия на видимост", + "losLegendTerrain": "Терен", + "losFrequencyLabel": "Честота", + "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", + "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението умножава 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, за да достигне k приблизително {kFactor} за текущата лента {frequencyMHz} MHz, което определя извитата граница на радиохоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index bdea5749..1f678986 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1746,5 +1746,21 @@ "losPointName": "Punktname", "losShowPanelTooltip": "LOS-Panel anzeigen", "losHidePanelTooltip": "LOS-Panel ausblenden", - "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Höhendaten: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Funkhorizont", + "losLegendLosBeam": "Sichtlinie", + "losLegendTerrain": "Gelände", + "losFrequencyLabel": "Frequenz", + "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", + "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz multipliziert die Berechnung 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, um k etwa {kFactor} für das aktuelle Band {frequencyMHz} MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 99db15dd..9721f6bd 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1746,5 +1746,21 @@ "losPointName": "Nombre del punto", "losShowPanelTooltip": "Mostrar panel LOS", "losHidePanelTooltip": "Ocultar panel LOS", - "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Datos de elevación: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte radioeléctrico", + "losLegendLosBeam": "Línea de visión", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frecuencia", + "losFrequencyInfoTooltip": "Ver detalles del cálculo", + "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", + "losFrequencyDialogDescription": "Partiendo de k={baselineK} a {baselineFreq} MHz, el cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para alcanzar k aprox {kFactor} para la banda actual {frequencyMHz} MHz, lo que define el límite curvo del horizonte radioeléctrico.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bc82195d..c6d5de34 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nom du point", "losShowPanelTooltip": "Afficher le panneau LOS", "losHidePanelTooltip": "Masquer le panneau LOS", - "losElevationAttribution": "Données d'altitude : Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizon radio", + "losLegendLosBeam": "Ligne de visée", + "losLegendTerrain": "Terrain", + "losFrequencyLabel": "Fréquence", + "losFrequencyInfoTooltip": "Voir les détails du calcul", + "losFrequencyDialogTitle": "Calcul de l’horizon radio", + "losFrequencyDialogDescription": "En partant de k={baselineK} à {baselineFreq} MHz, le calcul multiplie 0.15 × (frequency − {baselineFreq}) / {baselineFreq} pour atteindre k env {kFactor} pour la bande actuelle {frequencyMHz} MHz, ce qui définit la limite courbe de l’horizon radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index fe4bffc4..a51cb6bf 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nome del punto", "losShowPanelTooltip": "Mostra il pannello LOS", "losHidePanelTooltip": "Nascondi il pannello LOS", - "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dati di elevazione: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Orizzonte radio", + "losLegendLosBeam": "Linea di vista", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequenza", + "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", + "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo moltiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} per raggiungere k circa {kFactor} per la banda corrente {frequencyMHz} MHz, che definisce il limite curvo dell’orizzonte radio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 7da07146..52c4246f 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2868,22 +2868,22 @@ class AppLocalizationsBg extends AppLocalizations { 'Данни за надморска височина: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Радиохоризонт'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Линия на видимост'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Терен'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Честота'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Преглед на детайли за изчислението'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Изчисляване на радиохоризонта'; @override String losFrequencyDialogDescription( @@ -2892,7 +2892,7 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението умножава 0.15 × (frequency − $baselineFreq) / $baselineFreq, за да достигне k приблизително $kFactor за текущата лента $frequencyMHz MHz, което определя извитата граница на радиохоризонта.'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b81565c2..e5eb247f 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2874,22 +2874,22 @@ class AppLocalizationsDe extends AppLocalizations { String get losElevationAttribution => 'Höhendaten: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Funkhorizont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Sichtlinie'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Gelände'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequenz'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Details zur Berechnung anzeigen'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Berechnung des Funkhorizonts'; @override String losFrequencyDialogDescription( @@ -2898,7 +2898,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz multipliziert die Berechnung 0.15 × (frequency − $baselineFreq) / $baselineFreq, um k etwa $kFactor für das aktuelle Band $frequencyMHz MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 971d9d3b..f411b187 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2868,22 +2868,22 @@ class AppLocalizationsEs extends AppLocalizations { 'Datos de elevación: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horizonte radioeléctrico'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Línea de visión'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frecuencia'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Ver detalles del cálculo'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico'; @override String losFrequencyDialogDescription( @@ -2892,7 +2892,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Partiendo de k=$baselineK a $baselineFreq MHz, el cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para alcanzar k aprox $kFactor para la banda actual $frequencyMHz MHz, lo que define el límite curvo del horizonte radioeléctrico.'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1627987f..2269ea1b 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2880,25 +2880,25 @@ class AppLocalizationsFr extends AppLocalizations { @override String get losElevationAttribution => - 'Données d\'altitude : Open-Meteo (CC BY 4.0)'; + 'Données d’altitude : Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horizon radio'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Ligne de visée'; @override String get losLegendTerrain => 'Terrain'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Fréquence'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Voir les détails du calcul'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Calcul de l’horizon radio'; @override String losFrequencyDialogDescription( @@ -2907,7 +2907,7 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'En partant de k=$baselineK à $baselineFreq MHz, le calcul multiplie 0.15 × (frequency − $baselineFreq) / $baselineFreq pour atteindre k env $kFactor pour la bande actuelle $frequencyMHz MHz, ce qui définit la limite courbe de l’horizon radio.'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index dccc31af..91d7c355 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2868,22 +2868,22 @@ class AppLocalizationsIt extends AppLocalizations { 'Dati di elevazione: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Orizzonte radio'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linea di vista'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequenza'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Visualizza i dettagli del calcolo'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Calcolo dell’orizzonte radio'; @override String losFrequencyDialogDescription( @@ -2892,7 +2892,7 @@ class AppLocalizationsIt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo moltiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq per raggiungere k circa $kFactor per la banda corrente $frequencyMHz MHz, che definisce il limite curvo dell’orizzonte radio.'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b02ffa76..8e03b537 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2858,22 +2858,22 @@ class AppLocalizationsNl extends AppLocalizations { 'Hoogtegegevens: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Radiohorizon'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Zichtlijn'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terrein'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequentie'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Bekijk details van de berekening'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Berekening van de radiohorizon'; @override String losFrequencyDialogDescription( @@ -2882,7 +2882,7 @@ class AppLocalizationsNl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Uitgaande van k=$baselineK bij $baselineFreq MHz vermenigvuldigt de berekening 0.15 × (frequency − $baselineFreq) / $baselineFreq om k ongeveer $kFactor te bereiken voor de huidige band $frequencyMHz MHz, wat de gebogen radiohorizon-grens definieert.'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index b8e27051..29bbbc4b 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2864,22 +2864,22 @@ class AppLocalizationsPl extends AppLocalizations { 'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horyzont radiowy'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linia widoczności'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Teren'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Częstotliwość'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego'; @override String losFrequencyDialogDescription( @@ -2888,7 +2888,7 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenie mnoży 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby osiągnąć k około $kFactor dla bieżącego pasma $frequencyMHz MHz, co definiuje zakrzywioną granicę horyzontu radiowego.'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b492c333..e3673f52 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2867,22 +2867,22 @@ class AppLocalizationsPt extends AppLocalizations { 'Dados de elevação: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Horizonte de rádio'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linha de visada'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terreno'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frequência'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Ver detalhes do cálculo'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Cálculo do horizonte de rádio'; @override String losFrequencyDialogDescription( @@ -2891,7 +2891,7 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Partindo de k=$baselineK a $baselineFreq MHz, o cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para atingir k aprox $kFactor para a banda atual $frequencyMHz MHz, o que define o limite curvo do horizonte de rádio.'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 383bf09c..44ccf7cb 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2870,22 +2870,22 @@ class AppLocalizationsRu extends AppLocalizations { 'Данные о высоте: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Радиогоризонт'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Линия прямой видимости'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Рельеф'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Просмотреть детали расчёта'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Расчёт радиогоризонта'; @override String losFrequencyDialogDescription( @@ -2894,7 +2894,7 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Начиная с k=$baselineK при $baselineFreq MHz, расчёт умножает 0.15 × (frequency − $baselineFreq) / $baselineFreq, чтобы получить k примерно $kFactor для текущего диапазона $frequencyMHz MHz, что определяет изогнутую границу радиогоризонта.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 474cf32e..39c277bc 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2852,22 +2852,22 @@ class AppLocalizationsSk extends AppLocalizations { 'Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Rádiový horizont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Priama viditeľnosť'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terén'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frekvencia'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Zobraziť podrobnosti výpočtu'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Výpočet rádiového horizontu'; @override String losFrequencyDialogDescription( @@ -2876,7 +2876,7 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Vychádzajúc z k=$baselineK pri $baselineFreq MHz výpočet násobí 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby dosiahol k približne $kFactor pre aktuálne pásmo $frequencyMHz MHz, čo definuje zakrivenú hranicu rádiového horizontu.'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 6662bc14..db973e89 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2855,22 +2855,22 @@ class AppLocalizationsSl extends AppLocalizations { 'Podatki o višini: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Radijski horizont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Linija vidnosti'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Teren'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frekvenca'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Prikaži podrobnosti izračuna'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Izračun radijskega horizonta'; @override String losFrequencyDialogDescription( @@ -2879,7 +2879,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Začenši z k=$baselineK pri $baselineFreq MHz izračun množi 0.15 × (frequency − $baselineFreq) / $baselineFreq, da doseže k približno $kFactor za trenutni pas $frequencyMHz MHz, kar določa ukrivljeno mejo radijskega horizonta.'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 35a532bd..8a3f29fd 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2838,22 +2838,22 @@ class AppLocalizationsSv extends AppLocalizations { String get losElevationAttribution => 'Höjddata: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Radiohorisont'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Siktlinje'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Terräng'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Frekvens'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Visa detaljer om beräkningen'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Beräkning av radiohorisonten'; @override String losFrequencyDialogDescription( @@ -2862,7 +2862,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Med utgångspunkt från k=$baselineK vid $baselineFreq MHz multiplicerar beräkningen 0.15 × (frequency − $baselineFreq) / $baselineFreq för att nå k cirka $kFactor för det aktuella bandet $frequencyMHz MHz, vilket definierar den krökta radiohorisontgränsen.'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 4e7130bd..1d38ca05 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2878,22 +2878,22 @@ class AppLocalizationsUk extends AppLocalizations { 'Дані про висоту: Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => 'Радіогоризонт'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => 'Лінія прямої видимості'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => 'Рельєф'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => 'Частота'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => 'Переглянути деталі розрахунку'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => 'Розрахунок радіогоризонту'; @override String losFrequencyDialogDescription( @@ -2902,7 +2902,7 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Починаючи з k=$baselineK при $baselineFreq MHz, розрахунок множить 0.15 × (frequency − $baselineFreq) / $baselineFreq, щоб досягти k приблизно $kFactor для поточного діапазону $frequencyMHz MHz, що визначає вигнуту межу радіогоризонту.'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 2ecf38e9..fa1a34a3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2716,22 +2716,22 @@ class AppLocalizationsZh extends AppLocalizations { String get losElevationAttribution => '高程数据:Open-Meteo (CC BY 4.0)'; @override - String get losLegendRadioHorizon => 'Radio horizon'; + String get losLegendRadioHorizon => '无线电地平线'; @override - String get losLegendLosBeam => 'LOS beam'; + String get losLegendLosBeam => '视距波束'; @override - String get losLegendTerrain => 'Terrain'; + String get losLegendTerrain => '地形'; @override - String get losFrequencyLabel => 'Frequency'; + String get losFrequencyLabel => '频率'; @override - String get losFrequencyInfoTooltip => 'View calculation details'; + String get losFrequencyInfoTooltip => '查看计算详情'; @override - String get losFrequencyDialogTitle => 'Radio horizon calculation'; + String get losFrequencyDialogTitle => '无线电地平线计算'; @override String losFrequencyDialogDescription( @@ -2740,7 +2740,7 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return '从 $baselineFreq MHz 的 k=$baselineK 开始,计算将 0.15 × (frequency − $baselineFreq) / $baselineFreq 相乘,以在当前频段 $frequencyMHz MHz 下得到约 k=$kFactor,从而定义弯曲的无线电地平线边界。'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 2f39fdf1..f47a3b95 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1718,5 +1718,21 @@ "losPointName": "Puntnaam", "losShowPanelTooltip": "Toon LOS-paneel", "losHidePanelTooltip": "LOS-paneel verbergen", - "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Hoogtegegevens: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorizon", + "losLegendLosBeam": "Zichtlijn", + "losLegendTerrain": "Terrein", + "losFrequencyLabel": "Frequentie", + "losFrequencyInfoTooltip": "Bekijk details van de berekening", + "losFrequencyDialogTitle": "Berekening van de radiohorizon", + "losFrequencyDialogDescription": "Uitgaande van k={baselineK} bij {baselineFreq} MHz vermenigvuldigt de berekening 0.15 × (frequency − {baselineFreq}) / {baselineFreq} om k ongeveer {kFactor} te bereiken voor de huidige band {frequencyMHz} MHz, wat de gebogen radiohorizon-grens definieert.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0432f8f6..3b56900f 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nazwa punktu", "losShowPanelTooltip": "Pokaż panel LOS", "losHidePanelTooltip": "Ukryj panel LOS", - "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horyzont radiowy", + "losLegendLosBeam": "Linia widoczności", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Częstotliwość", + "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", + "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenie mnoży 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby osiągnąć k około {kFactor} dla bieżącego pasma {frequencyMHz} MHz, co definiuje zakrzywioną granicę horyzontu radiowego.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 01c5a83c..a058bff5 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1718,5 +1718,21 @@ "losPointName": "Nome do ponto", "losShowPanelTooltip": "Mostrar painel LOS", "losHidePanelTooltip": "Ocultar painel LOS", - "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Dados de elevação: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Horizonte de rádio", + "losLegendLosBeam": "Linha de visada", + "losLegendTerrain": "Terreno", + "losFrequencyLabel": "Frequência", + "losFrequencyInfoTooltip": "Ver detalhes do cálculo", + "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", + "losFrequencyDialogDescription": "Partindo de k={baselineK} a {baselineFreq} MHz, o cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para atingir k aprox {kFactor} para a banda atual {frequencyMHz} MHz, o que define o limite curvo do horizonte de rádio.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b8a20d95..f439754c 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -958,5 +958,21 @@ "losPointName": "Имя точки", "losShowPanelTooltip": "Показать панель LOS", "losHidePanelTooltip": "Скрыть панель LOS", - "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Данные о высоте: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радиогоризонт", + "losLegendLosBeam": "Линия прямой видимости", + "losLegendTerrain": "Рельеф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Просмотреть детали расчёта", + "losFrequencyDialogTitle": "Расчёт радиогоризонта", + "losFrequencyDialogDescription": "Начиная с k={baselineK} при {baselineFreq} MHz, расчёт умножает 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, чтобы получить k примерно {kFactor} для текущего диапазона {frequencyMHz} MHz, что определяет изогнутую границу радиогоризонта.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 32452828..0801b8da 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1718,5 +1718,21 @@ "losPointName": "Názov bodu", "losShowPanelTooltip": "Zobraziť panel LOS", "losHidePanelTooltip": "Skryť panel LOS", - "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Údaje o nadmorskej výške: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Rádiový horizont", + "losLegendLosBeam": "Priama viditeľnosť", + "losLegendTerrain": "Terén", + "losFrequencyLabel": "Frekvencia", + "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", + "losFrequencyDialogTitle": "Výpočet rádiového horizontu", + "losFrequencyDialogDescription": "Vychádzajúc z k={baselineK} pri {baselineFreq} MHz výpočet násobí 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby dosiahol k približne {kFactor} pre aktuálne pásmo {frequencyMHz} MHz, čo definuje zakrivenú hranicu rádiového horizontu.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index c560c31f..d7e9ab3e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1718,5 +1718,21 @@ "losPointName": "Ime točke", "losShowPanelTooltip": "Pokaži ploščo LOS", "losHidePanelTooltip": "Skrij ploščo LOS", - "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Podatki o višini: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radijski horizont", + "losLegendLosBeam": "Linija vidnosti", + "losLegendTerrain": "Teren", + "losFrequencyLabel": "Frekvenca", + "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", + "losFrequencyDialogTitle": "Izračun radijskega horizonta", + "losFrequencyDialogDescription": "Začenši z k={baselineK} pri {baselineFreq} MHz izračun množi 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, da doseže k približno {kFactor} za trenutni pas {frequencyMHz} MHz, kar določa ukrivljeno mejo radijskega horizonta.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index b93c5ca0..dcb40696 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1718,5 +1718,21 @@ "losPointName": "Punktnamn", "losShowPanelTooltip": "Visa LOS-panelen", "losHidePanelTooltip": "Dölj LOS-panelen", - "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Höjddata: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Radiohorisont", + "losLegendLosBeam": "Siktlinje", + "losLegendTerrain": "Terräng", + "losFrequencyLabel": "Frekvens", + "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", + "losFrequencyDialogTitle": "Beräkning av radiohorisonten", + "losFrequencyDialogDescription": "Med utgångspunkt från k={baselineK} vid {baselineFreq} MHz multiplicerar beräkningen 0.15 × (frequency − {baselineFreq}) / {baselineFreq} för att nå k cirka {kFactor} för det aktuella bandet {frequencyMHz} MHz, vilket definierar den krökta radiohorisontgränsen.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 235e4ede..d339ec24 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1718,5 +1718,21 @@ "losPointName": "Назва точки", "losShowPanelTooltip": "Показати панель LOS", "losHidePanelTooltip": "Приховати панель LOS", - "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "Дані про висоту: Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "Радіогоризонт", + "losLegendLosBeam": "Лінія прямої видимості", + "losLegendTerrain": "Рельєф", + "losFrequencyLabel": "Частота", + "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", + "losFrequencyDialogTitle": "Розрахунок радіогоризонту", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} при {baselineFreq} MHz, розрахунок множить 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, щоб досягти k приблизно {kFactor} для поточного діапазону {frequencyMHz} MHz, що визначає вигнуту межу радіогоризонту.", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 72f48adc..626cbac1 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1718,5 +1718,21 @@ "losPointName": "点名称", "losShowPanelTooltip": "显示 LOS 面板", "losHidePanelTooltip": "隐藏 LOS 面板", - "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)" -} \ No newline at end of file + "losElevationAttribution": "高程数据:Open-Meteo (CC BY 4.0)", + "losLegendRadioHorizon": "无线电地平线", + "losLegendLosBeam": "视距波束", + "losLegendTerrain": "地形", + "losFrequencyLabel": "频率", + "losFrequencyInfoTooltip": "查看计算详情", + "losFrequencyDialogTitle": "无线电地平线计算", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 的 k={baselineK} 开始,计算将 0.15 × (frequency − {baselineFreq}) / {baselineFreq} 相乘,以在当前频段 {frequencyMHz} MHz 下得到约 k={kFactor},从而定义弯曲的无线电地平线边界。", + "@losFrequencyDialogDescription": { + "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", + "placeholders": { + "baselineK": { "type": "double" }, + "baselineFreq": { "type": "double" }, + "frequencyMHz": { "type": "double" }, + "kFactor": { "type": "double" } + } + } +} diff --git a/untranslated.json b/untranslated.json index f9183cb7..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,141 +1 @@ -{ - "bg": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "de": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "es": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "fr": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "it": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "nl": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "pl": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "pt": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "ru": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "sk": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "sl": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "sv": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "uk": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ], - - "zh": [ - "losLegendRadioHorizon", - "losLegendLosBeam", - "losLegendTerrain", - "losFrequencyLabel", - "losFrequencyInfoTooltip", - "losFrequencyDialogTitle", - "losFrequencyDialogDescription" - ] -} +{} \ No newline at end of file From 78f1a7b28e5e79fdeb5dae7e44fa4f85470d775b Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:12:32 -0500 Subject: [PATCH 78/99] fix: normalize stored frequency --- lib/screens/line_of_sight_map_screen.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 785dfe5f..efbdf346 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -993,11 +993,9 @@ class _LineOfSightMapScreenState extends State { ); } - double? _normalizeFrequencyMHz(int? frequencyHz) { - if (frequencyHz == null || frequencyHz <= 0) return null; - if (frequencyHz >= 1000000) return frequencyHz / 1e6; - if (frequencyHz >= 1000) return frequencyHz / 1e3; - return frequencyHz.toDouble(); + double? _normalizeFrequencyMHz(int? frequencyKHz) { + if (frequencyKHz == null || frequencyKHz <= 0) return null; + return frequencyKHz / 1000.0; } } From e2585c099289157145e98c1cfe09d2a62eb47a52 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:44:21 -0500 Subject: [PATCH 79/99] fix: reduce rebuilds in los panel --- lib/screens/line_of_sight_map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index efbdf346..c8b5a888 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -428,7 +428,7 @@ class _LineOfSightMapScreenState extends State { Widget _buildControlPanel(bool isImperial) { _sanitizeSelection(); final segment = _primarySegmentResult(); - final connector = context.watch(); + final connector = context.read(); final reportedFrequencyMHz = _normalizeFrequencyMHz( connector.currentFreqHz, ); From ea2f35ec2ebd52e3043fa8f04532eb68618e07f9 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:59:18 -0500 Subject: [PATCH 80/99] fix: keep los metadata on failure --- lib/services/line_of_sight_service.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/services/line_of_sight_service.dart b/lib/services/line_of_sight_service.dart index 61e9e27f..7f056c81 100644 --- a/lib/services/line_of_sight_service.dart +++ b/lib/services/line_of_sight_service.dart @@ -50,13 +50,13 @@ class LineOfSightResult { const LineOfSightResult.error({ required this.totalDistanceMeters, required this.errorMessage, + this.usedKFactor = 4.0 / 3.0, + this.frequencyMHz, }) : hasData = false, isClear = false, maxObstructionMeters = 0, firstObstructionDistanceMeters = null, - samples = const [], - usedKFactor = 4.0 / 3.0, - frequencyMHz = null; + samples = const []; } class LineOfSightPathSegment { @@ -203,6 +203,8 @@ class LineOfSightService { return LineOfSightResult.error( totalDistanceMeters: totalDistanceMeters, errorMessage: errorElevationUnavailable, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } @@ -227,9 +229,11 @@ class LineOfSightService { double obstructionToleranceMeters = 0.0, }) { if (points.length < 2 || elevations.length != points.length) { - return const LineOfSightResult.error( + return LineOfSightResult.error( totalDistanceMeters: 0, errorMessage: errorInvalidInput, + usedKFactor: kFactor, + frequencyMHz: frequencyMHz, ); } From 74e29a6c0f6f8f2e257de6f07134bb76468e1185 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:12:04 -0500 Subject: [PATCH 81/99] fix: clamp los profile bounds --- lib/l10n/app_en.arb | 2 +- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/screens/line_of_sight_map_screen.dart | 14 ++++++++------ 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 99ce9e21..49f2d348 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1673,7 +1673,7 @@ "losFrequencyLabel": "Frequency", "losFrequencyInfoTooltip": "View calculation details", "losFrequencyDialogTitle": "Radio horizon calculation", - "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", + "losFrequencyDialogDescription": "Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1996ad60..5f0cd5e1 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5043,7 +5043,7 @@ abstract class AppLocalizations { /// Explain how the calculation uses the baseline frequency and derived k-factor. /// /// In en, this message translates to: - /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation multiplies 0.15 × (frequency − {baselineFreq}) / {baselineFreq} to reach k approx {kFactor} for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** + /// **'Starting from k={baselineK} at {baselineFreq} MHz, the calculation adjusts the k-factor for the current {frequencyMHz} MHz band, which defines the curved radio horizon cap.'** String losFrequencyDialogDescription( double baselineK, double baselineFreq, diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 5c7cf365..98fee85a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2849,7 +2849,7 @@ class AppLocalizationsEn extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation multiplies 0.15 × (frequency − $baselineFreq) / $baselineFreq to reach k approx $kFactor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; + return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.'; } @override diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index c8b5a888..be164e31 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -1065,13 +1065,15 @@ class _LosProfilePainter extends CustomPainter { samples.last.terrainMeters, ); - double distanceForCanvasX(double x) => - ((x - horizontalPadding) / chartWidth) * maxDist; + double distanceForCanvasX(double x) { + final normalized = ((x - horizontalPadding) / chartWidth).clamp(0.0, 1.0); + return normalized * maxDist; + } - double elevationToPixel(double elevation) => - size.height - - verticalPadding - - ((elevation - minY) / ySpan) * chartHeight; + double elevationToPixel(double elevation) { + final normalized = ((elevation - minY) / ySpan).clamp(0.0, 1.0); + return size.height - verticalPadding - normalized * chartHeight; + } double extrapolateTerrain(double distance, bool isLeft) { final samplesForSlope = isLeft From 1a9b7b0d55597d8302d44665527678faa39b1a54 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:18:02 -0500 Subject: [PATCH 82/99] chore: remove 0.15 text --- translate_arb.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 translate_arb.py diff --git a/translate_arb.py b/translate_arb.py new file mode 100644 index 00000000..737c059b --- /dev/null +++ b/translate_arb.py @@ -0,0 +1,104 @@ +import json +import time +from pathlib import Path + +import requests + + +SOURCE_PATH = Path("lib/l10n/app_en.arb") +L10N_DIR = Path("lib/l10n") +API_URL = "https://libretranslate.de/translate" +DELAY_SECONDS = 0.5 + + +def load_json(path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def save_json(path: Path, data: dict) -> None: + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +def translate_text(text: str, target_locale: str) -> str | None: + payload = { + "q": text, + "source": "en", + "target": target_locale, + "format": "text", + } + try: + response = requests.post(API_URL, json=payload, timeout=30) + response.raise_for_status() + translated = response.json().get("translatedText") + return translated + except requests.RequestException as exc: + print(f"[{target_locale}] Translation failed: {exc}") + except ValueError: + print(f"[{target_locale}] Invalid response from translation service") + return None + + +def translate_locale( + locale: str, + target_path: Path, + english_data: dict, +) -> None: + print(f"Processing locale '{locale}'") + target_data = load_json(target_path) + updated = False + missing_keys = [] + + for key, value in english_data.items(): + if key.startswith("@"): + continue + if not isinstance(value, str): + continue + target_value = target_data.get(key) + if target_value is None or (isinstance(target_value, str) and target_value.strip() == ""): + missing_keys.append((key, value)) + + if not missing_keys: + print(f" -> No missing entries for {locale}") + return + + print(f" -> Translating {len(missing_keys)} entries") + for key, english_text in missing_keys: + time.sleep(DELAY_SECONDS) + translated = translate_text(english_text, locale) + if translated: + target_data[key] = translated + updated = True + else: + print(f" → [{locale}] Keeping English text for {key}") + target_data[key] = english_text + + metadata_key = f"@{key}" + if metadata_key not in target_data: + target_data[metadata_key] = {"description": ""} + updated = True + + if updated: + save_json(target_path, target_data) + print(f" → Saved translations for {locale}") + else: + print(f" → No updates written for {locale}") + + +def main() -> None: + english_data = load_json(SOURCE_PATH) + if not english_data: + print("English source not found or empty") + return + + locales = sorted(L10N_DIR.glob("app_*.arb")) + for path in locales: + if path.name == SOURCE_PATH.name: + continue + locale = path.name.split("_", 1)[1].split(".")[0] + translate_locale(locale, path, english_data) + + +if __name__ == "__main__": + main() From 2188b4972689e058cd35c7ded607f6a45c9dd8b5 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:55:43 -0500 Subject: [PATCH 83/99] fix: refresh los localization --- lib/l10n/app_bg.arb | 20 ++++++++++++++------ lib/l10n/app_de.arb | 20 ++++++++++++++------ lib/l10n/app_en.arb | 18 +++++++++++++----- lib/l10n/app_es.arb | 20 ++++++++++++++------ lib/l10n/app_fr.arb | 20 ++++++++++++++------ lib/l10n/app_it.arb | 20 ++++++++++++++------ lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 20 ++++++++++++++------ lib/l10n/app_pl.arb | 20 ++++++++++++++------ lib/l10n/app_pt.arb | 20 ++++++++++++++------ lib/l10n/app_ru.arb | 20 ++++++++++++++------ lib/l10n/app_sk.arb | 20 ++++++++++++++------ lib/l10n/app_sl.arb | 20 ++++++++++++++------ lib/l10n/app_sv.arb | 20 ++++++++++++++------ lib/l10n/app_uk.arb | 20 ++++++++++++++------ lib/l10n/app_zh.arb | 20 ++++++++++++++------ 29 files changed, 223 insertions(+), 103 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index c2451019..83c35e79 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Честота", "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", - "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението умножава 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, за да достигне k приблизително {kFactor} за текущата лента {frequencyMHz} MHz, което определя извитата граница на радиохоризонта.", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {frequencyMHz} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 1f678986..e5243bf9 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1753,14 +1753,22 @@ "losFrequencyLabel": "Frequenz", "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", - "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz multipliziert die Berechnung 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, um k etwa {kFactor} für das aktuelle Band {frequencyMHz} MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {frequencyMHz} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 49f2d348..8f231e78 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1677,10 +1677,18 @@ "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } }, "contacts_pathTrace": "Path Trace", @@ -1763,4 +1771,4 @@ "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_lastSeen": "Last seen" -} +} \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9721f6bd..d0bd7326 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1753,14 +1753,22 @@ "losFrequencyLabel": "Frecuencia", "losFrequencyInfoTooltip": "Ver detalles del cálculo", "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", - "losFrequencyDialogDescription": "Partiendo de k={baselineK} a {baselineFreq} MHz, el cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para alcanzar k aprox {kFactor} para la banda actual {frequencyMHz} MHz, lo que define el límite curvo del horizonte radioeléctrico.", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {frequencyMHz} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c6d5de34..81cffc31 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Fréquence", "losFrequencyInfoTooltip": "Voir les détails du calcul", "losFrequencyDialogTitle": "Calcul de l’horizon radio", - "losFrequencyDialogDescription": "En partant de k={baselineK} à {baselineFreq} MHz, le calcul multiplie 0.15 × (frequency − {baselineFreq}) / {baselineFreq} pour atteindre k env {kFactor} pour la bande actuelle {frequencyMHz} MHz, ce qui définit la limite courbe de l’horizon radio.", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {frequencyMHz} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a51cb6bf..25e3918a 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frequenza", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", - "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo moltiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} per raggiungere k circa {kFactor} per la banda corrente {frequencyMHz} MHz, che definisce il limite curvo dell’orizzonte radio.", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {frequencyMHz} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 52c4246f..c300e5e3 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението умножава 0.15 × (frequency − $baselineFreq) / $baselineFreq, за да достигне k приблизително $kFactor за текущата лента $frequencyMHz MHz, което определя извитата граница на радиохоризонта.'; + return 'Започвайки от k=$baselineK при $frequencyMHz MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e5eb247f..a6107e54 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2898,7 +2898,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Ausgehend von k=$baselineK bei $baselineFreq MHz multipliziert die Berechnung 0.15 × (frequency − $baselineFreq) / $baselineFreq, um k etwa $kFactor für das aktuelle Band $frequencyMHz MHz zu erreichen, was die gekrümmte Funkhorizont-Begrenzung definiert.'; + return 'Ausgehend von k=$baselineK bei $frequencyMHz MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index f411b187..8bd50c77 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partiendo de k=$baselineK a $baselineFreq MHz, el cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para alcanzar k aprox $kFactor para la banda actual $frequencyMHz MHz, lo que define el límite curvo del horizonte radioeléctrico.'; + return 'A partir de k=$baselineK en $frequencyMHz MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 2269ea1b..d22ede1f 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2907,7 +2907,7 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'En partant de k=$baselineK à $baselineFreq MHz, le calcul multiplie 0.15 × (frequency − $baselineFreq) / $baselineFreq pour atteindre k env $kFactor pour la bande actuelle $frequencyMHz MHz, ce qui définit la limite courbe de l’horizon radio.'; + return 'À partir de k=$baselineK à $frequencyMHz MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 91d7c355..01351080 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsIt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo moltiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq per raggiungere k circa $kFactor per la banda corrente $frequencyMHz MHz, che definisce il limite curvo dell’orizzonte radio.'; + return 'Partendo da k=$baselineK a $frequencyMHz MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8e03b537..3e9bc0a9 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2882,7 +2882,7 @@ class AppLocalizationsNl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Uitgaande van k=$baselineK bij $baselineFreq MHz vermenigvuldigt de berekening 0.15 × (frequency − $baselineFreq) / $baselineFreq om k ongeveer $kFactor te bereiken voor de huidige band $frequencyMHz MHz, wat de gebogen radiohorizon-grens definieert.'; + return 'Beginnend met k=$baselineK bij $frequencyMHz MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 29bbbc4b..c0e75fd2 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2888,7 +2888,7 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenie mnoży 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby osiągnąć k około $kFactor dla bieżącego pasma $frequencyMHz MHz, co definiuje zakrzywioną granicę horyzontu radiowego.'; + return 'Zaczynając od k=$baselineK przy $frequencyMHz MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index e3673f52..de53c867 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2891,7 +2891,7 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partindo de k=$baselineK a $baselineFreq MHz, o cálculo multiplica 0.15 × (frequency − $baselineFreq) / $baselineFreq para atingir k aprox $kFactor para a banda atual $frequencyMHz MHz, o que define o limite curvo do horizonte de rádio.'; + return 'Começando em k=$baselineK em $frequencyMHz MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 44ccf7cb..c32e6634 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2894,7 +2894,7 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Начиная с k=$baselineK при $baselineFreq MHz, расчёт умножает 0.15 × (frequency − $baselineFreq) / $baselineFreq, чтобы получить k примерно $kFactor для текущего диапазона $frequencyMHz MHz, что определяет изогнутую границу радиогоризонта.'; + return 'Начиная с k=$baselineK на частоте $frequencyMHz МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 39c277bc..2326e11c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2876,7 +2876,7 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Vychádzajúc z k=$baselineK pri $baselineFreq MHz výpočet násobí 0.15 × (frequency − $baselineFreq) / $baselineFreq, aby dosiahol k približne $kFactor pre aktuálne pásmo $frequencyMHz MHz, čo definuje zakrivenú hranicu rádiového horizontu.'; + return 'Počnúc od k=$baselineK pri $frequencyMHz MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index db973e89..181a8946 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2879,7 +2879,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Začenši z k=$baselineK pri $baselineFreq MHz izračun množi 0.15 × (frequency − $baselineFreq) / $baselineFreq, da doseže k približno $kFactor za trenutni pas $frequencyMHz MHz, kar določa ukrivljeno mejo radijskega horizonta.'; + return 'Začenši od k=$baselineK pri $frequencyMHz MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 8a3f29fd..a71a8a5e 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2862,7 +2862,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Med utgångspunkt från k=$baselineK vid $baselineFreq MHz multiplicerar beräkningen 0.15 × (frequency − $baselineFreq) / $baselineFreq för att nå k cirka $kFactor för det aktuella bandet $frequencyMHz MHz, vilket definierar den krökta radiohorisontgränsen.'; + return 'Med start från k=$baselineK vid $frequencyMHz MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1d38ca05..ffdf835d 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2902,7 +2902,7 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Починаючи з k=$baselineK при $baselineFreq MHz, розрахунок множить 0.15 × (frequency − $baselineFreq) / $baselineFreq, щоб досягти k приблизно $kFactor для поточного діапазону $frequencyMHz MHz, що визначає вигнуту межу радіогоризонту.'; + return 'Починаючи з k=$baselineK на $frequencyMHz МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index fa1a34a3..18424665 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2740,7 +2740,7 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return '从 $baselineFreq MHz 的 k=$baselineK 开始,计算将 0.15 × (frequency − $baselineFreq) / $baselineFreq 相乘,以在当前频段 $frequencyMHz MHz 下得到约 k=$kFactor,从而定义弯曲的无线电地平线边界。'; + return '从 $frequencyMHz MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index f47a3b95..17e4b3ce 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frequentie", "losFrequencyInfoTooltip": "Bekijk details van de berekening", "losFrequencyDialogTitle": "Berekening van de radiohorizon", - "losFrequencyDialogDescription": "Uitgaande van k={baselineK} bij {baselineFreq} MHz vermenigvuldigt de berekening 0.15 × (frequency − {baselineFreq}) / {baselineFreq} om k ongeveer {kFactor} te bereiken voor de huidige band {frequencyMHz} MHz, wat de gebogen radiohorizon-grens definieert.", + "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {frequencyMHz} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 3b56900f..db45f75e 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Częstotliwość", "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", - "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenie mnoży 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby osiągnąć k około {kFactor} dla bieżącego pasma {frequencyMHz} MHz, co definiuje zakrzywioną granicę horyzontu radiowego.", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {frequencyMHz} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index a058bff5..c3557e50 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frequência", "losFrequencyInfoTooltip": "Ver detalhes do cálculo", "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", - "losFrequencyDialogDescription": "Partindo de k={baselineK} a {baselineFreq} MHz, o cálculo multiplica 0.15 × (frequency − {baselineFreq}) / {baselineFreq} para atingir k aprox {kFactor} para a banda atual {frequencyMHz} MHz, o que define o limite curvo do horizonte de rádio.", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {frequencyMHz} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index f439754c..7c8a325f 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -965,14 +965,22 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Просмотреть детали расчёта", "losFrequencyDialogTitle": "Расчёт радиогоризонта", - "losFrequencyDialogDescription": "Начиная с k={baselineK} при {baselineFreq} MHz, расчёт умножает 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, чтобы получить k примерно {kFactor} для текущего диапазона {frequencyMHz} MHz, что определяет изогнутую границу радиогоризонта.", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {frequencyMHz} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 0801b8da..c3d75477 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frekvencia", "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", "losFrequencyDialogTitle": "Výpočet rádiového horizontu", - "losFrequencyDialogDescription": "Vychádzajúc z k={baselineK} pri {baselineFreq} MHz výpočet násobí 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, aby dosiahol k približne {kFactor} pre aktuálne pásmo {frequencyMHz} MHz, čo definuje zakrivenú hranicu rádiového horizontu.", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {frequencyMHz} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index d7e9ab3e..1350f791 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frekvenca", "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", "losFrequencyDialogTitle": "Izračun radijskega horizonta", - "losFrequencyDialogDescription": "Začenši z k={baselineK} pri {baselineFreq} MHz izračun množi 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, da doseže k približno {kFactor} za trenutni pas {frequencyMHz} MHz, kar določa ukrivljeno mejo radijskega horizonta.", + "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {frequencyMHz} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index dcb40696..2394f87d 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Frekvens", "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", "losFrequencyDialogTitle": "Beräkning av radiohorisonten", - "losFrequencyDialogDescription": "Med utgångspunkt från k={baselineK} vid {baselineFreq} MHz multiplicerar beräkningen 0.15 × (frequency − {baselineFreq}) / {baselineFreq} för att nå k cirka {kFactor} för det aktuella bandet {frequencyMHz} MHz, vilket definierar den krökta radiohorisontgränsen.", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {frequencyMHz} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index d339ec24..5ead3a20 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", "losFrequencyDialogTitle": "Розрахунок радіогоризонту", - "losFrequencyDialogDescription": "Починаючи з k={baselineK} при {baselineFreq} MHz, розрахунок множить 0.15 × (frequency − {baselineFreq}) / {baselineFreq}, щоб досягти k приблизно {kFactor} для поточного діапазону {frequencyMHz} MHz, що визначає вигнуту межу радіогоризонту.", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {frequencyMHz} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 626cbac1..5ecdebfb 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1725,14 +1725,22 @@ "losFrequencyLabel": "频率", "losFrequencyInfoTooltip": "查看计算详情", "losFrequencyDialogTitle": "无线电地平线计算", - "losFrequencyDialogDescription": "从 {baselineFreq} MHz 的 k={baselineK} 开始,计算将 0.15 × (frequency − {baselineFreq}) / {baselineFreq} 相乘,以在当前频段 {frequencyMHz} MHz 下得到约 k={kFactor},从而定义弯曲的无线电地平线边界。", + "losFrequencyDialogDescription": "从 {frequencyMHz} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { - "baselineK": { "type": "double" }, - "baselineFreq": { "type": "double" }, - "frequencyMHz": { "type": "double" }, - "kFactor": { "type": "double" } + "baselineK": { + "type": "double" + }, + "baselineFreq": { + "type": "double" + }, + "frequencyMHz": { + "type": "double" + }, + "kFactor": { + "type": "double" + } } } -} +} \ No newline at end of file From ddc87f3a274234d22c2c54c7c3717490a45929ec Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:14:00 -0500 Subject: [PATCH 84/99] chore: remove translation script --- translate_arb.py | 104 ----------------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 translate_arb.py diff --git a/translate_arb.py b/translate_arb.py deleted file mode 100644 index 737c059b..00000000 --- a/translate_arb.py +++ /dev/null @@ -1,104 +0,0 @@ -import json -import time -from pathlib import Path - -import requests - - -SOURCE_PATH = Path("lib/l10n/app_en.arb") -L10N_DIR = Path("lib/l10n") -API_URL = "https://libretranslate.de/translate" -DELAY_SECONDS = 0.5 - - -def load_json(path: Path) -> dict: - if not path.exists(): - return {} - return json.loads(path.read_text(encoding="utf-8")) - - -def save_json(path: Path, data: dict) -> None: - path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") - - -def translate_text(text: str, target_locale: str) -> str | None: - payload = { - "q": text, - "source": "en", - "target": target_locale, - "format": "text", - } - try: - response = requests.post(API_URL, json=payload, timeout=30) - response.raise_for_status() - translated = response.json().get("translatedText") - return translated - except requests.RequestException as exc: - print(f"[{target_locale}] Translation failed: {exc}") - except ValueError: - print(f"[{target_locale}] Invalid response from translation service") - return None - - -def translate_locale( - locale: str, - target_path: Path, - english_data: dict, -) -> None: - print(f"Processing locale '{locale}'") - target_data = load_json(target_path) - updated = False - missing_keys = [] - - for key, value in english_data.items(): - if key.startswith("@"): - continue - if not isinstance(value, str): - continue - target_value = target_data.get(key) - if target_value is None or (isinstance(target_value, str) and target_value.strip() == ""): - missing_keys.append((key, value)) - - if not missing_keys: - print(f" -> No missing entries for {locale}") - return - - print(f" -> Translating {len(missing_keys)} entries") - for key, english_text in missing_keys: - time.sleep(DELAY_SECONDS) - translated = translate_text(english_text, locale) - if translated: - target_data[key] = translated - updated = True - else: - print(f" → [{locale}] Keeping English text for {key}") - target_data[key] = english_text - - metadata_key = f"@{key}" - if metadata_key not in target_data: - target_data[metadata_key] = {"description": ""} - updated = True - - if updated: - save_json(target_path, target_data) - print(f" → Saved translations for {locale}") - else: - print(f" → No updates written for {locale}") - - -def main() -> None: - english_data = load_json(SOURCE_PATH) - if not english_data: - print("English source not found or empty") - return - - locales = sorted(L10N_DIR.glob("app_*.arb")) - for path in locales: - if path.name == SOURCE_PATH.name: - continue - locale = path.name.split("_", 1)[1].split(".")[0] - translate_locale(locale, path, english_data) - - -if __name__ == "__main__": - main() From faefef14ff2a0f22c3a6db24cbd9783bd6939613 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:29:36 -0500 Subject: [PATCH 85/99] fix: restore baseline freq in los text --- lib/l10n/app_bg.arb | 4 ++-- lib/l10n/app_de.arb | 4 ++-- lib/l10n/app_es.arb | 4 ++-- lib/l10n/app_fr.arb | 4 ++-- lib/l10n/app_it.arb | 4 ++-- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/l10n/app_nl.arb | 4 ++-- lib/l10n/app_pl.arb | 4 ++-- lib/l10n/app_pt.arb | 4 ++-- lib/l10n/app_ru.arb | 4 ++-- lib/l10n/app_sk.arb | 4 ++-- lib/l10n/app_sl.arb | 4 ++-- lib/l10n/app_sv.arb | 4 ++-- lib/l10n/app_uk.arb | 4 ++-- lib/l10n/app_zh.arb | 4 ++-- 28 files changed, 42 insertions(+), 42 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 83c35e79..b94b2cbd 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Честота", "losFrequencyInfoTooltip": "Преглед на детайли за изчислението", "losFrequencyDialogTitle": "Изчисляване на радиохоризонта", - "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {frequencyMHz} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", + "losFrequencyDialogDescription": "Започвайки от k={baselineK} при {baselineFreq} MHz, изчислението коригира k-фактора за текущата {frequencyMHz} MHz лента, която определя границата на извития радиохоризонт.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index e5243bf9..3963e317 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1753,7 +1753,7 @@ "losFrequencyLabel": "Frequenz", "losFrequencyInfoTooltip": "Details zur Berechnung anzeigen", "losFrequencyDialogTitle": "Berechnung des Funkhorizonts", - "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {frequencyMHz} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", + "losFrequencyDialogDescription": "Ausgehend von k={baselineK} bei {baselineFreq} MHz passt die Berechnung den k-Faktor für das aktuelle {frequencyMHz} MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1771,4 +1771,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index d0bd7326..d1940937 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1753,7 +1753,7 @@ "losFrequencyLabel": "Frecuencia", "losFrequencyInfoTooltip": "Ver detalles del cálculo", "losFrequencyDialogTitle": "Cálculo del horizonte radioeléctrico", - "losFrequencyDialogDescription": "A partir de k={baselineK} en {frequencyMHz} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", + "losFrequencyDialogDescription": "A partir de k={baselineK} en {baselineFreq} MHz, el cálculo ajusta el factor k para la banda actual de {frequencyMHz} MHz, que define el límite curvo del horizonte de radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1771,4 +1771,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 81cffc31..f3e9ea83 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Fréquence", "losFrequencyInfoTooltip": "Voir les détails du calcul", "losFrequencyDialogTitle": "Calcul de l’horizon radio", - "losFrequencyDialogDescription": "À partir de k={baselineK} à {frequencyMHz} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", + "losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 25e3918a..8b095f5f 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frequenza", "losFrequencyInfoTooltip": "Visualizza i dettagli del calcolo", "losFrequencyDialogTitle": "Calcolo dell’orizzonte radio", - "losFrequencyDialogDescription": "Partendo da k={baselineK} a {frequencyMHz} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", + "losFrequencyDialogDescription": "Partendo da k={baselineK} a {baselineFreq} MHz, il calcolo regola il fattore k per l'attuale banda {frequencyMHz} MHz, che definisce il limite curvo dell'orizzonte radio.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index c300e5e3..91e5a947 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsBg extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Започвайки от k=$baselineK при $frequencyMHz MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; + return 'Започвайки от k=$baselineK при $baselineFreq MHz, изчислението коригира k-фактора за текущата $frequencyMHz MHz лента, която определя границата на извития радиохоризонт.'; } @override diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index a6107e54..4c591e5c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2898,7 +2898,7 @@ class AppLocalizationsDe extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Ausgehend von k=$baselineK bei $frequencyMHz MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; + return 'Ausgehend von k=$baselineK bei $baselineFreq MHz passt die Berechnung den k-Faktor für das aktuelle $frequencyMHz MHz-Band an, das die gekrümmte Funkhorizontobergrenze definiert.'; } @override diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8bd50c77..b868aadd 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsEs extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'A partir de k=$baselineK en $frequencyMHz MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; + return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.'; } @override diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d22ede1f..a939e091 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2907,7 +2907,7 @@ class AppLocalizationsFr extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'À partir de k=$baselineK à $frequencyMHz MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; + return 'À partir de k=$baselineK à $baselineFreq MHz, le calcul ajuste le facteur k pour la bande actuelle de $frequencyMHz MHz, ce qui définit la limite incurvée de l\'horizon radio.'; } @override diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 01351080..d4cda691 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2892,7 +2892,7 @@ class AppLocalizationsIt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Partendo da k=$baselineK a $frequencyMHz MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; + return 'Partendo da k=$baselineK a $baselineFreq MHz, il calcolo regola il fattore k per l\'attuale banda $frequencyMHz MHz, che definisce il limite curvo dell\'orizzonte radio.'; } @override diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3e9bc0a9..5c382270 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2882,7 +2882,7 @@ class AppLocalizationsNl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Beginnend met k=$baselineK bij $frequencyMHz MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; + return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.'; } @override diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c0e75fd2..6f68786e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2888,7 +2888,7 @@ class AppLocalizationsPl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Zaczynając od k=$baselineK przy $frequencyMHz MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; + return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.'; } @override diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index de53c867..cedf4007 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2891,7 +2891,7 @@ class AppLocalizationsPt extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Começando em k=$baselineK em $frequencyMHz MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; + return 'Começando em k=$baselineK em $baselineFreq MHz, o cálculo ajusta o fator k para a banda atual de $frequencyMHz MHz, que define o limite do horizonte de rádio curvo.'; } @override diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index c32e6634..d35b1749 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2894,7 +2894,7 @@ class AppLocalizationsRu extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Начиная с k=$baselineK на частоте $frequencyMHz МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; + return 'Начиная с k=$baselineK на частоте $baselineFreq МГц, расчет корректирует коэффициент k для текущего диапазона $frequencyMHz МГц, который определяет изогнутую границу радиогоризонта.'; } @override diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 2326e11c..c4f9a920 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2876,7 +2876,7 @@ class AppLocalizationsSk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Počnúc od k=$baselineK pri $frequencyMHz MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; + return 'Počnúc od k=$baselineK pri $baselineFreq MHz výpočet upraví k-faktor pre aktuálne pásmo $frequencyMHz MHz, ktorý definuje zakrivený strop rádiového horizontu.'; } @override diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 181a8946..a012ef11 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2879,7 +2879,7 @@ class AppLocalizationsSl extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Začenši od k=$baselineK pri $frequencyMHz MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; + return 'Začenši od k=$baselineK pri $baselineFreq MHz, izračun prilagodi k-faktor za trenutni pas $frequencyMHz MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.'; } @override diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a71a8a5e..abfd89c2 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2862,7 +2862,7 @@ class AppLocalizationsSv extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Med start från k=$baselineK vid $frequencyMHz MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; + return 'Med start från k=$baselineK vid $baselineFreq MHz, justerar beräkningen k-faktorn för det aktuella $frequencyMHz MHz-bandet, som definierar den böjda radiohorisonten.'; } @override diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index ffdf835d..c6b8ff23 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2902,7 +2902,7 @@ class AppLocalizationsUk extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return 'Починаючи з k=$baselineK на $frequencyMHz МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; + return 'Починаючи з k=$baselineK на $baselineFreq МГц, обчислення коригує k-фактор для поточного діапазону $frequencyMHz МГц, який визначає викривлену межу радіогоризонту.'; } @override diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 18424665..5677c090 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2740,7 +2740,7 @@ class AppLocalizationsZh extends AppLocalizations { double frequencyMHz, double kFactor, ) { - return '从 $frequencyMHz MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; + return '从 $baselineFreq MHz 处的 k=$baselineK 开始,计算调整当前 $frequencyMHz MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。'; } @override diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 17e4b3ce..a94560bf 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frequentie", "losFrequencyInfoTooltip": "Bekijk details van de berekening", "losFrequencyDialogTitle": "Berekening van de radiohorizon", - "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {frequencyMHz} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", + "losFrequencyDialogDescription": "Beginnend met k={baselineK} bij {baselineFreq} MHz, wordt bij de berekening de k-factor aangepast voor de huidige {frequencyMHz} MHz-band, die de gebogen radiohorizonkap definieert.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index db45f75e..2af10587 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Częstotliwość", "losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia", "losFrequencyDialogTitle": "Obliczanie horyzontu radiowego", - "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {frequencyMHz} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", + "losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index c3557e50..c9d37241 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frequência", "losFrequencyInfoTooltip": "Ver detalhes do cálculo", "losFrequencyDialogTitle": "Cálculo do horizonte de rádio", - "losFrequencyDialogDescription": "Começando em k={baselineK} em {frequencyMHz} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", + "losFrequencyDialogDescription": "Começando em k={baselineK} em {baselineFreq} MHz, o cálculo ajusta o fator k para a banda atual de {frequencyMHz} MHz, que define o limite do horizonte de rádio curvo.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 7c8a325f..e1a2066f 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -965,7 +965,7 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Просмотреть детали расчёта", "losFrequencyDialogTitle": "Расчёт радиогоризонта", - "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {frequencyMHz} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", + "losFrequencyDialogDescription": "Начиная с k={baselineK} на частоте {baselineFreq} МГц, расчет корректирует коэффициент k для текущего диапазона {frequencyMHz} МГц, который определяет изогнутую границу радиогоризонта.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -983,4 +983,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index c3d75477..34e5933e 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frekvencia", "losFrequencyInfoTooltip": "Zobraziť podrobnosti výpočtu", "losFrequencyDialogTitle": "Výpočet rádiového horizontu", - "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {frequencyMHz} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", + "losFrequencyDialogDescription": "Počnúc od k={baselineK} pri {baselineFreq} MHz výpočet upraví k-faktor pre aktuálne pásmo {frequencyMHz} MHz, ktorý definuje zakrivený strop rádiového horizontu.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 1350f791..1371f97e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frekvenca", "losFrequencyInfoTooltip": "Prikaži podrobnosti izračuna", "losFrequencyDialogTitle": "Izračun radijskega horizonta", - "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {frequencyMHz} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", + "losFrequencyDialogDescription": "Začenši od k={baselineK} pri {baselineFreq} MHz, izračun prilagodi k-faktor za trenutni pas {frequencyMHz} MHz, ki določa ukrivljeno zgornjo mejo radijskega horizonta.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 2394f87d..2bdaec4b 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Frekvens", "losFrequencyInfoTooltip": "Visa detaljer om beräkningen", "losFrequencyDialogTitle": "Beräkning av radiohorisonten", - "losFrequencyDialogDescription": "Med start från k={baselineK} vid {frequencyMHz} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", + "losFrequencyDialogDescription": "Med start från k={baselineK} vid {baselineFreq} MHz, justerar beräkningen k-faktorn för det aktuella {frequencyMHz} MHz-bandet, som definierar den böjda radiohorisonten.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 5ead3a20..13d93620 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "Частота", "losFrequencyInfoTooltip": "Переглянути деталі розрахунку", "losFrequencyDialogTitle": "Розрахунок радіогоризонту", - "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {frequencyMHz} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", + "losFrequencyDialogDescription": "Починаючи з k={baselineK} на {baselineFreq} МГц, обчислення коригує k-фактор для поточного діапазону {frequencyMHz} МГц, який визначає викривлену межу радіогоризонту.", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5ecdebfb..b2dc3305 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1725,7 +1725,7 @@ "losFrequencyLabel": "频率", "losFrequencyInfoTooltip": "查看计算详情", "losFrequencyDialogTitle": "无线电地平线计算", - "losFrequencyDialogDescription": "从 {frequencyMHz} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", + "losFrequencyDialogDescription": "从 {baselineFreq} MHz 处的 k={baselineK} 开始,计算调整当前 {frequencyMHz} MHz 频段的 k 因子,该因子定义了弯曲的无线电范围上限。", "@losFrequencyDialogDescription": { "description": "Explain how the calculation uses the baseline frequency and derived k-factor.", "placeholders": { @@ -1743,4 +1743,4 @@ } } } -} \ No newline at end of file +} From 6065059241c3c884e559c45c4393db42a6615880 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:35:51 -0500 Subject: [PATCH 86/99] fix: keep los panel reactive --- lib/screens/line_of_sight_map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index be164e31..196cd2e7 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -111,7 +111,7 @@ class _LineOfSightMapScreenState extends State { }); try { - final connector = context.read(); + final connector = context.watch(); final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], From 0f17e2382cb2dea3176eae9372b23dfff5205d60 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:41:32 -0500 Subject: [PATCH 87/99] feat(chat): add global pinch-to-zoom text scaling via ChatTextScaleService --- lib/main.dart | 8 ++ lib/screens/channel_chat_screen.dart | 118 +++++++++++++-------- lib/screens/chat_screen.dart | 123 +++++++++++++--------- lib/services/chat_text_scale_service.dart | 45 ++++++++ lib/widgets/chat_zoom_wrapper.dart | 45 ++++++++ 5 files changed, 247 insertions(+), 92 deletions(-) create mode 100644 lib/services/chat_text_scale_service.dart create mode 100644 lib/widgets/chat_zoom_wrapper.dart diff --git a/lib/main.dart b/lib/main.dart index 3650a7e5..5a11188c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'services/ble_debug_log_service.dart'; import 'services/app_debug_log_service.dart'; import 'services/background_service.dart'; import 'services/map_tile_cache_service.dart'; +import 'services/chat_text_scale_service.dart'; import 'storage/prefs_manager.dart'; import 'utils/app_logger.dart'; @@ -34,6 +35,7 @@ void main() async { final appDebugLogService = AppDebugLogService(); final backgroundService = BackgroundService(); final mapTileCacheService = MapTileCacheService(); + final chatTextScaleService = ChatTextScaleService(); // Load settings await appSettingsService.loadSettings(); @@ -50,6 +52,8 @@ void main() async { await backgroundService.initialize(); _registerThirdPartyLicenses(); + await chatTextScaleService.initialize(); + // Wire up connector with services connector.initialize( retryService: retryService, @@ -78,6 +82,7 @@ void main() async { bleDebugLogService: bleDebugLogService, appDebugLogService: appDebugLogService, mapTileCacheService: mapTileCacheService, + chatTextScaleService: chatTextScaleService, ), ); } @@ -112,6 +117,7 @@ class MeshCoreApp extends StatelessWidget { final BleDebugLogService bleDebugLogService; final AppDebugLogService appDebugLogService; final MapTileCacheService mapTileCacheService; + final ChatTextScaleService chatTextScaleService; const MeshCoreApp({ super.key, @@ -123,6 +129,7 @@ class MeshCoreApp extends StatelessWidget { required this.bleDebugLogService, required this.appDebugLogService, required this.mapTileCacheService, + required this.chatTextScaleService, }); @override @@ -135,6 +142,7 @@ class MeshCoreApp extends StatelessWidget { ChangeNotifierProvider.value(value: appSettingsService), ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: appDebugLogService), + ChangeNotifierProvider.value(value: chatTextScaleService), Provider.value(value: storage), Provider.value(value: mapTileCacheService), ], diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index 9df91c3b..79d30e5e 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -18,7 +18,9 @@ import '../l10n/l10n.dart'; import '../models/channel.dart'; import '../models/channel_message.dart'; import '../services/app_settings_service.dart'; +import '../services/chat_text_scale_service.dart'; import '../utils/emoji_utils.dart'; +import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/emoji_picker.dart'; import '../widgets/gif_message.dart'; import '../widgets/jump_to_bottom_button.dart'; @@ -219,37 +221,50 @@ class _ChannelChatScreenState extends State { return Stack( children: [ - ListView.builder( - reverse: true, // List grows from bottom up - controller: _scrollController, - padding: const EdgeInsets.all(8), - itemCount: itemCount, - itemBuilder: (context, index) { - // Loading indicator now appears at end (bottom) of reversed list - if (_isLoadingOlder && index == itemCount - 1) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, + ChatZoomWrapper( + child: ListView.builder( + reverse: true, // List grows from bottom up + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: itemCount, + itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), ), ), + ); + } + final messageIndex = index; + final message = reversedMessages[messageIndex]; + if (!_messageKeys.containsKey(message.messageId)) { + _messageKeys[message.messageId] = GlobalKey(); + } + return Container( + key: _messageKeys[message.messageId]!, + child: Builder( + builder: (context) { + final textScale = context + .select( + (service) => service.scale, + ); + return _buildMessageBubble( + message, + textScale, + ); + }, ), ); - } - final messageIndex = index; - final message = reversedMessages[messageIndex]; - if (!_messageKeys.containsKey(message.messageId)) { - _messageKeys[message.messageId] = GlobalKey(); - } - return Container( - key: _messageKeys[message.messageId]!, - child: _buildMessageBubble(message), - ); - }, + }, + ), ), JumpToBottomButton(scrollController: _scrollController), ], @@ -264,7 +279,7 @@ class _ChannelChatScreenState extends State { ); } - Widget _buildMessageBubble(ChannelMessage message) { + Widget _buildMessageBubble(ChannelMessage message, double textScale) { final settingsService = context.watch(); final enableTracing = settingsService.settings.enableMessageTracing; final isOutgoing = message.isOutgoing; @@ -278,6 +293,7 @@ class _ChannelChatScreenState extends State { const maxSwipeOffset = 64.0; const replySwipeThreshold = 64.0; + const bodyFontSize = 14.0; final messageBody = Column( crossAxisAlignment: isOutgoing ? CrossAxisAlignment.end @@ -334,7 +350,7 @@ class _ChannelChatScreenState extends State { if (gifId == null) const SizedBox(height: 4), ], if (message.replyToMessageId != null) ...[ - _buildReplyPreview(message), + _buildReplyPreview(message, textScale), const SizedBox(height: 8), ], if (poi != null) @@ -342,6 +358,7 @@ class _ChannelChatScreenState extends State { context, poi, isOutgoing, + textScale, trailing: (!enableTracing && isOutgoing) ? Padding( padding: const EdgeInsets.only(bottom: 2), @@ -415,9 +432,11 @@ class _ChannelChatScreenState extends State { Flexible( child: Linkify( text: message.text, - style: const TextStyle(fontSize: 14), - linkStyle: const TextStyle( - fontSize: 14, + style: TextStyle( + fontSize: bodyFontSize * textScale, + ), + linkStyle: TextStyle( + fontSize: bodyFontSize * textScale, color: Colors.green, decoration: TextDecoration.underline, ), @@ -595,7 +614,7 @@ class _ChannelChatScreenState extends State { ); } - Widget _buildReplyPreview(ChannelMessage message) { + Widget _buildReplyPreview(ChannelMessage message, double textScale) { final connector = context.read(); final isOwnNode = message.replyToSenderName == connector.selfName; final replyText = message.replyToText ?? ''; @@ -623,7 +642,7 @@ class _ChannelChatScreenState extends State { const SizedBox(width: 4), Text( context.l10n.chat_location, - style: TextStyle(fontSize: 12, color: previewTextColor), + style: TextStyle(fontSize: 12 * textScale, color: previewTextColor), ), ], ); @@ -633,7 +652,7 @@ class _ChannelChatScreenState extends State { maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 12, + fontSize: 12 * textScale, color: previewTextColor, fontStyle: FontStyle.italic, ), @@ -657,7 +676,7 @@ class _ChannelChatScreenState extends State { Text( context.l10n.chat_replyTo(message.replyToSenderName ?? ''), style: TextStyle( - fontSize: 11, + fontSize: 11 * textScale, fontWeight: FontWeight.bold, color: isOwnNode ? Theme.of(context).colorScheme.primary @@ -736,7 +755,8 @@ class _ChannelChatScreenState extends State { Widget _buildPoiMessage( BuildContext context, _PoiInfo poi, - bool isOutgoing, { + bool isOutgoing, + double textScale, { Widget? trailing, }) { final colorScheme = Theme.of(context).colorScheme; @@ -774,12 +794,16 @@ class _ChannelChatScreenState extends State { children: [ Text( context.l10n.chat_poiShared, - style: TextStyle(color: textColor, fontWeight: FontWeight.w600), + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + fontSize: 14 * textScale, + ), ), if (poi.label.isNotEmpty) Text( poi.label, - style: TextStyle(color: metaColor, fontSize: 12), + style: TextStyle(color: metaColor, fontSize: 12 * textScale), ), ], ), @@ -849,7 +873,7 @@ class _ChannelChatScreenState extends State { return colors[hash.abs() % colors.length]; } - Widget _buildReplyBanner() { + Widget _buildReplyBanner(double textScale) { final message = _replyingToMessage!; return Container( width: double.infinity, @@ -875,7 +899,7 @@ class _ChannelChatScreenState extends State { Text( context.l10n.chat_replyingTo(message.senderName), style: TextStyle( - fontSize: 12, + fontSize: 12 * textScale, fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onSecondaryContainer, ), @@ -885,7 +909,7 @@ class _ChannelChatScreenState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 11, + fontSize: 11 * textScale, color: Theme.of( context, ).colorScheme.onSecondaryContainer.withValues(alpha: 0.7), @@ -912,7 +936,15 @@ class _ChannelChatScreenState extends State { return Column( mainAxisSize: MainAxisSize.min, children: [ - if (_replyingToMessage != null) _buildReplyBanner(), + if (_replyingToMessage != null) + Builder( + builder: (context) { + final textScale = context.select( + (service) => service.scale, + ); + return _buildReplyBanner(textScale); + }, + ), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 3556d6de..d623f33d 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -22,7 +22,9 @@ import '../models/contact.dart'; import '../models/message.dart'; import '../models/path_history.dart'; import '../services/app_settings_service.dart'; +import '../services/chat_text_scale_service.dart'; import '../services/path_history_service.dart'; +import '../widgets/chat_zoom_wrapper.dart'; import '../widgets/elements_ui.dart'; import 'channel_message_path_screen.dart'; import 'map_screen.dart'; @@ -270,52 +272,62 @@ class _ChatScreenState extends State { _scrollController.scrollToBottomIfAtBottom(); }); - return ListView.builder( - reverse: true, // List grows from bottom up - controller: _scrollController, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), - itemCount: itemCount, - itemBuilder: (context, index) { - // Loading indicator now appears at end (bottom) of reversed list - if (_isLoadingOlder && index == itemCount - 1) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), + return ChatZoomWrapper( + child: ListView.builder( + reverse: true, // List grows from bottom up + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + itemCount: itemCount, + itemBuilder: (context, index) { + // Loading indicator now appears at end (bottom) of reversed list + if (_isLoadingOlder && index == itemCount - 1) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), ), - ), - ); - } - final messageIndex = index; - Contact contact = widget.contact; - final message = reversedMessages[messageIndex]; - String fourByteHex = ''; - if (widget.contact.type == advTypeRoom) { - contact = _resolveContactFrom4Bytes( - connector, - message.fourByteRoomContactKey.isEmpty - ? Uint8List.fromList([0, 0, 0, 0]) - : message.fourByteRoomContactKey, - ); - fourByteHex = message.fourByteRoomContactKey - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join() - .toUpperCase(); - } + ); + } + final messageIndex = index; + Contact contact = widget.contact; + final message = reversedMessages[messageIndex]; + String fourByteHex = ''; + if (widget.contact.type == advTypeRoom) { + contact = _resolveContactFrom4Bytes( + connector, + message.fourByteRoomContactKey.isEmpty + ? Uint8List.fromList([0, 0, 0, 0]) + : message.fourByteRoomContactKey, + ); + fourByteHex = message.fourByteRoomContactKey + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join() + .toUpperCase(); + } - return _MessageBubble( - message: message, - senderName: widget.contact.type == advTypeRoom - ? "${contact.name} [$fourByteHex]" - : contact.name, - isRoomServer: widget.contact.type == advTypeRoom, - onTap: () => _openMessagePath(message, contact), - onLongPress: () => _showMessageActions(message, contact), - ); - }, + return Builder( + builder: (context) { + final textScale = context.select( + (service) => service.scale, + ); + return _MessageBubble( + message: message, + senderName: widget.contact.type == advTypeRoom + ? "${contact.name} [$fourByteHex]" + : contact.name, + isRoomServer: widget.contact.type == advTypeRoom, + textScale: textScale, + onTap: () => _openMessagePath(message, contact), + onLongPress: () => _showMessageActions(message, contact), + ); + }, + ); + }, + ), ); } @@ -1163,11 +1175,13 @@ class _MessageBubble extends StatelessWidget { final bool isRoomServer; final VoidCallback? onTap; final VoidCallback? onLongPress; + final double textScale; const _MessageBubble({ required this.message, required this.senderName, required this.isRoomServer, + required this.textScale, this.onTap, this.onLongPress, }); @@ -1190,6 +1204,7 @@ class _MessageBubble extends StatelessWidget { ? colorScheme.onErrorContainer : (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface); final metaColor = textColor.withValues(alpha: 0.7); + const bodyFontSize = 14.0; String messageText = message.text; if (isRoomServer && !isOutgoing) { messageText = message.text.substring(4.clamp(0, message.text.length)); @@ -1258,6 +1273,7 @@ class _MessageBubble extends StatelessWidget { poi, textColor, metaColor, + textScale, trailing: (!enableTracing && isOutgoing) ? Padding( padding: const EdgeInsets.only(bottom: 2), @@ -1321,10 +1337,14 @@ class _MessageBubble extends StatelessWidget { Flexible( child: Linkify( text: messageText, - style: TextStyle(color: textColor), - linkStyle: const TextStyle( + style: TextStyle( + color: textColor, + fontSize: bodyFontSize * textScale, + ), + linkStyle: TextStyle( color: Colors.green, decoration: TextDecoration.underline, + fontSize: bodyFontSize * textScale, ), options: const LinkifyOptions( humanize: false, @@ -1464,7 +1484,8 @@ class _MessageBubble extends StatelessWidget { BuildContext context, _PoiInfo poi, Color textColor, - Color metaColor, { + Color metaColor, + double textScale, { Widget? trailing, }) { return Row( @@ -1493,12 +1514,16 @@ class _MessageBubble extends StatelessWidget { children: [ Text( context.l10n.chat_poiShared, - style: TextStyle(color: textColor, fontWeight: FontWeight.w600), + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + fontSize: 14 * textScale, + ), ), if (poi.label.isNotEmpty) Text( poi.label, - style: TextStyle(color: metaColor, fontSize: 12), + style: TextStyle(color: metaColor, fontSize: 12 * textScale), ), ], ), diff --git a/lib/services/chat_text_scale_service.dart b/lib/services/chat_text_scale_service.dart new file mode 100644 index 00000000..0257a56c --- /dev/null +++ b/lib/services/chat_text_scale_service.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; + +import '../storage/prefs_manager.dart'; + +/// Client-side accessibility/UI service that exposes a persistent shared text scale +/// factor. No MeshCoreConnector/RoomServer or protocol interaction occurs, and the +/// value is saved locally via SharedPreferences so it can be reused in Markdown +/// viewers, log panels, or other text-heavy widgets without redundant network +/// dependencies. +/// +/// Widgets should scope rebuilds using the snippet below so only the scaled text +/// is rebuilt instead of the entire chat list: +/// ```dart +/// context.select( +/// (service) => service.scale, +/// ) +/// ``` +class ChatTextScaleService extends ChangeNotifier { + static const _prefKey = 'chat_text_scale'; + static const double _minScale = 0.8; + static const double _maxScale = 1.8; + + double _scale = 1.0; + + double get scale => _scale; + + Future initialize() async { + final stored = PrefsManager.instance.getDouble(_prefKey); + if (stored != null) { + _scale = _clamp(stored); + } + } + + void setScale(double value) { + final next = _clamp(value); + if (next == _scale) return; + _scale = next; + PrefsManager.instance.setDouble(_prefKey, _scale); + notifyListeners(); + } + + void reset() => setScale(1.0); + + double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble(); +} diff --git a/lib/widgets/chat_zoom_wrapper.dart b/lib/widgets/chat_zoom_wrapper.dart new file mode 100644 index 00000000..e18662de --- /dev/null +++ b/lib/widgets/chat_zoom_wrapper.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../services/chat_text_scale_service.dart'; + +/// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables. +/// Double-tap resets the scale. Only the wrapper itself listens to gestures; +/// child scrollables keep their normal touch handling. +class ChatZoomWrapper extends StatelessWidget { + ChatZoomWrapper({super.key, required this.child, this.onDoubleTap}); + + final Widget child; + final VoidCallback? onDoubleTap; + final _ZoomGestureState _state = _ZoomGestureState(); + + @override + Widget build(BuildContext context) { + final service = context.read(); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTap: () { + service.reset(); + onDoubleTap?.call(); + }, + onScaleStart: (details) { + if (details.pointerCount != 2) return; + _state.startScale = service.scale; + }, + onScaleUpdate: (details) { + if (details.pointerCount != 2) return; + final baseScale = _state.startScale ?? service.scale; + service.setScale(baseScale * details.scale); + }, + onScaleEnd: (_) { + _state.startScale = null; + }, + child: child, + ); + } +} + +class _ZoomGestureState { + double? startScale; +} From 8a160246424023f76969f33e5bbd6e937e7c47d1 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:06:27 -0500 Subject: [PATCH 88/99] fix(chat): stabilize pinch-to-zoom scaling --- lib/services/chat_text_scale_service.dart | 33 ++++++++++++++++++++--- lib/widgets/chat_zoom_wrapper.dart | 28 ++++++++++--------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/lib/services/chat_text_scale_service.dart b/lib/services/chat_text_scale_service.dart index 0257a56c..21d6a5f8 100644 --- a/lib/services/chat_text_scale_service.dart +++ b/lib/services/chat_text_scale_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import '../storage/prefs_manager.dart'; @@ -21,6 +23,7 @@ class ChatTextScaleService extends ChangeNotifier { static const double _maxScale = 1.8; double _scale = 1.0; + Timer? _saveTimer; double get scale => _scale; @@ -31,15 +34,39 @@ class ChatTextScaleService extends ChangeNotifier { } } - void setScale(double value) { + void setScale(double value, {bool persistImmediately = false}) { final next = _clamp(value); if (next == _scale) return; _scale = next; - PrefsManager.instance.setDouble(_prefKey, _scale); notifyListeners(); + if (persistImmediately) { + _commitScale(); + } else { + _scheduleSave(); + } } - void reset() => setScale(1.0); + void reset() { + setScale(1.0, persistImmediately: true); + } + + void persist() => _commitScale(); + + @override + void dispose() { + _saveTimer?.cancel(); + super.dispose(); + } + + void _scheduleSave() { + _saveTimer?.cancel(); + _saveTimer = Timer(const Duration(milliseconds: 250), _commitScale); + } + + void _commitScale() { + _saveTimer?.cancel(); + PrefsManager.instance.setDouble(_prefKey, _scale); + } double _clamp(double value) => value.clamp(_minScale, _maxScale).toDouble(); } diff --git a/lib/widgets/chat_zoom_wrapper.dart b/lib/widgets/chat_zoom_wrapper.dart index e18662de..f0c6815e 100644 --- a/lib/widgets/chat_zoom_wrapper.dart +++ b/lib/widgets/chat_zoom_wrapper.dart @@ -6,12 +6,18 @@ import '../services/chat_text_scale_service.dart'; /// Gesture wrapper that exposes two-finger pinch-to-zoom for chat scrollables. /// Double-tap resets the scale. Only the wrapper itself listens to gestures; /// child scrollables keep their normal touch handling. -class ChatZoomWrapper extends StatelessWidget { - ChatZoomWrapper({super.key, required this.child, this.onDoubleTap}); +class ChatZoomWrapper extends StatefulWidget { + const ChatZoomWrapper({super.key, required this.child, this.onDoubleTap}); final Widget child; final VoidCallback? onDoubleTap; - final _ZoomGestureState _state = _ZoomGestureState(); + + @override + State createState() => _ChatZoomWrapperState(); +} + +class _ChatZoomWrapperState extends State { + double? _startScale; @override Widget build(BuildContext context) { @@ -21,25 +27,23 @@ class ChatZoomWrapper extends StatelessWidget { behavior: HitTestBehavior.translucent, onDoubleTap: () { service.reset(); - onDoubleTap?.call(); + service.persist(); + widget.onDoubleTap?.call(); }, onScaleStart: (details) { if (details.pointerCount != 2) return; - _state.startScale = service.scale; + _startScale = service.scale; }, onScaleUpdate: (details) { if (details.pointerCount != 2) return; - final baseScale = _state.startScale ?? service.scale; + final baseScale = _startScale ?? service.scale; service.setScale(baseScale * details.scale); }, onScaleEnd: (_) { - _state.startScale = null; + _startScale = null; + service.persist(); }, - child: child, + child: widget.child, ); } } - -class _ZoomGestureState { - double? startScale; -} From 2a7cc28a3a5410417d1310fce0e204115048511f Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 23 Feb 2026 23:46:25 -0500 Subject: [PATCH 89/99] fix --- lib/l10n/app_bg.arb | 2 +- lib/l10n/app_de.arb | 2 +- lib/l10n/app_es.arb | 2 +- lib/l10n/app_fr.arb | 2 +- lib/l10n/app_it.arb | 2 +- lib/l10n/app_nl.arb | 2 +- lib/l10n/app_pl.arb | 2 +- lib/l10n/app_pt.arb | 2 +- lib/l10n/app_ru.arb | 2 +- lib/l10n/app_sk.arb | 2 +- lib/l10n/app_sl.arb | 2 +- lib/l10n/app_sv.arb | 2 +- lib/l10n/app_uk.arb | 2 +- lib/l10n/app_zh.arb | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 96db350a..b59d4078 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "bg", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 3898e76d..409bdbb4 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "de", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index e07b48be..9e031380 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "es", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f3c79bff..78c155ca 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "fr", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 22666412..a6871a1f 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "it", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index ab573557..57797f95 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "nl", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d16052d8..908892ea 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pl", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8d4e827c..d221ae5f 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "pt", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 3d178892..eb6627c4 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "ru", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index c87ef14f..2e7a06b9 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sk", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index b0fc9487..5b0cbfbf 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sl", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 7806ed7a..7cec9b7f 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "sv", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index b990b14d..2ad9ddeb 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "uk", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 295274d9..43adb812 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,4 +1,4 @@ -{ +{ "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, "@@locale": "zh", From c880c2d107c6cbc85c4ba39fb25d4d452a62f5ff Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:02:10 -0500 Subject: [PATCH 90/99] fix channel actions context --- lib/screens/channels_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index d2a2e3de..582fee74 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -522,7 +522,7 @@ class _ChannelsScreenState extends State : context.l10n.channels_muteChannel, ), onTap: () async { - Navigator.pop(context); + Navigator.pop(sheetContext); if (isMuted) { await settingsService.unmuteChannel(channel.name); } else { From 515b9c1f295220b51641a7e8587c514ce27948de Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:51:58 -0500 Subject: [PATCH 91/99] fix los init localization --- lib/screens/line_of_sight_map_screen.dart | 12 +++++++++++- pubspec.lock | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 0fbc21bd..402b417e 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -73,6 +73,7 @@ class _LineOfSightMapScreenState extends State { bool _showMarkerLabels = true; bool _didReceivePositionUpdate = false; int _losRequestNonce = 0; + bool _initialLosScheduled = false; @override void initState() { @@ -83,7 +84,16 @@ class _LineOfSightMapScreenState extends State { _end = widget.candidates[1]; } } - _runLos(); + _scheduleInitialRun(); + } + + void _scheduleInitialRun() { + if (_initialLosScheduled) return; + _initialLosScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _runLos(); + }); } @override diff --git a/pubspec.lock b/pubspec.lock index aa8819e0..fa23b27e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1140,4 +1140,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" \ No newline at end of file + flutter: ">=3.38.4" From 31db565ebfa5ccd967fb1f82b4b7a0749b65097f Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:54:03 -0500 Subject: [PATCH 92/99] PR Combined #228 #220 #219 #201 --- lib/screens/line_of_sight_map_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/screens/line_of_sight_map_screen.dart b/lib/screens/line_of_sight_map_screen.dart index 402b417e..ec8a391f 100644 --- a/lib/screens/line_of_sight_map_screen.dart +++ b/lib/screens/line_of_sight_map_screen.dart @@ -122,7 +122,7 @@ class _LineOfSightMapScreenState extends State { }); try { - final connector = context.watch(); + final connector = context.read(); final frequencyMHz = _normalizeFrequencyMHz(connector.currentFreqHz); final result = await _lineOfSightService.analyzePath( [start.point, end.point], From 5a70ed48cf3e1143f0263140b8320408522a8166 Mon Sep 17 00:00:00 2001 From: ericz Date: Tue, 24 Feb 2026 23:56:30 +0100 Subject: [PATCH 93/99] favorites handling only --- .github/workflows/build.yml | 5 ++ lib/connector/meshcore_connector.dart | 68 +++++++++++++++++++ lib/connector/meshcore_protocol.dart | 1 + lib/l10n/app_de.arb | 1 + lib/l10n/app_en.arb | 3 +- lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/models/contact.dart | 7 ++ lib/screens/contacts_screen.dart | 26 +++++++ lib/storage/contact_store.dart | 2 + lib/widgets/list_filter_widget.dart | 21 ++++-- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + untranslated.json | 54 ++++++++++++++- 27 files changed, 233 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05c82de6..c376a4ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,11 @@ jobs: ${{ runner.os }}-gradle- - run: flutter pub get - run: flutter build apk --release --no-pub + - name: Upload Debug APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: build/app/outputs/flutter-apk/app-release.apk ios: runs-on: macos-latest diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index afd1626d..ef19f020 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -669,6 +669,7 @@ class MeshCoreConnector extends ChangeNotifier { publicKey: contact.publicKey, name: contact.name, type: contact.type, + flags: contact.flags, pathLength: selection.hopCount >= 0 ? selection.hopCount : contact.pathLength, @@ -1185,11 +1186,78 @@ class MeshCoreConnector extends ChangeNotifier { customPath, pathLen, type: contact.type, + flags: contact.flags, name: contact.name, ), ); } + Future setContactFavorite(Contact contact, bool isFavorite) async { + if (!isConnected) return; + final latestContact = + await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact; + final updatedFlags = isFavorite + ? (latestContact.flags | contactFlagFavorite) + : (latestContact.flags & ~contactFlagFavorite); + + await sendFrame( + buildUpdateContactPathFrame( + latestContact.publicKey, + latestContact.path, + latestContact.pathLength, + type: latestContact.type, + flags: updatedFlags, + name: latestContact.name, + ), + ); + + final index = _contacts.indexWhere( + (c) => c.publicKeyHex == contact.publicKeyHex, + ); + if (index >= 0) { + _contacts[index] = _contacts[index].copyWith( + type: latestContact.type, + name: latestContact.name, + pathLength: latestContact.pathLength, + path: latestContact.path, + flags: updatedFlags, + ); + notifyListeners(); + unawaited(_persistContacts()); + } + } + + Future _fetchContactSnapshotFromDevice( + Uint8List pubKey, { + Duration timeout = const Duration(seconds: 3), + }) async { + if (!isConnected) return null; + final expectedKeyHex = pubKeyToHex(pubKey); + final completer = Completer(); + + void finish(Contact? result) { + if (!completer.isCompleted) { + completer.complete(result); + } + } + + final subscription = receivedFrames.listen((frame) { + if (frame.isEmpty || frame[0] != respCodeContact) return; + final parsed = Contact.fromFrame(frame); + if (parsed == null || parsed.publicKeyHex != expectedKeyHex) return; + finish(parsed); + }); + + final timer = Timer(timeout, () => finish(null)); + try { + await getContactByKey(pubKey); + return await completer.future; + } finally { + timer.cancel(); + await subscription.cancel(); + } + } + /// Set path override for a contact (persists across contact refreshes) /// pathLen: -1 = force flood, null = auto (use device path), >= 0 = specific path Future setPathOverride( diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 2933e802..d5ce9ee1 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -290,6 +290,7 @@ int _minPositive(int a, int b) { const int contactPubKeyOffset = 1; const int contactTypeOffset = 33; const int contactFlagsOffset = 34; +const int contactFlagFavorite = 0x01; const int contactPathLenOffset = 35; const int contactPathOffset = 36; const int contactNameOffset = 100; diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index a1647fa3..f596ae43 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1343,6 +1343,7 @@ "listFilter_az": "A-Z", "listFilter_filters": "Filtere", "listFilter_all": "Alle", + "listFilter_favorites": "Favoriten", "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eec03bc3..5b15b65a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1555,6 +1555,7 @@ "listFilter_az": "A-Z", "listFilter_filters": "Filters", "listFilter_all": "All", + "listFilter_favorites": "Favorites", "listFilter_users": "Users", "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", @@ -1779,4 +1780,4 @@ "settings_gpxExportShareSubject": "meshcore-open GPX map data export", "snrIndicator_nearByRepeaters": "Nearby Repeaters", "snrIndicator_lastSeen": "Last seen" -} \ No newline at end of file +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 54e9cdcd..5d6a6a24 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4772,6 +4772,12 @@ abstract class AppLocalizations { /// **'All'** String get listFilter_all; + /// No description provided for @listFilter_favorites. + /// + /// In en, this message translates to: + /// **'Favorites'** + String get listFilter_favorites; + /// No description provided for @listFilter_users. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 21a6e795..513cdc30 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2728,6 +2728,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get listFilter_all => 'Всички'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Потребители'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index e5a3a49d..bada9f6f 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2733,6 +2733,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get listFilter_all => 'Alle'; + @override + String get listFilter_favorites => 'Favoriten'; + @override String get listFilter_users => 'Benutzer'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a56e2174..b090c45b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2686,6 +2686,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get listFilter_all => 'All'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Users'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 98cd658d..ac51330f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2726,6 +2726,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get listFilter_all => 'Todas'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Usuarios'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index a52ff004..3ca86e64 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2742,6 +2742,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get listFilter_all => 'Tout'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Utilisateurs'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index aebea2ff..c9900c7d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2726,6 +2726,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get listFilter_all => 'Tutti'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Utenti'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 5460d29c..8c537f2b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2717,6 +2717,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get listFilter_all => 'Alles'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Gebruikers'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 72860335..2c11c827 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2724,6 +2724,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get listFilter_all => 'Wszystko'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Użytkownicy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 025c81ca..c2fac6c1 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2727,6 +2727,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get listFilter_all => 'Tudo'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Usuários'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 399b158d..9fde3b89 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2730,6 +2730,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get listFilter_all => 'Все'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Пользователи'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 51383119..5d314625 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2712,6 +2712,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get listFilter_all => 'Všetko'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Používatelia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f42e8e07..19934e80 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2715,6 +2715,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get listFilter_all => 'Vse'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Uporabniki'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index ba99455d..04ae8359 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2700,6 +2700,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get listFilter_all => 'Alla'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Användare'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e2bbbe8a..1c3442d6 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2737,6 +2737,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get listFilter_all => 'Все'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => 'Користувачі'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 4da17ea7..6a9881e8 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2582,6 +2582,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get listFilter_all => '全部'; + @override + String get listFilter_favorites => 'Favorites'; + @override String get listFilter_users => '用户'; diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 143a62a2..5e532e6f 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -5,6 +5,7 @@ class Contact { final Uint8List publicKey; final String name; final int type; + final int flags; final int pathLength; // -1 = flood, 0+ = direct hops (from device) final Uint8List path; // Path bytes from device final int? @@ -19,6 +20,7 @@ class Contact { required this.publicKey, required this.name, required this.type, + this.flags = 0, required this.pathLength, required this.path, this.pathOverride, @@ -58,11 +60,13 @@ class Contact { } bool get hasLocation => latitude != null && longitude != null; + bool get isFavorite => (flags & contactFlagFavorite) != 0; Contact copyWith({ Uint8List? publicKey, String? name, int? type, + int? flags, int? pathLength, Uint8List? path, int? pathOverride, @@ -77,6 +81,7 @@ class Contact { publicKey: publicKey ?? this.publicKey, name: name ?? this.name, type: type ?? this.type, + flags: flags ?? this.flags, pathLength: pathLength ?? this.pathLength, path: path ?? this.path, pathOverride: clearPathOverride @@ -167,6 +172,7 @@ class Contact { data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize), ); final type = data[contactTypeOffset]; + final flags = data[contactFlagsOffset]; final pathLen = data[contactPathLenOffset].toSigned(8); final safePathLen = pathLen > 0 ? (pathLen > maxPathSize ? maxPathSize : pathLen) @@ -191,6 +197,7 @@ class Contact { publicKey: pubKey, name: name.isEmpty ? 'Unknown' : name, type: type, + flags: flags, pathLength: pathLen, path: pathBytes, latitude: lat, diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index c3f783c6..08e3e14a 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -13,6 +13,7 @@ import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../models/contact_group.dart'; import '../storage/contact_group_store.dart'; +import '../storage/contact_settings_store.dart'; import '../utils/contact_search.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; @@ -481,6 +482,7 @@ class _ContactsScreenState extends State contact: contact, lastSeen: _resolveLastSeen(contact), unreadCount: unreadCount, + isFavorite: contact.isFavorite, onTap: () => _openChat(context, contact), onLongPress: () => _showContactOptions(context, connector, contact), @@ -517,6 +519,7 @@ class _ContactsScreenState extends State }) .where((group) { if (_typeFilter == ContactTypeFilter.all) return true; + if (_typeFilter == ContactTypeFilter.favorites) return false; for (final key in group.memberKeys) { final contact = contactsByKey[key]; if (contact != null && _matchesTypeFilter(contact)) return true; @@ -591,6 +594,8 @@ class _ContactsScreenState extends State switch (_typeFilter) { case ContactTypeFilter.all: return true; + case ContactTypeFilter.favorites: + return contact.isFavorite; case ContactTypeFilter.users: return contact.type == advTypeChat; case ContactTypeFilter.repeaters: @@ -981,6 +986,7 @@ class _ContactsScreenState extends State ) { final isRepeater = contact.type == advTypeRepeater; final isRoom = contact.type == advTypeRoom; + final isFavorite = contact.isFavorite; showModalBottomSheet( context: context, @@ -1087,6 +1093,21 @@ class _ContactsScreenState extends State }, ), ], + ListTile( + leading: Icon( + isFavorite ? Icons.star : Icons.star_border, + color: Colors.amber[700], + ), + title: Text( + isFavorite + ? '${context.l10n.common_remove} ${context.l10n.listFilter_favorites}' + : '${context.l10n.common_add} ${context.l10n.listFilter_favorites}', + ), + onTap: () async { + Navigator.pop(sheetContext); + await connector.setContactFavorite(contact, !isFavorite); + }, + ), ListTile( leading: const Icon(Icons.copy), title: Text(context.l10n.contacts_ShareContact), @@ -1155,6 +1176,7 @@ class _ContactTile extends StatelessWidget { final Contact contact; final DateTime lastSeen; final int unreadCount; + final bool isFavorite; final VoidCallback onTap; final VoidCallback onLongPress; @@ -1162,6 +1184,7 @@ class _ContactTile extends StatelessWidget { required this.contact, required this.lastSeen, required this.unreadCount, + required this.isFavorite, required this.onTap, required this.onLongPress, }); @@ -1214,6 +1237,9 @@ class _ContactTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ + if (isFavorite) + Icon(Icons.star, size: 14, color: Colors.amber[700]), + if (isFavorite && contact.hasLocation) const SizedBox(width: 2), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], diff --git a/lib/storage/contact_store.dart b/lib/storage/contact_store.dart index 08d158b4..504ff165 100644 --- a/lib/storage/contact_store.dart +++ b/lib/storage/contact_store.dart @@ -33,6 +33,7 @@ class ContactStore { 'publicKey': base64Encode(contact.publicKey), 'name': contact.name, 'type': contact.type, + 'flags': contact.flags, 'pathLength': contact.pathLength, 'path': base64Encode(contact.path), 'pathOverride': contact.pathOverride, @@ -53,6 +54,7 @@ class ContactStore { publicKey: Uint8List.fromList(base64Decode(json['publicKey'] as String)), name: json['name'] as String? ?? 'Unknown', type: json['type'] as int? ?? 0, + flags: json['flags'] as int? ?? 0, pathLength: json['pathLength'] as int? ?? -1, path: json['path'] != null ? Uint8List.fromList(base64Decode(json['path'] as String)) diff --git a/lib/widgets/list_filter_widget.dart b/lib/widgets/list_filter_widget.dart index e9c0d9e8..473a3df5 100644 --- a/lib/widgets/list_filter_widget.dart +++ b/lib/widgets/list_filter_widget.dart @@ -3,7 +3,7 @@ import '../l10n/l10n.dart'; enum ContactSortOption { lastSeen, recentMessages, name } -enum ContactTypeFilter { all, users, repeaters, rooms } +enum ContactTypeFilter { all, favorites, users, repeaters, rooms } class SortFilterMenuOption { final int value; @@ -94,11 +94,12 @@ const int _actionSortRecentMessages = 1; const int _actionSortName = 2; const int _actionSortLastSeen = 3; const int _actionFilterAll = 4; -const int _actionFilterUsers = 5; -const int _actionFilterRepeaters = 6; -const int _actionFilterRooms = 7; -const int _actionToggleUnreadOnly = 8; -const int _actionNewGroup = 9; +const int _actionFilterFavorites = 5; +const int _actionFilterUsers = 6; +const int _actionFilterRepeaters = 7; +const int _actionFilterRooms = 8; +const int _actionToggleUnreadOnly = 9; +const int _actionNewGroup = 10; class ContactsFilterMenu extends StatelessWidget { final ContactSortOption sortOption; @@ -154,6 +155,11 @@ class ContactsFilterMenu extends StatelessWidget { label: l10n.listFilter_all, checked: typeFilter == ContactTypeFilter.all, ), + SortFilterMenuOption( + value: _actionFilterFavorites, + label: l10n.listFilter_favorites, + checked: typeFilter == ContactTypeFilter.favorites, + ), SortFilterMenuOption( value: _actionFilterUsers, label: l10n.listFilter_users, @@ -198,6 +204,9 @@ class ContactsFilterMenu extends StatelessWidget { case _actionFilterUsers: onTypeFilterChanged(ContactTypeFilter.users); break; + case _actionFilterFavorites: + onTypeFilterChanged(ContactTypeFilter.favorites); + break; case _actionFilterRepeaters: onTypeFilterChanged(ContactTypeFilter.repeaters); break; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d2ea57e9..4084d9b8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus +import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/untranslated.json b/untranslated.json index 9e26dfee..a6d29372 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,53 @@ -{} \ No newline at end of file +{ + "bg": [ + "listFilter_favorites" + ], + + "es": [ + "listFilter_favorites" + ], + + "fr": [ + "listFilter_favorites" + ], + + "it": [ + "listFilter_favorites" + ], + + "nl": [ + "listFilter_favorites" + ], + + "pl": [ + "listFilter_favorites" + ], + + "pt": [ + "listFilter_favorites" + ], + + "ru": [ + "listFilter_favorites" + ], + + "sk": [ + "listFilter_favorites" + ], + + "sl": [ + "listFilter_favorites" + ], + + "sv": [ + "listFilter_favorites" + ], + + "uk": [ + "listFilter_favorites" + ], + + "zh": [ + "listFilter_favorites" + ] +} From a26055c93f648a70ae6065033190bd91e94f07da Mon Sep 17 00:00:00 2001 From: ericz Date: Wed, 25 Feb 2026 00:49:41 +0100 Subject: [PATCH 94/99] resolved analyte code failure: unused import --- lib/screens/contacts_screen.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 08e3e14a..28e7aa51 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -13,7 +13,6 @@ import '../connector/meshcore_protocol.dart'; import '../models/contact.dart'; import '../models/contact_group.dart'; import '../storage/contact_group_store.dart'; -import '../storage/contact_settings_store.dart'; import '../utils/contact_search.dart'; import '../utils/dialog_utils.dart'; import '../utils/disconnect_navigation_mixin.dart'; From a2d1cb2a998fde7dba403bd727f854d73d637416 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 19:42:12 -0700 Subject: [PATCH 95/99] add pubspec.lock to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2d9a3fcc..157c7ece 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ migrate_working_dir/ .flutter-plugins-dependencies .pub-cache/ .pub/ +pubspec.lock /build/ /coverage/ From 190fd3b353a5b4594ac1d653fb329095fe16d3b8 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 19:44:15 -0700 Subject: [PATCH 96/99] Remove pubspec.lock from version control --- pubspec.lock | 1143 -------------------------------------------------- 1 file changed, 1143 deletions(-) delete mode 100644 pubspec.lock diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index fa23b27e..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,1143 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff - url: "https://pub.dev" - source: hosted - version: "4.0.9" - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - bluez: - dependency: transitive - description: - name: bluez - sha256: "61a7204381925896a374301498f2f5399e59827c6498ae1e924aaa598751b545" - url: "https://pub.dev" - source: hosted - version: "0.8.3" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - cached_network_image: - dependency: "direct main" - description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: - dependency: transitive - description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - characters: - dependency: "direct main" - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" - url: "https://pub.dev" - source: hosted - version: "2.0.4" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c - url: "https://pub.dev" - source: hosted - version: "0.4.2" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - code_assets: - dependency: transitive - description: - name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" - url: "https://pub.dev" - source: hosted - version: "0.3.5+2" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf - url: "https://pub.dev" - source: hosted - version: "3.0.7" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - dart_earcut: - dependency: transitive - description: - name: dart_earcut - sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dart_polylabel2: - dependency: transitive - description: - name: dart_polylabel2 - sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - dbus: - dependency: transitive - description: - name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 - url: "https://pub.dev" - source: hosted - version: "0.7.12" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file: - dependency: transitive - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blue_plus: - dependency: "direct main" - description: - name: flutter_blue_plus - sha256: "88a65ead7dea67ddcc03e6ca846163c6b6cc09a2dcebdb8bb601fcd654ea9382" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - flutter_blue_plus_android: - dependency: transitive - description: - name: flutter_blue_plus_android - sha256: "5010b0960cce533a8fa71401573f044362c3e2e111dc6eb4898c92e85f85f50c" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_darwin: - dependency: transitive - description: - name: flutter_blue_plus_darwin - sha256: "52b155d868e17c1c8ad85520a0912d447d92aedccb5a5e234d3edc98ebd1307a" - url: "https://pub.dev" - source: hosted - version: "8.1.1" - flutter_blue_plus_linux: - dependency: transitive - description: - name: flutter_blue_plus_linux - sha256: f5b02244d89465ba82c8c512686c66362fbb01f52fa03d645ed353ebf3883242 - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_platform_interface: - dependency: transitive - description: - name: flutter_blue_plus_platform_interface - sha256: "6e0fc04b77491dbfdbcd46c1a021b12f2f5fc5d6e01777f93a38a8431989b7f0" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_web: - dependency: transitive - description: - name: flutter_blue_plus_web - sha256: "376aad9595ee389c7cd56e0c373e78abcaa790c821ece9cb81f0969ec94c5bca" - url: "https://pub.dev" - source: hosted - version: "8.1.0" - flutter_blue_plus_winrt: - dependency: transitive - description: - name: flutter_blue_plus_winrt - sha256: ed894f0ab341f4cece8fa33edc381d46424a7c5bfd0e841d933d0f8c34c86521 - url: "https://pub.dev" - source: hosted - version: "0.0.18" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - flutter_foreground_task: - dependency: "direct main" - description: - name: flutter_foreground_task - sha256: "48ea45056155a99fb30b15f14f4039a044d925bc85f381ed0b2d3b00a60b99de" - url: "https://pub.dev" - source: hosted - version: "9.2.0" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" - url: "https://pub.dev" - source: hosted - version: "0.14.4" - flutter_linkify: - dependency: "direct main" - description: - name: flutter_linkify - sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "2b50e938a275e1ad77352d6a25e25770f4130baa61eaf02de7a9a884680954ad" - url: "https://pub.dev" - source: hosted - version: "20.1.0" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: dce0116868cedd2cdf768af0365fc37ff1cbef7c02c4f51d0587482e625868d0 - url: "https://pub.dev" - source: hosted - version: "7.0.0" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "23de31678a48c084169d7ae95866df9de5c9d2a44be3e5915a2ff067aeeba899" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - flutter_local_notifications_windows: - dependency: transitive - description: - name: flutter_local_notifications_windows - sha256: e97a1a3016512437d9c0b12fae7d1491c3c7b9aa7f03a69b974308840656b02a - url: "https://pub.dev" - source: hosted - version: "2.0.1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_map: - dependency: "direct main" - description: - name: flutter_map - sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" - url: "https://pub.dev" - source: hosted - version: "8.2.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - gpx: - dependency: "direct main" - description: - name: gpx - sha256: f5b12b86402c639079243600ee2b3afd85cd08d26117fc8885cf48efce471d8e - url: "https://pub.dev" - source: hosted - version: "2.3.0" - hooks: - dependency: transitive - description: - name: hooks - sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - http: - dependency: "direct main" - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce - url: "https://pub.dev" - source: hosted - version: "4.8.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" - url: "https://pub.dev" - source: hosted - version: "0.20.2" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 - url: "https://pub.dev" - source: hosted - version: "4.11.0" - latlong2: - dependency: "direct main" - description: - name: latlong2 - sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" - url: "https://pub.dev" - source: hosted - version: "0.9.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - linkify: - dependency: transitive - description: - name: linkify - sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - lists: - dependency: transitive - description: - name: lists - sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - logger: - dependency: transitive - description: - name: logger - sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 - url: "https://pub.dev" - source: hosted - version: "2.6.2" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" - url: "https://pub.dev" - source: hosted - version: "0.12.18" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - material_symbols_icons: - dependency: "direct main" - description: - name: material_symbols_icons - sha256: c62b15f2b3de98d72cbff0148812f5ef5159f05e61fc9f9a089ec2bb234df082 - url: "https://pub.dev" - source: hosted - version: "4.2906.0" - meta: - dependency: transitive - description: - name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" - url: "https://pub.dev" - source: hosted - version: "1.18.0" - mgrs_dart: - dependency: transitive - description: - name: mgrs_dart - sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - mobile_scanner: - dependency: "direct main" - description: - name: mobile_scanner - sha256: c92c26bf2231695b6d3477c8dcf435f51e28f87b1745966b1fe4c47a286171ce - url: "https://pub.dev" - source: hosted - version: "7.2.0" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" - url: "https://pub.dev" - source: hosted - version: "0.17.4" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - objective_c: - dependency: transitive - description: - name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" - url: "https://pub.dev" - source: hosted - version: "9.3.0" - octo_image: - dependency: transitive - description: - name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - package_info_plus: - dependency: "direct main" - description: - name: package_info_plus - sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d - url: "https://pub.dev" - source: hosted - version: "9.0.0" - package_info_plus_platform_interface: - dependency: transitive - description: - name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - path_provider: - dependency: "direct main" - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e - url: "https://pub.dev" - source: hosted - version: "2.2.22" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" - url: "https://pub.dev" - source: hosted - version: "2.6.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - pointycastle: - dependency: "direct main" - description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - posix: - dependency: transitive - description: - name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" - url: "https://pub.dev" - source: hosted - version: "6.5.0" - proj4dart: - dependency: transitive - description: - name: proj4dart - sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e - url: "https://pub.dev" - source: hosted - version: "2.1.0" - provider: - dependency: "direct main" - description: - name: provider - sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" - url: "https://pub.dev" - source: hosted - version: "6.1.5+1" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - qr: - dependency: transitive - description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - qr_flutter: - dependency: "direct main" - description: - name: qr_flutter - sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" - url: "https://pub.dev" - source: hosted - version: "4.1.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - share_plus: - dependency: "direct main" - description: - name: share_plus - sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" - url: "https://pub.dev" - source: hosted - version: "12.0.1" - share_plus_platform_interface: - dependency: transitive - description: - name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" - url: "https://pub.dev" - source: hosted - version: "2.5.4" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: cbc40be9be1c5af4dab4d6e0de4d5d3729e6f3d65b89d21e1815d57705644a6f - url: "https://pub.dev" - source: hosted - version: "2.4.20" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 - url: "https://pub.dev" - source: hosted - version: "2.4.3" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 - url: "https://pub.dev" - source: hosted - version: "2.4.2+2" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" - url: "https://pub.dev" - source: hosted - version: "2.5.6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: transitive - description: - name: synchronized - sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 - url: "https://pub.dev" - source: hosted - version: "3.4.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" - url: "https://pub.dev" - source: hosted - version: "0.7.9" - timezone: - dependency: transitive - description: - name: timezone - sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 - url: "https://pub.dev" - source: hosted - version: "0.10.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - unicode: - dependency: transitive - description: - name: unicode - sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" - url: "https://pub.dev" - source: hosted - version: "6.3.28" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f - url: "https://pub.dev" - source: hosted - version: "2.4.2" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" - url: "https://pub.dev" - source: hosted - version: "4.5.3" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 - url: "https://pub.dev" - source: hosted - version: "1.1.19" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" - url: "https://pub.dev" - source: hosted - version: "1.1.13" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - wakelock_plus: - dependency: "direct main" - description: - name: wakelock_plus - sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" - url: "https://pub.dev" - source: hosted - version: "1.4.0" - wakelock_plus_platform_interface: - dependency: transitive - description: - name: wakelock_plus_platform_interface - sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - web: - dependency: "direct main" - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - wkt_parser: - dependency: transitive - description: - name: wkt_parser - sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" - yaml: - dependency: transitive - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.10.3 <4.0.0" - flutter: ">=3.38.4" From 50af2e0bc9c2ddedc4bdbafcc8709071e055b886 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 20:07:15 -0700 Subject: [PATCH 97/99] Fix review issues: dedicated l10n keys, remove unrelated CI/macOS changes, translate all locales - Replace concatenated favorite toggle strings with dedicated listFilter_addToFavorites/removeFromFavorites keys - Remove unrelated CI artifact upload step from build.yml - Revert unrelated macOS GeneratedPluginRegistrant.swift change - Add comment explaining groups hidden under favorites filter - Translate new keys across all 14 locales --- .github/workflows/build.yml | 5 -- lib/l10n/app_bg.arb | 13 ++++- lib/l10n/app_de.arb | 2 + lib/l10n/app_en.arb | 2 + lib/l10n/app_es.arb | 13 ++++- lib/l10n/app_fr.arb | 13 ++++- lib/l10n/app_it.arb | 13 ++++- lib/l10n/app_localizations.dart | 12 +++++ lib/l10n/app_localizations_bg.dart | 8 ++- lib/l10n/app_localizations_de.dart | 6 +++ lib/l10n/app_localizations_en.dart | 6 +++ lib/l10n/app_localizations_es.dart | 8 ++- lib/l10n/app_localizations_fr.dart | 8 ++- lib/l10n/app_localizations_it.dart | 8 ++- lib/l10n/app_localizations_nl.dart | 8 ++- lib/l10n/app_localizations_pl.dart | 8 ++- lib/l10n/app_localizations_pt.dart | 8 ++- lib/l10n/app_localizations_ru.dart | 8 ++- lib/l10n/app_localizations_sk.dart | 8 ++- lib/l10n/app_localizations_sl.dart | 8 ++- lib/l10n/app_localizations_sv.dart | 8 ++- lib/l10n/app_localizations_uk.dart | 8 ++- lib/l10n/app_localizations_zh.dart | 8 ++- lib/l10n/app_nl.arb | 13 ++++- lib/l10n/app_pl.arb | 13 ++++- lib/l10n/app_pt.arb | 13 ++++- lib/l10n/app_ru.arb | 13 ++++- lib/l10n/app_sk.arb | 13 ++++- lib/l10n/app_sl.arb | 13 ++++- lib/l10n/app_sv.arb | 13 ++++- lib/l10n/app_uk.arb | 13 ++++- lib/l10n/app_zh.arb | 13 ++++- lib/screens/contacts_screen.dart | 5 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 - untranslated.json | 54 +------------------ 35 files changed, 266 insertions(+), 101 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c376a4ac..05c82de6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,11 +30,6 @@ jobs: ${{ runner.os }}-gradle- - run: flutter pub get - run: flutter build apk --release --no-pub - - name: Upload Debug APK - uses: actions/upload-artifact@v4 - with: - name: app-debug - path: build/app/outputs/flutter-apk/app-release.apk ios: runs-on: macos-latest diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index b88e3bae..3613e85d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "bg", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Премахване от списъка с любими", + "listFilter_addToFavorites": "Добави към любими", + "listFilter_favorites": "Любими" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f596ae43..64e8cd32 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1344,6 +1344,8 @@ "listFilter_filters": "Filtere", "listFilter_all": "Alle", "listFilter_favorites": "Favoriten", + "listFilter_addToFavorites": "Zu Favoriten hinzufügen", + "listFilter_removeFromFavorites": "Aus Favoriten entfernen", "listFilter_users": "Benutzer", "listFilter_repeaters": "Repeater", "listFilter_roomServers": "Raumserver", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5b15b65a..8d9f385f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1556,6 +1556,8 @@ "listFilter_filters": "Filters", "listFilter_all": "All", "listFilter_favorites": "Favorites", + "listFilter_addToFavorites": "Add to favorites", + "listFilter_removeFromFavorites": "Remove from favorites", "listFilter_users": "Users", "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 2f62b543..dd8ce6c0 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "es", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", @@ -1772,5 +1778,8 @@ "type": "double" } } - } + }, + "listFilter_favorites": "Favoritos", + "listFilter_removeFromFavorites": "Eliminar de las favoritas", + "listFilter_addToFavorites": "Añadir a favoritos" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 9eb66e7b..fe738f79 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "fr", "appTitle": "MeshCore Open", "nav_contacts": "Contacts", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Ajouter à mes favoris", + "listFilter_removeFromFavorites": "Supprimer des favoris", + "listFilter_favorites": "Préférences" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index e08dd675..d6c02f0c 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "it", "appTitle": "MeshCore Open", "nav_contacts": "Contatti", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Aggiungi ai preferiti", + "listFilter_removeFromFavorites": "Rimuovi dai preferiti", + "listFilter_favorites": "Preferiti" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5d6a6a24..d64cdb0f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4778,6 +4778,18 @@ abstract class AppLocalizations { /// **'Favorites'** String get listFilter_favorites; + /// No description provided for @listFilter_addToFavorites. + /// + /// In en, this message translates to: + /// **'Add to favorites'** + String get listFilter_addToFavorites; + + /// No description provided for @listFilter_removeFromFavorites. + /// + /// In en, this message translates to: + /// **'Remove from favorites'** + String get listFilter_removeFromFavorites; + /// No description provided for @listFilter_users. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 513cdc30..f9637aa7 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -2729,7 +2729,13 @@ class AppLocalizationsBg extends AppLocalizations { String get listFilter_all => 'Всички'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Любими'; + + @override + String get listFilter_addToFavorites => 'Добави към любими'; + + @override + String get listFilter_removeFromFavorites => 'Премахване от списъка с любими'; @override String get listFilter_users => 'Потребители'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index bada9f6f..15742813 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2736,6 +2736,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get listFilter_favorites => 'Favoriten'; + @override + String get listFilter_addToFavorites => 'Zu Favoriten hinzufügen'; + + @override + String get listFilter_removeFromFavorites => 'Aus Favoriten entfernen'; + @override String get listFilter_users => 'Benutzer'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b090c45b..039ad22f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2689,6 +2689,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get listFilter_favorites => 'Favorites'; + @override + String get listFilter_addToFavorites => 'Add to favorites'; + + @override + String get listFilter_removeFromFavorites => 'Remove from favorites'; + @override String get listFilter_users => 'Users'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ac51330f..8961b18a 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2727,7 +2727,13 @@ class AppLocalizationsEs extends AppLocalizations { String get listFilter_all => 'Todas'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favoritos'; + + @override + String get listFilter_addToFavorites => 'Añadir a favoritos'; + + @override + String get listFilter_removeFromFavorites => 'Eliminar de las favoritas'; @override String get listFilter_users => 'Usuarios'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3ca86e64..3737f463 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2743,7 +2743,13 @@ class AppLocalizationsFr extends AppLocalizations { String get listFilter_all => 'Tout'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Préférences'; + + @override + String get listFilter_addToFavorites => 'Ajouter à mes favoris'; + + @override + String get listFilter_removeFromFavorites => 'Supprimer des favoris'; @override String get listFilter_users => 'Utilisateurs'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index c9900c7d..b64ec6d1 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -2727,7 +2727,13 @@ class AppLocalizationsIt extends AppLocalizations { String get listFilter_all => 'Tutti'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Preferiti'; + + @override + String get listFilter_addToFavorites => 'Aggiungi ai preferiti'; + + @override + String get listFilter_removeFromFavorites => 'Rimuovi dai preferiti'; @override String get listFilter_users => 'Utenti'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 8c537f2b..523cd6e1 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2718,7 +2718,13 @@ class AppLocalizationsNl extends AppLocalizations { String get listFilter_all => 'Alles'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favorieten'; + + @override + String get listFilter_addToFavorites => 'Toevoegen aan favorieten'; + + @override + String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten'; @override String get listFilter_users => 'Gebruikers'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2c11c827..16396001 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -2725,7 +2725,13 @@ class AppLocalizationsPl extends AppLocalizations { String get listFilter_all => 'Wszystko'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Ulubione'; + + @override + String get listFilter_addToFavorites => 'Dodaj do ulubionych'; + + @override + String get listFilter_removeFromFavorites => 'Usuń z ulubionych'; @override String get listFilter_users => 'Użytkownicy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c2fac6c1..c59a4f5e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2728,7 +2728,13 @@ class AppLocalizationsPt extends AppLocalizations { String get listFilter_all => 'Tudo'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favoritos'; + + @override + String get listFilter_addToFavorites => 'Adicionar aos favoritos'; + + @override + String get listFilter_removeFromFavorites => 'Remover da lista de favoritos'; @override String get listFilter_users => 'Usuários'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9fde3b89..69d60447 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2731,7 +2731,13 @@ class AppLocalizationsRu extends AppLocalizations { String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Избранное'; + + @override + String get listFilter_addToFavorites => 'Добавить в избранное'; + + @override + String get listFilter_removeFromFavorites => 'Удалить из избранного'; @override String get listFilter_users => 'Пользователи'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 5d314625..17cbd7b9 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -2713,7 +2713,13 @@ class AppLocalizationsSk extends AppLocalizations { String get listFilter_all => 'Všetko'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Obľúbené'; + + @override + String get listFilter_addToFavorites => 'Pridaj do obľúbených'; + + @override + String get listFilter_removeFromFavorites => 'Odstrániť z označení'; @override String get listFilter_users => 'Používatelia'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 19934e80..6478b2bd 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -2716,7 +2716,13 @@ class AppLocalizationsSl extends AppLocalizations { String get listFilter_all => 'Vse'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Priljubljene'; + + @override + String get listFilter_addToFavorites => 'Dodaj v priljubljene'; + + @override + String get listFilter_removeFromFavorites => 'Odstrani iz priljubljenih'; @override String get listFilter_users => 'Uporabniki'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 04ae8359..54b998d9 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -2701,7 +2701,13 @@ class AppLocalizationsSv extends AppLocalizations { String get listFilter_all => 'Alla'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Favoriter'; + + @override + String get listFilter_addToFavorites => 'Lägg till i favoriter'; + + @override + String get listFilter_removeFromFavorites => 'Ta bort från favoriter'; @override String get listFilter_users => 'Användare'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 1c3442d6..e3564f1c 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -2738,7 +2738,13 @@ class AppLocalizationsUk extends AppLocalizations { String get listFilter_all => 'Все'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => 'Улюблені'; + + @override + String get listFilter_addToFavorites => 'Додати до улюблених'; + + @override + String get listFilter_removeFromFavorites => 'Видалити зі списку улюблених'; @override String get listFilter_users => 'Користувачі'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 6a9881e8..42f9130e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2583,7 +2583,13 @@ class AppLocalizationsZh extends AppLocalizations { String get listFilter_all => '全部'; @override - String get listFilter_favorites => 'Favorites'; + String get listFilter_favorites => '收藏'; + + @override + String get listFilter_addToFavorites => '添加到收藏'; + + @override + String get listFilter_removeFromFavorites => '从收藏中移除'; @override String get listFilter_users => '用户'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index cb690fb6..fee1f225 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "nl", "appTitle": "MeshCore Open", "nav_contacts": "Contacten", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Verwijderen uit favorieten", + "listFilter_favorites": "Favorieten", + "listFilter_addToFavorites": "Toevoegen aan favorieten" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 21619834..8096bb49 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "pl", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Usuń z ulubionych", + "listFilter_addToFavorites": "Dodaj do ulubionych", + "listFilter_favorites": "Ulubione" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8a02a4d4..53f8f934 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "pt", "appTitle": "MeshCore Open", "nav_contacts": "Contactos", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Adicionar aos favoritos", + "listFilter_removeFromFavorites": "Remover da lista de favoritos", + "listFilter_favorites": "Favoritos" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index ab2ed36b..b4d7b597 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Не удалось удалить канал {name}.", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "ru", "appTitle": "MeshCore Open", "nav_contacts": "Контакты", @@ -984,5 +990,8 @@ "type": "double" } } - } + }, + "listFilter_addToFavorites": "Добавить в избранное", + "listFilter_favorites": "Избранное", + "listFilter_removeFromFavorites": "Удалить из избранного" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index ff958971..23a0aeac 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "sk", "appTitle": "MeshCore Open", "nav_contacts": "Kontakty", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Odstrániť z označení", + "listFilter_addToFavorites": "Pridaj do obľúbených", + "listFilter_favorites": "Obľúbené" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index bd431d4d..6b64f68e 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "sl", "appTitle": "MeshCore Open", "nav_contacts": "Stiki", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_favorites": "Priljubljene", + "listFilter_removeFromFavorites": "Odstrani iz priljubljenih", + "listFilter_addToFavorites": "Dodaj v priljubljene" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 86da858d..3b96df77 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "sv", "appTitle": "MeshCore Open", "nav_contacts": "Kontakter", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Ta bort från favoriter", + "listFilter_addToFavorites": "Lägg till i favoriter", + "listFilter_favorites": "Favoriter" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 691aa8db..78b4a901 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Не вдалося видалити канал \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "uk", "appTitle": "MeshCore Open", "nav_contacts": "Контакти", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_removeFromFavorites": "Видалити зі списку улюблених", + "listFilter_addToFavorites": "Додати до улюблених", + "listFilter_favorites": "Улюблені" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f6bb5261..259e29b1 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "无法删除频道 \"{name}\"", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "zh", "appTitle": "MeshCore Open", "nav_contacts": "联系方式", @@ -1744,5 +1750,8 @@ "type": "double" } } - } + }, + "listFilter_favorites": "收藏", + "listFilter_addToFavorites": "添加到收藏", + "listFilter_removeFromFavorites": "从收藏中移除" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 28e7aa51..e9018a74 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -518,6 +518,7 @@ class _ContactsScreenState extends State }) .where((group) { if (_typeFilter == ContactTypeFilter.all) return true; + // Groups don't have a favorite flag, so hide them under favorites filter if (_typeFilter == ContactTypeFilter.favorites) return false; for (final key in group.memberKeys) { final contact = contactsByKey[key]; @@ -1099,8 +1100,8 @@ class _ContactsScreenState extends State ), title: Text( isFavorite - ? '${context.l10n.common_remove} ${context.l10n.listFilter_favorites}' - : '${context.l10n.common_add} ${context.l10n.listFilter_favorites}', + ? context.l10n.listFilter_removeFromFavorites + : context.l10n.listFilter_addToFavorites, ), onTap: () async { Navigator.pop(sheetContext); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 4084d9b8..d2ea57e9 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import flutter_blue_plus_darwin import flutter_local_notifications import mobile_scanner import package_info_plus -import path_provider_foundation import share_plus import shared_preferences_foundation import sqflite_darwin @@ -21,7 +20,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/untranslated.json b/untranslated.json index a6d29372..9e26dfee 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,53 +1 @@ -{ - "bg": [ - "listFilter_favorites" - ], - - "es": [ - "listFilter_favorites" - ], - - "fr": [ - "listFilter_favorites" - ], - - "it": [ - "listFilter_favorites" - ], - - "nl": [ - "listFilter_favorites" - ], - - "pl": [ - "listFilter_favorites" - ], - - "pt": [ - "listFilter_favorites" - ], - - "ru": [ - "listFilter_favorites" - ], - - "sk": [ - "listFilter_favorites" - ], - - "sl": [ - "listFilter_favorites" - ], - - "sv": [ - "listFilter_favorites" - ], - - "uk": [ - "listFilter_favorites" - ], - - "zh": [ - "listFilter_favorites" - ] -} +{} \ No newline at end of file From ea379ce50b3fd98341d4b39710536603569c4200 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Tue, 24 Feb 2026 20:11:56 -0700 Subject: [PATCH 98/99] Fix dart format line length in contacts_screen.dart --- lib/screens/contacts_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index e9018a74..6c683cc9 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1239,7 +1239,8 @@ class _ContactTile extends StatelessWidget { children: [ if (isFavorite) Icon(Icons.star, size: 14, color: Colors.amber[700]), - if (isFavorite && contact.hasLocation) const SizedBox(width: 2), + if (isFavorite && contact.hasLocation) + const SizedBox(width: 2), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], From e139383335cfe71cf5057ef9ef0e8aaaeb64ee7a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Thu, 26 Feb 2026 22:53:52 -0800 Subject: [PATCH 99/99] Add localized search functionality for contacts (#244) - Introduced new localization keys for searching contacts, users, favorites, repeaters, and room servers in multiple languages. - Updated localization files for Italian, Bulgarian, German, English, Spanish, French, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese. - Enhanced the contacts screen to dynamically display search hints based on the selected contact type filter. - Modified the map screen to utilize the new search functionality for contacts without a number. --- lib/l10n/app_bg.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_de.arb | 56 +++++++++++++++++++++++++++-- lib/l10n/app_en.arb | 58 +++++++++++++++++++++++++++++- lib/l10n/app_es.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_fr.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_it.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_localizations.dart | 40 +++++++++++++++++++-- lib/l10n/app_localizations_bg.dart | 30 +++++++++++++++- lib/l10n/app_localizations_de.dart | 30 +++++++++++++++- lib/l10n/app_localizations_en.dart | 30 +++++++++++++++- lib/l10n/app_localizations_es.dart | 30 +++++++++++++++- lib/l10n/app_localizations_fr.dart | 30 +++++++++++++++- lib/l10n/app_localizations_it.dart | 30 +++++++++++++++- lib/l10n/app_localizations_nl.dart | 30 +++++++++++++++- lib/l10n/app_localizations_pl.dart | 30 +++++++++++++++- lib/l10n/app_localizations_pt.dart | 30 +++++++++++++++- lib/l10n/app_localizations_ru.dart | 30 +++++++++++++++- lib/l10n/app_localizations_sk.dart | 30 +++++++++++++++- lib/l10n/app_localizations_sl.dart | 30 +++++++++++++++- lib/l10n/app_localizations_sv.dart | 30 +++++++++++++++- lib/l10n/app_localizations_uk.dart | 30 +++++++++++++++- lib/l10n/app_localizations_zh.dart | 30 +++++++++++++++- lib/l10n/app_nl.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_pl.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_pt.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_ru.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_sk.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_sl.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_sv.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_uk.arb | 48 ++++++++++++++++++++++++- lib/l10n/app_zh.arb | 48 ++++++++++++++++++++++++- lib/screens/contacts_screen.dart | 37 ++++++++++++++++++- lib/screens/map_screen.dart | 3 +- 33 files changed, 1233 insertions(+), 35 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 3613e85d..975e0671 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Премахване от списъка с любими", "listFilter_addToFavorites": "Добави към любими", - "listFilter_favorites": "Любими" + "listFilter_favorites": "Любими", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchFavorites": "Търсене на {number}{str} любими...", + "contacts_searchRoomServers": "Търсене на {number}{str} сървъри в стаята...", + "contacts_unread": "Непрочетено", + "contacts_searchRepeaters": "Търсене на {number}{str} повтарящи се...", + "contacts_searchContactsNoNumber": "Търси контакти...", + "contacts_searchUsers": "Търсене на {number}{str} потребители..." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 64e8cd32..74b6c05a 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1,6 +1,12 @@ { "channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden", - "@channels_channelDeleteFailed": { "placeholders": { "name": { "type": "String" } } }, + "@channels_channelDeleteFailed": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "@@locale": "de", "appTitle": "MeshCore Open", "nav_contacts": "Kontakte", @@ -1775,5 +1781,51 @@ "type": "double" } } - } + }, + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Ungelesen", + "contacts_searchContactsNoNumber": "Kontakte suchen...", + "contacts_searchRepeaters": "Suche {number}{str} Repeater...", + "contacts_searchFavorites": "Suche {number}{str} Favoriten...", + "contacts_searchUsers": "Suche {number}{str} Benutzer...", + "contacts_searchRoomServers": "Suche {number}{str} Raumserver..." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8d9f385f..175c3464 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -268,7 +268,63 @@ "contacts_title": "Contacts", "contacts_noContacts": "No contacts yet", "contacts_contactsWillAppear": "Contacts will appear when devices advertise", - "contacts_searchContacts": "Search contacts...", + "contacts_unread": "Unread", + "contacts_searchContactsNoNumber": "Search Contacts...", + "contacts_searchContacts": "Search {number}{str} Contacts...", + "@contacts_searchContacts": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchFavorites": "Search {number}{str} Favorites...", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "Search {number}{str} Users...", + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Search {number}{str} Repeaters...", + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Search {number}{str} Room servers...", + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, "contacts_noUnreadContacts": "No unread contacts", "contacts_noContactsFound": "No contacts or groups found", "contacts_deleteContact": "Delete Contact", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index dd8ce6c0..74339ff7 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1781,5 +1781,51 @@ }, "listFilter_favorites": "Favoritos", "listFilter_removeFromFavorites": "Eliminar de las favoritas", - "listFilter_addToFavorites": "Añadir a favoritos" + "listFilter_addToFavorites": "Añadir a favoritos", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchContactsNoNumber": "Buscar contactos...", + "contacts_unread": "No leído", + "contacts_searchFavorites": "Buscar {number}{str} Favoritos...", + "contacts_searchUsers": "Buscar {number}{str} Usuarios...", + "contacts_searchRepeaters": "Buscar {number}{str} Repetidores...", + "contacts_searchRoomServers": "Buscar {number}{str} servidores de sala..." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index fe738f79..0697aee7 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1753,5 +1753,51 @@ }, "listFilter_addToFavorites": "Ajouter à mes favoris", "listFilter_removeFromFavorites": "Supprimer des favoris", - "listFilter_favorites": "Préférences" + "listFilter_favorites": "Préférences", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Non lu", + "contacts_searchFavorites": "Rechercher {number}{str} Favoris...", + "contacts_searchUsers": "Rechercher {number}{str} utilisateurs...", + "contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...", + "contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...", + "contacts_searchContactsNoNumber": "Rechercher des contacts..." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d6c02f0c..4798d263 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1753,5 +1753,51 @@ }, "listFilter_addToFavorites": "Aggiungi ai preferiti", "listFilter_removeFromFavorites": "Rimuovi dai preferiti", - "listFilter_favorites": "Preferiti" + "listFilter_favorites": "Preferiti", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "Cerca {number}{str} Utenti...", + "contacts_searchContactsNoNumber": "Cerca Contatti...", + "contacts_searchFavorites": "Cerca {number}{str} Preferiti...", + "contacts_unread": "Non letti", + "contacts_searchRepeaters": "Cerca {number}{str} Ripetitori...", + "contacts_searchRoomServers": "Cerca {number}{str} server Room..." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index d64cdb0f..ff2c726b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1336,11 +1336,47 @@ abstract class AppLocalizations { /// **'Contacts will appear when devices advertise'** String get contacts_contactsWillAppear; + /// No description provided for @contacts_unread. + /// + /// In en, this message translates to: + /// **'Unread'** + String get contacts_unread; + + /// No description provided for @contacts_searchContactsNoNumber. + /// + /// In en, this message translates to: + /// **'Search Contacts...'** + String get contacts_searchContactsNoNumber; + /// No description provided for @contacts_searchContacts. /// /// In en, this message translates to: - /// **'Search contacts...'** - String get contacts_searchContacts; + /// **'Search {number}{str} Contacts...'** + String contacts_searchContacts(int number, String str); + + /// No description provided for @contacts_searchFavorites. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Favorites...'** + String contacts_searchFavorites(int number, String str); + + /// No description provided for @contacts_searchUsers. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Users...'** + String contacts_searchUsers(int number, String str); + + /// No description provided for @contacts_searchRepeaters. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Repeaters...'** + String contacts_searchRepeaters(int number, String str); + + /// No description provided for @contacts_searchRoomServers. + /// + /// In en, this message translates to: + /// **'Search {number}{str} Room servers...'** + String contacts_searchRoomServers(int number, String str); /// No description provided for @contacts_noUnreadContacts. /// diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index f9637aa7..da7ddc91 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -681,7 +681,35 @@ class AppLocalizationsBg extends AppLocalizations { 'Контактите ще се появят, когато устройствата рекламират.'; @override - String get contacts_searchContacts => 'Търсене на контакти...'; + String get contacts_unread => 'Непрочетено'; + + @override + String get contacts_searchContactsNoNumber => 'Търси контакти...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Търсене на контакти...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Търсене на $number$str любими...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Търсене на $number$str потребители...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Търсене на $number$str повтарящи се...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Търсене на $number$str сървъри в стаята...'; + } @override String get contacts_noUnreadContacts => 'Няма непрочетени контакти'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 15742813..228ffe7f 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -677,7 +677,35 @@ class AppLocalizationsDe extends AppLocalizations { 'Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.'; @override - String get contacts_searchContacts => 'Suche Kontakte...'; + String get contacts_unread => 'Ungelesen'; + + @override + String get contacts_searchContactsNoNumber => 'Kontakte suchen...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Suche Kontakte...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Suche $number$str Favoriten...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Suche $number$str Benutzer...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Suche $number$str Repeater...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Suche $number$str Raumserver...'; + } @override String get contacts_noUnreadContacts => 'Keine ungesehene Kontakte'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 039ad22f..70b3393e 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -670,7 +670,35 @@ class AppLocalizationsEn extends AppLocalizations { 'Contacts will appear when devices advertise'; @override - String get contacts_searchContacts => 'Search contacts...'; + String get contacts_unread => 'Unread'; + + @override + String get contacts_searchContactsNoNumber => 'Search Contacts...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Search $number$str Contacts...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Search $number$str Favorites...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Search $number$str Users...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Search $number$str Repeaters...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Search $number$str Room servers...'; + } @override String get contacts_noUnreadContacts => 'No unread contacts'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 8961b18a..876666b7 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -678,7 +678,35 @@ class AppLocalizationsEs extends AppLocalizations { 'Los contactos aparecerán cuando los dispositivos anuncien.'; @override - String get contacts_searchContacts => 'Buscar contactos...'; + String get contacts_unread => 'No leído'; + + @override + String get contacts_searchContactsNoNumber => 'Buscar contactos...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Buscar contactos...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Buscar $number$str Favoritos...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Buscar $number$str Usuarios...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Buscar $number$str Repetidores...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Buscar $number$str servidores de sala...'; + } @override String get contacts_noUnreadContacts => 'No contactos sin leer'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 3737f463..0c11eac7 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -681,7 +681,35 @@ class AppLocalizationsFr extends AppLocalizations { 'Les contacts apparaîtront lorsque les appareils font leur annonce.'; @override - String get contacts_searchContacts => 'Rechercher des contacts...'; + String get contacts_unread => 'Non lu'; + + @override + String get contacts_searchContactsNoNumber => 'Rechercher des contacts...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Rechercher des contacts...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Rechercher $number$str Favoris...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Rechercher $number$str utilisateurs...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Rechercher $number$str Répéteurs...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Rechercher $number$str serveurs de salle...'; + } @override String get contacts_noUnreadContacts => 'Aucun contact non lu'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index b64ec6d1..8a8fe71c 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -677,7 +677,35 @@ class AppLocalizationsIt extends AppLocalizations { 'I contatti appariranno quando i dispositivi pubblicizzano.'; @override - String get contacts_searchContacts => 'Cerca contatti...'; + String get contacts_unread => 'Non letti'; + + @override + String get contacts_searchContactsNoNumber => 'Cerca Contatti...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Cerca contatti...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Cerca $number$str Preferiti...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Cerca $number$str Utenti...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Cerca $number$str Ripetitori...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Cerca $number$str server Room...'; + } @override String get contacts_noUnreadContacts => 'Nessun contatto non letto'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 523cd6e1..8b4eee5e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -674,7 +674,35 @@ class AppLocalizationsNl extends AppLocalizations { 'Contacten verschijnen wanneer apparaten zich aanbieden.'; @override - String get contacts_searchContacts => 'Zoek contacten...'; + String get contacts_unread => 'Ongelezen'; + + @override + String get contacts_searchContactsNoNumber => 'Zoek contacten...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Zoek contacten...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Zoek $number$str favorieten...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Zoek $number$str gebruikers...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Zoek $number$str Repeaters...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Zoek $number$str Room servers...'; + } @override String get contacts_noUnreadContacts => 'Geen ongelezen contacten'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 16396001..cff6010e 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -678,7 +678,35 @@ class AppLocalizationsPl extends AppLocalizations { 'Kontakty będą wyświetlane, gdy urządzenia reklamują się.'; @override - String get contacts_searchContacts => 'Wyszukaj kontakty...'; + String get contacts_unread => 'Nieprzeczytane'; + + @override + String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Wyszukaj kontakty...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Wyszukaj $number$str ulubione...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Wyszukaj $number$str Użytkowników...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Wyszukaj $number$str powtórników...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Wyszukaj $number$str serwerów Room...'; + } @override String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index c59a4f5e..831d47aa 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -679,7 +679,35 @@ class AppLocalizationsPt extends AppLocalizations { 'Os contatos serão exibidos quando os dispositivos anunciarem.'; @override - String get contacts_searchContacts => 'Pesquisar contatos...'; + String get contacts_unread => 'Não lido'; + + @override + String get contacts_searchContactsNoNumber => 'Pesquisar Contatos...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Pesquisar contatos...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Pesquisar $number$str Favoritos...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Pesquisar $number$str Usuários...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Pesquisar $number$str Repetidores...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Pesquisar $number$str servidores de sala...'; + } @override String get contacts_noUnreadContacts => 'Sem contatos não lidos.'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 69d60447..5c73e3eb 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -678,7 +678,35 @@ class AppLocalizationsRu extends AppLocalizations { 'Контакты появятся, когда устройства начнут рассылать оповещения'; @override - String get contacts_searchContacts => 'Поиск контактов...'; + String get contacts_unread => 'Непрочитанное'; + + @override + String get contacts_searchContactsNoNumber => 'Поиск контактов...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Поиск контактов...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Поиск $number$str избранного...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Поиск $number$str пользователей...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Поиск $number$str ретрансляторов...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Поиск $number$str серверов комнат...'; + } @override String get contacts_noUnreadContacts => 'Нет непрочитанных контактов'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 17cbd7b9..b4f28fb9 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -671,7 +671,35 @@ class AppLocalizationsSk extends AppLocalizations { 'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.'; @override - String get contacts_searchContacts => 'Vyhľadávajte kontakty...'; + String get contacts_unread => 'Neprečítané'; + + @override + String get contacts_searchContactsNoNumber => 'Hľadať kontakty...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Vyhľadávajte kontakty...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Hľadať $number$str obľúbené...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Hľadať $number$str používateľov...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Hľadať $number$str opakovače...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Hľadaj $number$str serverov miestností...'; + } @override String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 6478b2bd..e015e453 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -672,7 +672,35 @@ class AppLocalizationsSl extends AppLocalizations { 'Stiki se bodo prikazali, ko se naprave oglasijo.'; @override - String get contacts_searchContacts => 'Iskanje stikov...'; + String get contacts_unread => 'Neprebrano'; + + @override + String get contacts_searchContactsNoNumber => 'Iskanje stikov...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Iskanje stikov...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Iskanje $number$str priljubljenih...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Išči $number$str uporabnikov...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Išči $number$str ponavljalnike...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Išči $number$str strežnikov sob...'; + } @override String get contacts_noUnreadContacts => 'Ne prebrani stiki.'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 54b998d9..3a25c3c3 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -667,7 +667,35 @@ class AppLocalizationsSv extends AppLocalizations { 'Kontakter kommer att visas när enheter annonserar.'; @override - String get contacts_searchContacts => 'Sök kontakter...'; + String get contacts_unread => 'Oläst'; + + @override + String get contacts_searchContactsNoNumber => 'Sök kontakter...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Sök kontakter...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Sök $number$str Favoriter...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Sök $number$str användare...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Sök $number$str upprepningsenheter...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Sök $number$str Room-servrar...'; + } @override String get contacts_noUnreadContacts => 'Inga oinlästa kontakter'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index e3564f1c..cd820cf7 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -676,7 +676,35 @@ class AppLocalizationsUk extends AppLocalizations { 'Контакти з\'являться, коли пристрої надішлють оголошення.'; @override - String get contacts_searchContacts => 'Пошук контактів...'; + String get contacts_unread => 'Непрочитане'; + + @override + String get contacts_searchContactsNoNumber => 'Пошук контактів...'; + + @override + String contacts_searchContacts(int number, String str) { + return 'Пошук контактів...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return 'Пошук $number$str улюблених...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return 'Пошук $number$str користувачів...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return 'Пошук $number$str ретрансляторів...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return 'Пошук $number$str серверів кімнат...'; + } @override String get contacts_noUnreadContacts => 'Немає непрочитаних контактів'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b55c376e..b63f714e 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -636,7 +636,35 @@ class AppLocalizationsZh extends AppLocalizations { String get contacts_contactsWillAppear => '当设备发送广播时,联系人将显示。'; @override - String get contacts_searchContacts => '搜索联系人...'; + String get contacts_unread => '未读'; + + @override + String get contacts_searchContactsNoNumber => '搜索联系人...'; + + @override + String contacts_searchContacts(int number, String str) { + return '搜索联系人...'; + } + + @override + String contacts_searchFavorites(int number, String str) { + return '搜索 $number$str 收藏...'; + } + + @override + String contacts_searchUsers(int number, String str) { + return '搜索 $number$str 位用户...'; + } + + @override + String contacts_searchRepeaters(int number, String str) { + return '搜索 $number$str 重复器...'; + } + + @override + String contacts_searchRoomServers(int number, String str) { + return '搜索 $number$str 房间服务器...'; + } @override String get contacts_noUnreadContacts => '没有未读内容'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index fee1f225..69ebecce 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Verwijderen uit favorieten", "listFilter_favorites": "Favorieten", - "listFilter_addToFavorites": "Toevoegen aan favorieten" + "listFilter_addToFavorites": "Toevoegen aan favorieten", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Ongelezen", + "contacts_searchRepeaters": "Zoek {number}{str} Repeaters...", + "contacts_searchContactsNoNumber": "Zoek contacten...", + "contacts_searchUsers": "Zoek {number}{str} gebruikers...", + "contacts_searchFavorites": "Zoek {number}{str} favorieten...", + "contacts_searchRoomServers": "Zoek {number}{str} Room servers..." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8096bb49..75e1d348 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Usuń z ulubionych", "listFilter_addToFavorites": "Dodaj do ulubionych", - "listFilter_favorites": "Ulubione" + "listFilter_favorites": "Ulubione", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Nieprzeczytane", + "contacts_searchContactsNoNumber": "Wyszukaj kontakty...", + "contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...", + "contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...", + "contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...", + "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników..." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 53f8f934..f6ada195 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1753,5 +1753,51 @@ }, "listFilter_addToFavorites": "Adicionar aos favoritos", "listFilter_removeFromFavorites": "Remover da lista de favoritos", - "listFilter_favorites": "Favoritos" + "listFilter_favorites": "Favoritos", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Pesquisar {number}{str} Repetidores...", + "contacts_searchFavorites": "Pesquisar {number}{str} Favoritos...", + "contacts_searchUsers": "Pesquisar {number}{str} Usuários...", + "contacts_searchContactsNoNumber": "Pesquisar Contatos...", + "contacts_unread": "Não lido", + "contacts_searchRoomServers": "Pesquisar {number}{str} servidores de sala..." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b4d7b597..9aef298e 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -993,5 +993,51 @@ }, "listFilter_addToFavorites": "Добавить в избранное", "listFilter_favorites": "Избранное", - "listFilter_removeFromFavorites": "Удалить из избранного" + "listFilter_removeFromFavorites": "Удалить из избранного", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRepeaters": "Поиск {number}{str} ретрансляторов...", + "contacts_searchContactsNoNumber": "Поиск контактов...", + "contacts_unread": "Непрочитанное", + "contacts_searchRoomServers": "Поиск {number}{str} серверов комнат...", + "contacts_searchFavorites": "Поиск {number}{str} избранного...", + "contacts_searchUsers": "Поиск {number}{str} пользователей..." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 23a0aeac..672b7d74 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Odstrániť z označení", "listFilter_addToFavorites": "Pridaj do obľúbených", - "listFilter_favorites": "Obľúbené" + "listFilter_favorites": "Obľúbené", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Hľadaj {number}{str} serverov miestností...", + "contacts_searchFavorites": "Hľadať {number}{str} obľúbené...", + "contacts_searchRepeaters": "Hľadať {number}{str} opakovače...", + "contacts_searchUsers": "Hľadať {number}{str} používateľov...", + "contacts_searchContactsNoNumber": "Hľadať kontakty...", + "contacts_unread": "Neprečítané" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 6b64f68e..09359a13 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1753,5 +1753,51 @@ }, "listFilter_favorites": "Priljubljene", "listFilter_removeFromFavorites": "Odstrani iz priljubljenih", - "listFilter_addToFavorites": "Dodaj v priljubljene" + "listFilter_addToFavorites": "Dodaj v priljubljene", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Neprebrano", + "contacts_searchFavorites": "Iskanje {number}{str} priljubljenih...", + "contacts_searchRoomServers": "Išči {number}{str} strežnikov sob...", + "contacts_searchContactsNoNumber": "Iskanje stikov...", + "contacts_searchRepeaters": "Išči {number}{str} ponavljalnike...", + "contacts_searchUsers": "Išči {number}{str} uporabnikov..." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 3b96df77..a923cc9c 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Ta bort från favoriter", "listFilter_addToFavorites": "Lägg till i favoriter", - "listFilter_favorites": "Favoriter" + "listFilter_favorites": "Favoriter", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_unread": "Oläst", + "contacts_searchContactsNoNumber": "Sök kontakter...", + "contacts_searchRepeaters": "Sök {number}{str} upprepningsenheter...", + "contacts_searchFavorites": "Sök {number}{str} Favoriter...", + "contacts_searchUsers": "Sök {number}{str} användare...", + "contacts_searchRoomServers": "Sök {number}{str} Room-servrar..." } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 78b4a901..c86f11c5 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1753,5 +1753,51 @@ }, "listFilter_removeFromFavorites": "Видалити зі списку улюблених", "listFilter_addToFavorites": "Додати до улюблених", - "listFilter_favorites": "Улюблені" + "listFilter_favorites": "Улюблені", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchRoomServers": "Пошук {number}{str} серверів кімнат...", + "contacts_searchUsers": "Пошук {number}{str} користувачів...", + "contacts_searchFavorites": "Пошук {number}{str} улюблених...", + "contacts_searchContactsNoNumber": "Пошук контактів...", + "contacts_searchRepeaters": "Пошук {number}{str} ретрансляторів...", + "contacts_unread": "Непрочитане" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6b8fc096..63c02a57 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1758,5 +1758,51 @@ }, "listFilter_favorites": "收藏", "listFilter_addToFavorites": "添加到收藏", - "listFilter_removeFromFavorites": "从收藏中移除" + "listFilter_removeFromFavorites": "从收藏中移除", + "@contacts_searchFavorites": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchUsers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRepeaters": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "@contacts_searchRoomServers": { + "placeholders": { + "number": { + "type": "int" + }, + "str": { + "type": "String" + } + } + }, + "contacts_searchUsers": "搜索 {number}{str} 位用户...", + "contacts_unread": "未读", + "contacts_searchRepeaters": "搜索 {number}{str} 重复器...", + "contacts_searchContactsNoNumber": "搜索联系人...", + "contacts_searchRoomServers": "搜索 {number}{str} 房间服务器...", + "contacts_searchFavorites": "搜索 {number}{str} 收藏..." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 6c683cc9..eeecfb9b 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -402,6 +402,41 @@ class _ContactsScreenState extends State ? const [] : _filterAndSortGroups(_groups, contacts); + String hintText = ""; + + switch (_typeFilter) { + case ContactTypeFilter.all: + hintText = context.l10n.contacts_searchContacts( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.users: + hintText = context.l10n.contacts_searchUsers( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.repeaters: + hintText = context.l10n.contacts_searchRepeaters( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.rooms: + hintText = context.l10n.contacts_searchRoomServers( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + case ContactTypeFilter.favorites: + hintText = context.l10n.contacts_searchFavorites( + filteredAndSorted.length, + _showUnreadOnly ? " ${context.l10n.contacts_unread}" : "", + ); + break; + } + return Column( children: [ Padding( @@ -409,7 +444,7 @@ class _ContactsScreenState extends State child: TextField( controller: _searchController, decoration: InputDecoration( - hintText: context.l10n.contacts_searchContacts, + hintText: hintText, prefixIcon: const Icon(Icons.search), suffixIcon: Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index b688a30f..2ec71a08 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -1301,7 +1301,8 @@ class _MapScreenState extends State { padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), child: TextField( decoration: InputDecoration( - hintText: context.l10n.contacts_searchContacts, + hintText: + context.l10n.contacts_searchContactsNoNumber, prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(12),