Merge pull request #264 from zjs81/dev-guessed-locations

Dev guessed locations
This commit is contained in:
zjs81
2026-03-06 15:19:03 -07:00
committed by GitHub
39 changed files with 598 additions and 34 deletions
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.", "contactsSettings_overwriteOldestSubtitle": "Когато списъкът с контакти е пълен, най-старият неключов контакт ще бъде заменен.",
"discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти", "discoveredContacts_deleteContactAll": "Изтриване на Всички Открити Контакти",
"discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?", "discoveredContacts_deleteContactAllContent": "Сигурни ли сте, че искате да изтриете всички открити контакти?",
"common_deleteAll": "Изтрий всичко" "common_deleteAll": "Изтрий всичко",
"map_guessedLocation": "Предполагано местоположение",
"map_showGuessedLocations": "Покажете местоположенията на предположените възли."
} }
+3 -1
View File
@@ -1854,5 +1854,7 @@
"contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.", "contactsSettings_overwriteOldestSubtitle": "Wenn die Kontaktliste voll ist, wird der älteste nicht favorisierte Kontakt ersetzt.",
"common_deleteAll": "Alles löschen", "common_deleteAll": "Alles löschen",
"discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?", "discoveredContacts_deleteContactAllContent": "Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?",
"discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen" "discoveredContacts_deleteContactAll": "Alle entdeckten Kontakte löschen",
"map_showGuessedLocations": "Zeige die vermuteten Knotenpositionen",
"map_guessedLocation": "Geschätzter Ort"
} }
+2
View File
@@ -775,6 +775,8 @@
"map_publicKeyPrefix": "Public key prefix", "map_publicKeyPrefix": "Public key prefix",
"map_markers": "Markers", "map_markers": "Markers",
"map_showSharedMarkers": "Show shared markers", "map_showSharedMarkers": "Show shared markers",
"map_showGuessedLocations": "Show guessed node locations",
"map_guessedLocation": "Guessed location",
"map_lastSeenTime": "Last Seen Time", "map_lastSeenTime": "Last Seen Time",
"map_sharedPin": "Shared pin", "map_sharedPin": "Shared pin",
"map_joinRoom": "Join Room", "map_joinRoom": "Join Room",
+3 -1
View File
@@ -1854,5 +1854,7 @@
"contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.", "contactsSettings_overwriteOldestSubtitle": "Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.",
"common_deleteAll": "Eliminar todo", "common_deleteAll": "Eliminar todo",
"discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos", "discoveredContacts_deleteContactAll": "Eliminar Todos los Contactos Descubiertos",
"discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!" "discoveredContacts_deleteContactAllContent": "¿Está seguro de que desea eliminar todos los contactos descubiertos!",
"map_guessedLocation": "Ubicación estimada",
"map_showGuessedLocations": "Mostrar las ubicaciones estimadas de los nodos."
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.", "contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
"common_deleteAll": "Supprimer tout", "common_deleteAll": "Supprimer tout",
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts", "discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?" "discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
"map_guessedLocation": "Lieu deviné"
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.", "contactsSettings_overwriteOldestSubtitle": "Quando l'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.",
"common_deleteAll": "Elimina tutto", "common_deleteAll": "Elimina tutto",
"discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?", "discoveredContacts_deleteContactAllContent": "Sei sicuro di voler eliminare tutti i contatti scoperti?",
"discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti" "discoveredContacts_deleteContactAll": "Eliminare tutti i contatti scoperti",
"map_guessedLocation": "Località indovinata",
"map_showGuessedLocations": "Mostra le posizioni stimate dei nodi"
} }
+12
View File
@@ -2638,6 +2638,18 @@ abstract class AppLocalizations {
/// **'Show shared markers'** /// **'Show shared markers'**
String get map_showSharedMarkers; String get map_showSharedMarkers;
/// No description provided for @map_showGuessedLocations.
///
/// In en, this message translates to:
/// **'Show guessed node locations'**
String get map_showGuessedLocations;
/// No description provided for @map_guessedLocation.
///
/// In en, this message translates to:
/// **'Guessed location'**
String get map_guessedLocation;
/// No description provided for @map_lastSeenTime. /// No description provided for @map_lastSeenTime.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
+7
View File
@@ -1443,6 +1443,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Покажи споделени маркери'; String get map_showSharedMarkers => 'Покажи споделени маркери';
@override
String get map_showGuessedLocations =>
'Покажете местоположенията на предположените възли.';
@override
String get map_guessedLocation => 'Предполагано местоположение';
@override @override
String get map_lastSeenTime => 'Последна видяна дата'; String get map_lastSeenTime => 'Последна видяна дата';
+7
View File
@@ -1442,6 +1442,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker'; String get map_showSharedMarkers => 'Zeige gemeinsam genutzte Marker';
@override
String get map_showGuessedLocations =>
'Zeige die vermuteten Knotenpositionen';
@override
String get map_guessedLocation => 'Geschätzter Ort';
@override @override
String get map_lastSeenTime => 'Letzte Sichtung'; String get map_lastSeenTime => 'Letzte Sichtung';
+6
View File
@@ -1421,6 +1421,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Show shared markers'; String get map_showSharedMarkers => 'Show shared markers';
@override
String get map_showGuessedLocations => 'Show guessed node locations';
@override
String get map_guessedLocation => 'Guessed location';
@override @override
String get map_lastSeenTime => 'Last Seen Time'; String get map_lastSeenTime => 'Last Seen Time';
+7
View File
@@ -1440,6 +1440,13 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Mostrar marcadores compartidos'; String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
@override
String get map_showGuessedLocations =>
'Mostrar las ubicaciones estimadas de los nodos.';
@override
String get map_guessedLocation => 'Ubicación estimada';
@override @override
String get map_lastSeenTime => 'Última vez que se vio'; String get map_lastSeenTime => 'Última vez que se vio';
+7
View File
@@ -1447,6 +1447,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Afficher les marqueurs partagés'; String get map_showSharedMarkers => 'Afficher les marqueurs partagés';
@override
String get map_showGuessedLocations =>
'Afficher les emplacements des nœuds estimés';
@override
String get map_guessedLocation => 'Lieu deviné';
@override @override
String get map_lastSeenTime => 'Dernière fois vu'; String get map_lastSeenTime => 'Dernière fois vu';
+6
View File
@@ -1439,6 +1439,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi'; String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
@override
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
@override
String get map_guessedLocation => 'Località indovinata';
@override @override
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione'; String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
+7
View File
@@ -1434,6 +1434,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Toon gedeelde markeringen'; String get map_showSharedMarkers => 'Toon gedeelde markeringen';
@override
String get map_showGuessedLocations =>
'Toon de voorspelde locaties van de knopen';
@override
String get map_guessedLocation => 'Geroerde locatie';
@override @override
String get map_lastSeenTime => 'Laatste Bekeken Tijd'; String get map_lastSeenTime => 'Laatste Bekeken Tijd';
+7
View File
@@ -1440,6 +1440,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Pokaż współdzielone znaki.'; String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
@override
String get map_showGuessedLocations =>
'Wyświetl lokalizacje zgadanych węzłów';
@override
String get map_guessedLocation => 'Wydana lokalizacja';
@override @override
String get map_lastSeenTime => 'Ostatni raz widiany'; String get map_lastSeenTime => 'Ostatni raz widiany';
+7
View File
@@ -1441,6 +1441,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados'; String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
@override
String get map_showGuessedLocations =>
'Mostrar as localizações dos nós estimados';
@override
String get map_guessedLocation => 'Localização estimada';
@override @override
String get map_lastSeenTime => 'Último Tempo de Visualização'; String get map_lastSeenTime => 'Último Tempo de Visualização';
+7
View File
@@ -1442,6 +1442,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Показывать общие метки'; String get map_showSharedMarkers => 'Показывать общие метки';
@override
String get map_showGuessedLocations =>
'Отобразить предполагаемые места расположения узлов';
@override
String get map_guessedLocation => 'Угаданное место';
@override @override
String get map_lastSeenTime => 'Время последнего появления'; String get map_lastSeenTime => 'Время последнего появления';
+7
View File
@@ -1435,6 +1435,13 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Zobraziť zdieľané značky'; String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
@override
String get map_showGuessedLocations =>
'Zobraziť umiestnenia odhadnutých uzlov';
@override
String get map_guessedLocation => 'Odhadnutá lokalita';
@override @override
String get map_lastSeenTime => 'Posledný čas sledovania'; String get map_lastSeenTime => 'Posledný čas sledovania';
+6
View File
@@ -1431,6 +1431,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Pokaži skupno označenja'; String get map_showSharedMarkers => 'Pokaži skupno označenja';
@override
String get map_showGuessedLocations => 'Pokaži lokacije domnevnih not.';
@override
String get map_guessedLocation => 'Predpostavljena lokacija';
@override @override
String get map_lastSeenTime => 'Datum zadnjega vpogleda'; String get map_lastSeenTime => 'Datum zadnjega vpogleda';
+7
View File
@@ -1427,6 +1427,13 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Visa delade markörer'; String get map_showSharedMarkers => 'Visa delade markörer';
@override
String get map_showGuessedLocations =>
'Visa upp de antagna nodernas placeringar';
@override
String get map_guessedLocation => 'Gissad plats';
@override @override
String get map_lastSeenTime => 'Senaste Visats Tid'; String get map_lastSeenTime => 'Senaste Visats Tid';
+7
View File
@@ -1441,6 +1441,13 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get map_showSharedMarkers => 'Показувати спільні маркери'; String get map_showSharedMarkers => 'Показувати спільні маркери';
@override
String get map_showGuessedLocations =>
'Показати місцезнаходження передбачених вузлів';
@override
String get map_guessedLocation => 'Визначено місцезнаходження';
@override @override
String get map_lastSeenTime => 'Час останньої активності'; String get map_lastSeenTime => 'Час останньої активності';
+6
View File
@@ -1363,6 +1363,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get map_showSharedMarkers => '显示共享标记'; String get map_showSharedMarkers => '显示共享标记';
@override
String get map_showGuessedLocations => '显示猜测的节点位置';
@override
String get map_guessedLocation => '猜测的位置';
@override @override
String get map_lastSeenTime => '最后在线时间'; String get map_lastSeenTime => '最后在线时间';
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.", "contactsSettings_overwriteOldestSubtitle": "Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.",
"common_deleteAll": "Alles verwijderen", "common_deleteAll": "Alles verwijderen",
"discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten", "discoveredContacts_deleteContactAll": "Verwijder alle ontdekte contacten",
"discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?" "discoveredContacts_deleteContactAllContent": "Weet u zeker dat u alle ontdekte contacten wilt verwijderen?",
"map_guessedLocation": "Geroerde locatie",
"map_showGuessedLocations": "Toon de voorspelde locaties van de knopen"
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.", "contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.",
"common_deleteAll": "Usuń wszystko", "common_deleteAll": "Usuń wszystko",
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?", "discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty" "discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
"map_guessedLocation": "Wydana lokalizacja",
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów"
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.", "contactsSettings_overwriteOldestSubtitle": "Quando a lista de contatos estiver cheia, o contato mais antigo não favoritado será substituído.",
"common_deleteAll": "Excluir Tudo", "common_deleteAll": "Excluir Tudo",
"discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos", "discoveredContacts_deleteContactAll": "Excluir Todos os Contatos Descobertos",
"discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?" "discoveredContacts_deleteContactAllContent": "Tem certeza de que deseja excluir todos os contatos descobertos?",
"map_guessedLocation": "Localização estimada",
"map_showGuessedLocations": "Mostrar as localizações dos nós estimados"
} }
+3 -1
View File
@@ -1066,5 +1066,7 @@
"contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.", "contactsSettings_overwriteOldestSubtitle": "Когда список контактов заполнен, будет заменен самый старый контакт, который не находится в избранном.",
"common_deleteAll": "Удалить все", "common_deleteAll": "Удалить все",
"discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?", "discoveredContacts_deleteContactAllContent": "Вы уверены, что хотите удалить все обнаруженные контакты?",
"discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты" "discoveredContacts_deleteContactAll": "Удалить Все Обнаруженные Контакты",
"map_guessedLocation": "Угаданное место",
"map_showGuessedLocations": "Отобразить предполагаемые места расположения узлов"
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.", "contactsSettings_overwriteOldestSubtitle": "Keď je zoznam kontaktov plný, bude nahradený najstarší neoznačený kontakt.",
"discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty", "discoveredContacts_deleteContactAll": "Zmazať všetky objavené kontakty",
"common_deleteAll": "Zmazať všetko", "common_deleteAll": "Zmazať všetko",
"discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?" "discoveredContacts_deleteContactAllContent": "Ste si istí, že chcete zmazať všetky objavené kontakty?",
"map_showGuessedLocations": "Zobraziť umiestnenia odhadnutých uzlov",
"map_guessedLocation": "Odhadnutá lokalita"
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.", "contactsSettings_overwriteOldestSubtitle": "Ko je seznam stikov poln, bo najstarejši nestarševski stik zamenjan.",
"common_deleteAll": "Izbriši vse", "common_deleteAll": "Izbriši vse",
"discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?", "discoveredContacts_deleteContactAllContent": "Ste prepričani, da želite izbrisati vse odkrite kontakte?",
"discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte" "discoveredContacts_deleteContactAll": "Izbriši vse odkrite kontakte",
"map_guessedLocation": "Predpostavljena lokacija",
"map_showGuessedLocations": "Pokaži lokacije domnevnih not."
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.", "contactsSettings_overwriteOldestSubtitle": "När kontaktlistan är full ersätts den äldsta icke-favoriterade kontakten.",
"common_deleteAll": "Ta bort alla", "common_deleteAll": "Ta bort alla",
"discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?", "discoveredContacts_deleteContactAllContent": "Är du säker på att du vill ta bort alla upptäckta kontakter?",
"discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter" "discoveredContacts_deleteContactAll": "Ta bort alla upptäckta kontakter",
"map_guessedLocation": "Gissad plats",
"map_showGuessedLocations": "Visa upp de antagna nodernas placeringar"
} }
+3 -1
View File
@@ -1826,5 +1826,7 @@
"contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.", "contactsSettings_overwriteOldestSubtitle": "Коли список контактів заповнений, найстарший контакт без позначки улюбленого буде замінений.",
"common_deleteAll": "Видалити все", "common_deleteAll": "Видалити все",
"discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти", "discoveredContacts_deleteContactAll": "Видалити всі виявлені контакти",
"discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?" "discoveredContacts_deleteContactAllContent": "Ви впевнені, що хочете видалити всі виявлені контакти?",
"map_showGuessedLocations": "Показати місцезнаходження передбачених вузлів",
"map_guessedLocation": "Визначено місцезнаходження"
} }
+3 -1
View File
@@ -1831,5 +1831,7 @@
"contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。", "contactsSettings_overwriteOldestSubtitle": "当联系人列表已满时,将替换最老的非收藏联系人。",
"common_deleteAll": "删除全部", "common_deleteAll": "删除全部",
"discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?", "discoveredContacts_deleteContactAllContent": "您确定要删除所有发现的联系人吗?",
"discoveredContacts_deleteContactAll": "删除所有发现的联系人" "discoveredContacts_deleteContactAll": "删除所有发现的联系人",
"map_showGuessedLocations": "显示猜测的节点位置",
"map_guessedLocation": "猜测的位置"
} }
+8
View File
@@ -22,6 +22,7 @@ class AppSettings {
final bool mapKeyPrefixEnabled; final bool mapKeyPrefixEnabled;
final String mapKeyPrefix; final String mapKeyPrefix;
final bool mapShowMarkers; final bool mapShowMarkers;
final bool mapShowGuessedLocations;
final bool enableMessageTracing; final bool enableMessageTracing;
final Map<String, double>? mapCacheBounds; final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom; final int mapCacheMinZoom;
@@ -48,6 +49,7 @@ class AppSettings {
this.mapKeyPrefixEnabled = false, this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '', this.mapKeyPrefix = '',
this.mapShowMarkers = true, this.mapShowMarkers = true,
this.mapShowGuessedLocations = true,
this.enableMessageTracing = false, this.enableMessageTracing = false,
this.mapCacheBounds, this.mapCacheBounds,
this.mapCacheMinZoom = 10, this.mapCacheMinZoom = 10,
@@ -78,6 +80,7 @@ class AppSettings {
'map_key_prefix_enabled': mapKeyPrefixEnabled, 'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix, 'map_key_prefix': mapKeyPrefix,
'map_show_markers': mapShowMarkers, 'map_show_markers': mapShowMarkers,
'map_show_guessed_locations': mapShowGuessedLocations,
'enable_message_tracing': enableMessageTracing, 'enable_message_tracing': enableMessageTracing,
'map_cache_bounds': mapCacheBounds, 'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_min_zoom': mapCacheMinZoom,
@@ -115,6 +118,8 @@ class AppSettings {
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false, mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
mapKeyPrefix: json['map_key_prefix'] as String? ?? '', mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
mapShowMarkers: json['map_show_markers'] as bool? ?? true, mapShowMarkers: json['map_show_markers'] as bool? ?? true,
mapShowGuessedLocations:
json['map_show_guessed_locations'] as bool? ?? true,
enableMessageTracing: json['enable_message_tracing'] as bool? ?? false, enableMessageTracing: json['enable_message_tracing'] as bool? ?? false,
mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map( mapCacheBounds: (json['map_cache_bounds'] as Map?)?.map(
(key, value) => MapEntry(key.toString(), (value as num).toDouble()), (key, value) => MapEntry(key.toString(), (value as num).toDouble()),
@@ -159,6 +164,7 @@ class AppSettings {
bool? mapKeyPrefixEnabled, bool? mapKeyPrefixEnabled,
String? mapKeyPrefix, String? mapKeyPrefix,
bool? mapShowMarkers, bool? mapShowMarkers,
bool? mapShowGuessedLocations,
bool? enableMessageTracing, bool? enableMessageTracing,
Object? mapCacheBounds = _unset, Object? mapCacheBounds = _unset,
int? mapCacheMinZoom, int? mapCacheMinZoom,
@@ -185,6 +191,8 @@ class AppSettings {
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled, mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix, mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers, mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
mapShowGuessedLocations:
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing, enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
mapCacheBounds: mapCacheBounds == _unset mapCacheBounds: mapCacheBounds == _unset
? this.mapCacheBounds ? this.mapCacheBounds
+1
View File
@@ -818,6 +818,7 @@ class _ChatScreenState extends State<ChatScreen> {
title: context.l10n.contacts_repeaterPathTrace, title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes), path: Uint8List.fromList(pathBytes),
flipPathRound: true, flipPathRound: true,
targetContact: widget.contact,
), ),
), ),
), ),
+1
View File
@@ -1131,6 +1131,7 @@ class _ContactsScreenState extends State<ContactsScreen>
contact.name, contact.name,
), ),
path: contact.traceRouteBytes ?? Uint8List(0), path: contact.traceRouteBytes ?? Uint8List(0),
targetContact: contact,
), ),
), ),
); );
+284 -8
View File
@@ -15,6 +15,7 @@ import '../models/app_settings.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import '../services/app_settings_service.dart'; import '../services/app_settings_service.dart';
import '../services/path_history_service.dart';
import '../services/map_marker_service.dart'; import '../services/map_marker_service.dart';
import '../services/map_tile_cache_service.dart'; import '../services/map_tile_cache_service.dart';
import '../utils/contact_search.dart'; import '../utils/contact_search.dart';
@@ -64,6 +65,8 @@ class _MapScreenState extends State<MapScreen> {
final List<Polyline> _polylines = []; final List<Polyline> _polylines = [];
bool _legendExpanded = false; bool _legendExpanded = false;
bool _showNodeLabels = true; bool _showNodeLabels = true;
List<_GuessedLocation> _cachedGuessedLocations = [];
String _guessedLocationsCacheKey = '';
@override @override
void initState() { void initState() {
@@ -119,8 +122,8 @@ class _MapScreenState extends State<MapScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer2<MeshCoreConnector, AppSettingsService>( return Consumer3<MeshCoreConnector, AppSettingsService, PathHistoryService>(
builder: (context, connector, settingsService, child) { builder: (context, connector, settingsService, pathHistory, child) {
final tileCache = context.read<MapTileCacheService>(); final tileCache = context.read<MapTileCacheService>();
final settings = settingsService.settings; final settings = settingsService.settings;
final contacts = connector.contacts; final contacts = connector.contacts;
@@ -160,6 +163,40 @@ class _MapScreenState extends State<MapScreen> {
.where((c) => c.hasLocation) .where((c) => c.hasLocation)
.toList(); .toList();
// All contacts with a known location used as anchors regardless of
// time/key-prefix filters so that repeaters are always available.
final allContactsWithLocation = contacts
.where((c) => c.hasLocation)
.toList();
// Compute guessed locations with caching
final maxRangeKm = _estimateLoRaRangeKm(connector);
final filteredKeys = filteredByKeyPrefix
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
.join(',');
final anchorKeys = allContactsWithLocation
.map(
(c) =>
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
)
.join(',');
final cacheKey =
'$filteredKeys|$anchorKeys|${pathHistory.version}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
if (cacheKey != _guessedLocationsCacheKey) {
_guessedLocationsCacheKey = cacheKey;
_cachedGuessedLocations = settings.mapShowGuessedLocations
? _computeGuessedLocations(
filteredByKeyPrefix,
allContactsWithLocation,
pathHistory,
maxRangeKm,
)
: [];
}
final guessedLocations = settings.mapShowGuessedLocations
? _cachedGuessedLocations
: <_GuessedLocation>[];
_polylines.clear(); _polylines.clear();
_polylines.addAll( _polylines.addAll(
_points.length > 1 _points.length > 1
@@ -430,6 +467,8 @@ class _MapScreenState extends State<MapScreen> {
size: 34, size: 34,
), ),
), ),
if (!_isBuildingPathTrace)
...guessedLocations.map(_buildGuessedMarker),
..._buildMarkers( ..._buildMarkers(
contactsWithLocation, contactsWithLocation,
settings, settings,
@@ -489,6 +528,7 @@ class _MapScreenState extends State<MapScreen> {
contactsWithLocation, contactsWithLocation,
settings, settings,
sharedMarkers.length, sharedMarkers.length,
guessedLocations.length,
), ),
if (_isBuildingPathTrace) _buildPathTraceOverlay(), if (_isBuildingPathTrace) _buildPathTraceOverlay(),
], ],
@@ -512,6 +552,200 @@ class _MapScreenState extends State<MapScreen> {
); );
} }
List<_GuessedLocation> _computeGuessedLocations(
List<Contact> allContacts,
List<Contact> withLocation,
PathHistoryService pathHistory,
double? maxRangeKm,
) {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = <int, Contact?>{};
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
repeaterByHash[c.publicKey[0]] =
null; // collision: can't disambiguate
} else {
repeaterByHash[c.publicKey[0]] = c;
}
}
}
final result = <_GuessedLocation>[];
for (final contact in allContacts) {
if (contact.hasLocation) continue;
final anchorSet = <LatLng>{};
// Collect the contact-side (last-hop) repeater from every known path.
// path = [device-side hop, ..., contact-side hop]
// Only path.last is actually within radio range of the contact using
// earlier bytes would anchor against our own side of the network.
final pathSets = <List<int>>[
contact.path.toList(),
...pathHistory
.getRecentPaths(contact.publicKeyHex)
.map((r) => r.pathBytes),
];
final lastHopBytes = <int>{};
for (final pathBytes in pathSets) {
if (pathBytes.isEmpty) continue;
final lastHop = pathBytes.last;
lastHopBytes.add(lastHop);
final r = repeaterByHash[lastHop];
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
}
// Fallback: for any last-hop byte with no GPS repeater, average the
// positions of contacts with known GPS that share the same last hop.
// Those contacts are all adjacent to the same unknown repeater, so their
// centroid is a reasonable proxy for its location.
for (final byte in lastHopBytes) {
if (repeaterByHash.containsKey(byte)) continue;
for (final c in withLocation) {
if (c.path.isNotEmpty && c.path.last == byte) {
anchorSet.add(LatLng(c.latitude!, c.longitude!));
}
}
}
// Filter anchors that are geometrically inconsistent with radio range.
// Two anchors more than 2 * maxRange apart cannot both be in direct radio
// range of the same node, so isolated outliers are removed.
final anchors = maxRangeKm != null && anchorSet.length > 1
? _filterConsistentAnchors(anchorSet.toList(), maxRangeKm)
: anchorSet.toList();
if (anchors.isEmpty) continue;
final LatLng position;
if (anchors.length == 1) {
// Offset single-anchor guesses so they don't overlap the repeater marker.
// Use the contact's public key byte as a deterministic angle seed.
const offsetDeg = 0.003; // ~330 m at the equator
final angle = (contact.publicKey[1] / 255.0) * 2 * pi;
position = LatLng(
anchors[0].latitude + offsetDeg * cos(angle),
anchors[0].longitude + offsetDeg * sin(angle),
);
} else {
double lat = 0, lon = 0;
for (final a in anchors) {
lat += a.latitude;
lon += a.longitude;
}
position = LatLng(lat / anchors.length, lon / anchors.length);
}
result.add(
_GuessedLocation(
contact: contact,
position: position,
highConfidence: anchors.length >= 2,
),
);
}
return result;
}
/// Estimates the free-space maximum LoRa range in km from the connected
/// device's current radio parameters. Returns null if parameters are unknown.
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
final freqHz = connector.currentFreqHz;
final bwHz = connector.currentBwHz;
final sf = connector.currentSf;
final txPower = connector.currentTxPower;
if (freqHz == null || bwHz == null || sf == null || txPower == null) {
return null;
}
// LoRa receiver sensitivity = thermal noise + NF + required demod SNR
const noiseFigureDb = 6.0;
final thermalNoiseDbm = -174.0 + 10 * log(bwHz.toDouble()) / ln10;
final sensitivityDbm =
thermalNoiseDbm + noiseFigureDb + _sfToRequiredSnrDb(sf);
// FSPL at max range equals link budget:
// FSPL = 20*log10(d_m) + 20*log10(f_hz) - 147.55
final linkBudgetDb = txPower.toDouble() - sensitivityDbm;
final exponent =
(linkBudgetDb + 147.55 - 20 * log(freqHz.toDouble()) / ln10) / 20;
return pow(10, exponent) / 1000;
}
double _sfToRequiredSnrDb(int sf) {
switch (sf) {
case 5:
return -2.5;
case 6:
return -5.0;
case 7:
return -7.5;
case 8:
return -10.0;
case 9:
return -12.5;
case 10:
return -15.0;
case 11:
return -17.5;
case 12:
return -20.0;
default:
return -10.0;
}
}
/// Removes anchors that have no neighbour within 2 * maxRangeKm.
/// A node cannot be simultaneously in radio range of two points farther apart
/// than twice the expected maximum range.
List<LatLng> _filterConsistentAnchors(
List<LatLng> anchors,
double maxRangeKm,
) {
const distance = Distance();
final maxDistM = maxRangeKm * 2000;
return anchors
.where((a) => anchors.any((b) => b != a && distance(a, b) <= maxDistM))
.toList();
}
Marker _buildGuessedMarker(_GuessedLocation guess) {
final color = _getNodeColor(guess.contact.type);
return Marker(
point: guess.position,
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: color.withValues(alpha: guess.highConfidence ? 0.55 : 0.30),
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),
),
],
),
child: const Icon(
Icons.not_listed_location,
color: Colors.white,
size: 20,
),
),
),
);
}
List<Marker> _buildMarkers( List<Marker> _buildMarkers(
List<Contact> contacts, List<Contact> contacts,
settings, { settings, {
@@ -657,6 +891,7 @@ class _MapScreenState extends State<MapScreen> {
List<Contact> contactsWithLocation, List<Contact> contactsWithLocation,
settings, settings,
int markerCount, int markerCount,
int guessedCount,
) { ) {
int nodeCount = 0; int nodeCount = 0;
for (final contact in contactsWithLocation) { for (final contact in contactsWithLocation) {
@@ -696,7 +931,12 @@ class _MapScreenState extends State<MapScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
context.l10n.map_nodesCount(nodeCount), context.l10n.map_nodesCount(
nodeCount +
(settings.mapShowGuessedLocations
? guessedCount
: 0),
),
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
@@ -764,6 +1004,12 @@ class _MapScreenState extends State<MapScreen> {
context.l10n.map_pinPublic, context.l10n.map_pinPublic,
Colors.orange, Colors.orange,
), ),
if (settings.mapShowGuessedLocations && guessedCount > 0)
_buildLegendItem(
Icons.not_listed_location,
context.l10n.map_guessedLocation,
Colors.grey,
),
], ],
), ),
), ),
@@ -952,7 +1198,11 @@ class _MapScreenState extends State<MapScreen> {
); );
} }
void _showNodeInfo(BuildContext context, Contact contact) { void _showNodeInfo(
BuildContext context,
Contact contact, {
LatLng? guessedPosition,
}) {
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
@@ -972,10 +1222,16 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
_buildInfoRow('Type', contact.typeLabel), _buildInfoRow('Type', contact.typeLabel),
_buildInfoRow('Path', contact.pathLabel), _buildInfoRow('Path', contact.pathLabel),
_buildInfoRow( if (contact.hasLocation)
'Location', _buildInfoRow(
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}', 'Location',
), '${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
)
else if (guessedPosition != null)
_buildInfoRow(
'Est. Location',
'~${guessedPosition.latitude.toStringAsFixed(6)}, ${guessedPosition.longitude.toStringAsFixed(6)}',
),
_buildInfoRow( _buildInfoRow(
context.l10n.map_lastSeen, context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen), _formatLastSeen(contact.lastSeen),
@@ -1481,6 +1737,14 @@ class _MapScreenState extends State<MapScreen> {
}, },
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
), ),
CheckboxListTile(
title: Text(context.l10n.map_showGuessedLocations),
value: settings.mapShowGuessedLocations,
onChanged: (value) {
service.setMapShowGuessedLocations(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
context.l10n.map_keyPrefix, context.l10n.map_keyPrefix,
@@ -1744,6 +2008,18 @@ class _MapScreenState extends State<MapScreen> {
} }
} }
class _GuessedLocation {
final Contact contact;
final LatLng position;
final bool highConfidence;
_GuessedLocation({
required this.contact,
required this.position,
required this.highConfidence,
});
}
class _MarkerPayload { class _MarkerPayload {
final LatLng position; final LatLng position;
final String label; final String label;
+135 -12
View File
@@ -54,6 +54,7 @@ class PathTraceMapScreen extends StatefulWidget {
final int? repeaterId; final int? repeaterId;
final bool flipPathRound; final bool flipPathRound;
final bool reversePathRound; final bool reversePathRound;
final Contact? targetContact;
const PathTraceMapScreen({ const PathTraceMapScreen({
super.key, super.key,
@@ -62,6 +63,7 @@ class PathTraceMapScreen extends StatefulWidget {
this.repeaterId, this.repeaterId,
this.flipPathRound = false, this.flipPathRound = false,
this.reversePathRound = false, this.reversePathRound = false,
this.targetContact,
}); });
@override @override
@@ -78,6 +80,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
bool _failed2Loaded = false; bool _failed2Loaded = false;
bool _hasData = false; bool _hasData = false;
PathTraceData? _traceData; PathTraceData? _traceData;
// Inferred positions for hops that have no GPS location, keyed by hop byte.
Map<int, LatLng> _inferredHopPositions = {};
// Endpoint position for the target contact (GPS or guessed).
LatLng? _targetContactPosition;
bool _targetContactIsGuessed = false;
List<LatLng> _points = <LatLng>[]; List<LatLng> _points = <LatLng>[];
List<Polyline> _polylines = []; List<Polyline> _polylines = [];
LatLng? _initialCenter = LatLng(0, 0); LatLng? _initialCenter = LatLng(0, 0);
@@ -242,25 +249,91 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
} }
}); });
// For hops with no GPS contact, infer position from other contacts
// with known GPS that share the same last-hop byte.
final Map<int, LatLng> inferredPositions = {};
for (final hop in pathData) {
final contact = pathContacts[hop];
if (contact != null && contact.hasLocation) continue;
final peers = connector.contacts
.where(
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
)
.toList();
if (peers.isNotEmpty) {
final lat =
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
peers.length;
final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length;
inferredPositions[hop] = LatLng(lat, lon);
}
}
setState(() { setState(() {
_isLoading = false; _isLoading = false;
_hasData = true; _hasData = true;
_inferredHopPositions = inferredPositions;
_traceData = PathTraceData( _traceData = PathTraceData(
pathData: pathData, pathData: pathData,
snrData: snrData, snrData: snrData,
pathContacts: pathContacts, pathContacts: pathContacts,
); );
// Compute endpoint position for the target contact.
LatLng? targetPos;
bool targetGuessed = false;
final target = widget.targetContact;
if (target != null) {
if (target.hasLocation) {
targetPos = LatLng(target.latitude!, target.longitude!);
} else if (pathData.isNotEmpty) {
// Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathRound), the target-side hop sits
// in the middle of the symmetric sequence; .last is the local side.
final lastHop = (widget.flipPathRound && pathData.length > 1)
? pathData[(pathData.length - 1) ~/ 2]
: pathData.last;
final peers = connector.contacts
.where(
(c) =>
c.hasLocation &&
c.path.isNotEmpty &&
c.path.last == lastHop,
)
.toList();
if (peers.isNotEmpty) {
final lat =
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
peers.length;
final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length;
const offsetDeg = 0.003;
final angle = (target.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng(
lat + offsetDeg * cos(angle),
lon + offsetDeg * sin(angle),
);
targetGuessed = true;
}
}
}
_targetContactPosition = targetPos;
_targetContactIsGuessed = targetGuessed;
_points = <LatLng>[]; _points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
for (final hop in _traceData!.pathData) { for (final hop in _traceData!.pathData) {
final contact = _traceData!.pathContacts[hop]; final contact = _traceData!.pathContacts[hop];
if (contact != null && if (contact != null && contact.hasLocation) {
contact.hasLocation &&
contact.latitude != null &&
contact.longitude != null) {
_points.add(LatLng(contact.latitude!, contact.longitude!)); _points.add(LatLng(contact.latitude!, contact.longitude!));
} else {
final inferred = inferredPositions[hop];
if (inferred != null) _points.add(inferred);
} }
} }
if (targetPos != null) _points.add(targetPos);
_polylines = _points.length > 1 _polylines = _points.length > 1
? [ ? [
Polyline( Polyline(
@@ -382,8 +455,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
final markers = <Marker>[]; final markers = <Marker>[];
for (final hop in pathData) { for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop]; final contact = _traceData!.pathContacts[hop];
if (contact == null || !contact.hasLocation) continue; final inferred = _inferredHopPositions[hop];
final point = LatLng(contact.latitude!, contact.longitude!); final hasGps = contact != null && contact.hasLocation;
if (!hasGps && inferred == null) continue;
final point = hasGps
? LatLng(contact.latitude!, contact.longitude!)
: inferred!;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
markers.add( markers.add(
Marker( Marker(
point: point, point: point,
@@ -392,7 +470,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
child: Container( child: Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.green, color: hasGps
? Colors.green
: Colors.orange.withValues(alpha: 0.75),
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 2),
boxShadow: [ boxShadow: [
@@ -405,10 +485,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
contact.publicKey hasGps ? label : '~$label',
.sublist(0, 1)
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
.join(),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -419,7 +496,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
), ),
); );
if (showLabels) { if (showLabels) {
markers.add(_buildNodeLabelMarker(point: point, label: contact.name)); markers.add(
_buildNodeLabelMarker(
point: point,
label: contact?.name ?? '~$label',
),
);
} }
} }
@@ -468,6 +550,47 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
} }
} }
// Add target contact endpoint marker.
final targetPos = _targetContactPosition;
if (targetPos != null) {
final isGuessed = _targetContactIsGuessed;
final targetName = widget.targetContact?.name ?? '?';
markers.add(
Marker(
point: targetPos,
width: 35,
height: 35,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isGuessed
? Colors.purple.withValues(alpha: 0.55)
: Colors.red,
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: const Icon(Icons.person, color: Colors.white, size: 18),
),
),
);
if (showLabels) {
markers.add(
_buildNodeLabelMarker(
point: targetPos,
label: isGuessed ? '~$targetName' : targetName,
),
);
}
}
return markers; return markers;
} }
+4
View File
@@ -80,6 +80,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowMarkers: value)); await updateSettings(_settings.copyWith(mapShowMarkers: value));
} }
Future<void> setMapShowGuessedLocations(bool value) async {
await updateSettings(_settings.copyWith(mapShowGuessedLocations: value));
}
Future<void> setEnableMessageTracing(bool value) async { Future<void> setEnableMessageTracing(bool value) async {
await updateSettings(_settings.copyWith(enableMessageTracing: value)); await updateSettings(_settings.copyWith(enableMessageTracing: value));
} }
+7
View File
@@ -15,6 +15,9 @@ class PathHistoryService extends ChangeNotifier {
final List<String> _cacheAccessOrder = []; final List<String> _cacheAccessOrder = [];
static const int _maxHistoryEntries = 100; static const int _maxHistoryEntries = 100;
int _version = 0;
int get version => _version;
static const int _autoRotationTopCount = 3; static const int _autoRotationTopCount = 3;
PathHistoryService(this._storage); PathHistoryService(this._storage);
@@ -185,6 +188,7 @@ class PathHistoryService extends ChangeNotifier {
) { ) {
var history = _cache[contactPubKeyHex]; var history = _cache[contactPubKeyHex];
if (history == null) return; if (history == null) return;
_version++;
final existing = _findPathRecord(contactPubKeyHex, pathBytes); final existing = _findPathRecord(contactPubKeyHex, pathBytes);
if (existing != null) { if (existing != null) {
@@ -241,6 +245,7 @@ class PathHistoryService extends ChangeNotifier {
_cache[contactPubKeyHex] = loaded; _cache[contactPubKeyHex] = loaded;
_trackAccess(contactPubKeyHex); _trackAccess(contactPubKeyHex);
_evictIfNeeded(); _evictIfNeeded();
_version++;
notifyListeners(); notifyListeners();
} }
}); });
@@ -276,6 +281,7 @@ class PathHistoryService extends ChangeNotifier {
_autoRotationIndex.remove(contactPubKeyHex); _autoRotationIndex.remove(contactPubKeyHex);
_floodStats.remove(contactPubKeyHex); _floodStats.remove(contactPubKeyHex);
await _storage.clearPathHistory(contactPubKeyHex); await _storage.clearPathHistory(contactPubKeyHex);
_version++;
notifyListeners(); notifyListeners();
} }
@@ -295,6 +301,7 @@ class PathHistoryService extends ChangeNotifier {
); );
await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!); await _storage.savePathHistory(contactPubKeyHex, _cache[contactPubKeyHex]!);
_version++;
notifyListeners(); notifyListeners();
} }
+1
View File
@@ -79,6 +79,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
title: context.l10n.contacts_repeaterPathTrace, title: context.l10n.contacts_repeaterPathTrace,
path: Uint8List.fromList(pathBytes), path: Uint8List.fromList(pathBytes),
flipPathRound: true, flipPathRound: true,
targetContact: widget.contact,
), ),
), ),
), ),