Add show overlaps in public keys of repeaters functionality and localization support

This commit is contained in:
Winston Lowe 2026-02-22 16:00:51 -08:00
parent cac0cc15eb
commit c81c3efe7c
37 changed files with 242 additions and 77 deletions

View file

@ -5196,6 +5196,10 @@ class MeshCoreConnector extends ChangeNotifier {
markChannelRead(channelIndex);
notifyListeners();
}
void deleteAllPaths() {
_pathHistoryService?.clearAllHistories();
}
}
const int _phRouteMask = 0x03;

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен"
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча"
}

View file

@ -1969,5 +1969,6 @@
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}"
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
"map_showOverlaps": "Überlappungen der Repeater-Taste"
}

View file

@ -878,6 +878,7 @@
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Other Nodes",
"map_showOverlaps": "Repeater Key Overlaps",
"map_keyPrefix": "Key Prefix",
"map_filterByKeyPrefix": "Filter by key prefix",
"map_publicKeyPrefix": "Public key prefix",

View file

@ -1918,7 +1918,6 @@
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
"map_setAsMyLocation": "Establecer mi ubicación",
<<<<<<< HEAD
"@path_routeWeight": {
"placeholders": {
"weight": {
@ -1929,8 +1928,6 @@
}
}
},
=======
>>>>>>> c9e3ceb (feat: Enhance privacy settings and telemetry (#308))
"settings_privacySubtitle": "Controlar qué información se comparte.",
"settings_allowByContact": "Permitir por banderas de contacto",
"settings_denyAll": "Denegar todo",
@ -1960,7 +1957,6 @@
}
}
},
<<<<<<< HEAD
"appSettings_initialRouteWeight": "Peso inicial de la ruta",
"appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
"appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
@ -1974,9 +1970,4 @@
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}"
}
=======
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}"
}
>>>>>>> c9e3ceb (feat: Enhance privacy settings and telemetry (#308))
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Multi-ACKs : {value}",
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour"
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
"map_showOverlaps": "Chevauchement de la touche répétitive"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore"
}

View file

@ -3052,6 +3052,12 @@ abstract class AppLocalizations {
/// **'Other Nodes'**
String get map_otherNodes;
/// No description provided for @map_showOverlaps.
///
/// In en, this message translates to:
/// **'Repeater Key Overlaps'**
String get map_showOverlaps;
/// No description provided for @map_keyPrefix.
///
/// In en, this message translates to:

View file

@ -1689,6 +1689,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_otherNodes => 'Други възли';
@override
String get map_showOverlaps => 'Покриване на ключа на повтаряча';
@override
String get map_keyPrefix => 'Префикс на ключа';

View file

@ -1686,6 +1686,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Knoten';
@override
String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
@override
String get map_keyPrefix => 'Schlüsselpräfix';

View file

@ -1656,6 +1656,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_otherNodes => 'Other Nodes';
@override
String get map_showOverlaps => 'Repeater Key Overlaps';
@override
String get map_keyPrefix => 'Key Prefix';

View file

@ -1685,6 +1685,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_otherNodes => 'Otros Nodos';
@override
String get map_showOverlaps => 'Superposiciones de tecla repetidora';
@override
String get map_keyPrefix => 'Prefijo de clave';

View file

@ -1695,6 +1695,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_otherNodes => 'Autres nœuds';
@override
String get map_showOverlaps => 'Chevauchement de la touche répétitive';
@override
String get map_keyPrefix => 'Préfixe clé';

View file

@ -1687,6 +1687,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_otherNodes => 'Altri Nodi';
@override
String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
@override
String get map_keyPrefix => 'Prefisso Chiave';

View file

@ -1674,6 +1674,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Nodes';
@override
String get map_showOverlaps => 'Herhalingssleutel overlapt';
@override
String get map_keyPrefix => 'Prefix sleutel';

View file

@ -1688,6 +1688,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_otherNodes => 'Inne węzły';
@override
String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
@override
String get map_keyPrefix => 'Prefiks klucza';

View file

@ -1686,6 +1686,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_otherNodes => 'Outros Nós';
@override
String get map_showOverlaps => 'Sobreposições da Chave Repeater';
@override
String get map_keyPrefix => 'Prefixo Chave';

View file

@ -1689,6 +1689,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_otherNodes => 'Другие ноды';
@override
String get map_showOverlaps => 'Перекрытия ключа повтора';
@override
String get map_keyPrefix => 'Префикс ключа';

View file

@ -1675,6 +1675,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_otherNodes => 'Ostatné uzly';
@override
String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
@override
String get map_keyPrefix => 'Päťciferné predpona';

View file

@ -1671,6 +1671,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_otherNodes => 'Druge vozlišča';
@override
String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
@override
String get map_keyPrefix => 'Predpona ključa';

View file

@ -1664,6 +1664,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_otherNodes => 'Andra noder';
@override
String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
@override
String get map_keyPrefix => 'Nyckelprefix';

View file

@ -1684,6 +1684,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_otherNodes => 'Інші вузли';
@override
String get map_showOverlaps => 'Перекриття ключа повторювача';
@override
String get map_keyPrefix => 'Префікс ключа';

View file

@ -1582,6 +1582,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_otherNodes => '其他节点';
@override
String get map_showOverlaps => '重复键重叠';
@override
String get map_keyPrefix => '关键字前缀';

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Herhalingssleutel overlapt"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
"settings_multiAck": "Wiele potwierdzeń: {value}"
"settings_multiAck": "Wiele potwierdzeń: {value}",
"map_showOverlaps": "Nakładające się klucze powtarzalne"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sobreposições da Chave Repeater"
}

View file

@ -1181,5 +1181,6 @@
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}"
"settings_multiAck": "Мульти-ACK: {value}",
"map_showOverlaps": "Перекрытия ключа повтора"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}"
"settings_multiAck": "Viaceré ACK: {value}",
"map_showOverlaps": "Prekrývanie opakovača kľúča"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "Večkratni potrditvi: {value}",
"settings_telemetryModeUpdated": "Način telemetrije posodobljen"
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}"
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Repeater-nyckelöverlappningar"
}

View file

@ -1941,5 +1941,6 @@
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}"
"settings_multiAck": "Багатократне підтвердження: {value}",
"map_showOverlaps": "Перекриття ключа повторювача"
}

View file

@ -1946,5 +1946,6 @@
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}",
"settings_multiAck": "多重ACK{value}",
"settings_telemetryModeUpdated": "遥测模式已更新"
"settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠"
}

View file

@ -18,6 +18,7 @@ class AppSettings {
final bool mapShowRepeaters;
final bool mapShowChatNodes;
final bool mapShowOtherNodes;
final bool mapShowOverlaps;
final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
@ -53,6 +54,7 @@ class AppSettings {
this.mapShowRepeaters = true,
this.mapShowChatNodes = true,
this.mapShowOtherNodes = true,
this.mapShowOverlaps = false,
this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
@ -92,6 +94,7 @@ class AppSettings {
'map_show_repeaters': mapShowRepeaters,
'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes,
'map_show_overlaps': mapShowOverlaps,
'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
@ -137,6 +140,7 @@ class AppSettings {
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
@ -196,6 +200,7 @@ class AppSettings {
bool? mapShowRepeaters,
bool? mapShowChatNodes,
bool? mapShowOtherNodes,
bool? mapShowOverlaps,
double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
@ -231,6 +236,7 @@ class AppSettings {
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,

View file

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:math';
import 'dart:typed_data';
@ -52,7 +53,7 @@ class MapScreen extends StatefulWidget {
class _MapScreenState extends State<MapScreen> {
// Zoom level at which node labels start to appear
static const double _labelZoomThreshold = 12.0;
static const double _labelZoomThreshold = 14.0;
final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService();
@ -329,7 +330,9 @@ class _MapScreenState extends State<MapScreen> {
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
onPressed: () => _startPath(),
onPressed: () => _startPath(
LatLng(connector.selfLatitude!, connector.selfLongitude!),
),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
@ -580,6 +583,7 @@ class _MapScreenState extends State<MapScreen> {
// 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])) {
@ -595,6 +599,11 @@ class _MapScreenState extends State<MapScreen> {
for (final contact in allContacts) {
if (contact.hasLocation) continue;
if (contact.lastSeen.isBefore(
DateTime.now().subtract(const Duration(hours: 24)),
)) {
continue; // skip stale contacts
}
final anchorSet = <LatLng>{};
@ -641,10 +650,19 @@ class _MapScreenState extends State<MapScreen> {
continue; // discard implausible guesses near (0, 0)
}
} else {
double lat = 0, lon = 0;
double lat = 0, lon = 0, weight = 1.0;
int counted = 0;
for (final a in anchors) {
lat += a.latitude;
lon += a.longitude;
if (counted == 0) {
lat = a.latitude;
lon = a.longitude;
} else {
lat += a.latitude * weight;
lon += a.longitude * weight;
}
// weight subsequent anchors less to create a bias towards the first (if more than 2)
weight = weight / 2;
counted++;
}
position = _offsetGuessedPosition(
LatLng(lat / anchors.length, lon / anchors.length),
@ -812,31 +830,67 @@ class _MapScreenState extends State<MapScreen> {
return markers;
}
List<Contact> _filterContactsBySettings(
List<Contact> contacts,
dynamic settings,
) {
List<Contact> filtered = [];
bool addContact = false;
for (final contact in contacts) {
addContact = false;
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater &&
(settings.mapShowRepeaters ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
if (contact.type == advTypeChat &&
(settings.mapShowChatNodes || _isBuildingPathTrace)) {
addContact = true;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(settings.mapShowOtherNodes ||
_isBuildingPathTrace ||
settings.mapShowOverlaps)) {
addContact = true;
}
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false;
}
if (addContact) {
filtered.add(contact);
}
}
return filtered;
}
List<Marker> _buildMarkers(
List<Contact> contacts,
settings, {
required bool showLabels,
}) {
final markers = <Marker>[];
for (final contact in contacts) {
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater &&
(!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
continue;
}
if (contact.type == advTypeChat &&
!(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
continue;
}
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
(!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
continue;
}
final filteredContacts = _filterContactsBySettings(contacts, settings);
for (final contact in filteredContacts) {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 35,
@ -852,7 +906,9 @@ class _MapScreenState extends State<MapScreen> {
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: _getNodeColor(contact.type),
color: settings.mapShowOverlaps && !_isBuildingPathTrace
? Colors.red
: _getNodeColor(contact.type),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
@ -879,7 +935,9 @@ class _MapScreenState extends State<MapScreen> {
markers.add(
_buildNodeLabelMarker(
point: LatLng(contact.latitude!, contact.longitude!),
label: contact.name,
label: settings.mapShowOverlaps && !_isBuildingPathTrace
? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
: contact.name,
),
);
}
@ -959,20 +1017,12 @@ class _MapScreenState extends State<MapScreen> {
int markerCount,
int guessedCount,
) {
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++;
}
final filteredContacts = _filterContactsBySettings(
contactsWithLocation,
settings,
);
final nodeCount = filteredContacts.length;
return Positioned(
top: 16,
@ -1846,6 +1896,15 @@ class _MapScreenState extends State<MapScreen> {
},
contentPadding: EdgeInsets.zero,
),
CheckboxListTile(
title: Text(context.l10n.map_showOverlaps),
value: settings.mapShowOverlaps,
onChanged: (value) {
service.setMapShowOverlaps(value ?? true);
},
contentPadding: EdgeInsets.zero,
),
const SizedBox(height: 16),
Text(
context.l10n.map_keyPrefix,
@ -2004,12 +2063,13 @@ class _MapScreenState extends State<MapScreen> {
});
}
void _startPath() {
void _startPath(LatLng position) {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_points.clear();
_polylines.clear();
_points.add(position);
});
}
@ -2055,14 +2115,14 @@ class _MapScreenState extends State<MapScreen> {
.join(','),
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 6),
// const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 8,
spacing: 1,
runSpacing: 1,
children: [
if (_pathTrace.isNotEmpty)
ElevatedButton(
IconButton(
onPressed: () {
Navigator.push(
context,
@ -2077,15 +2137,37 @@ class _MapScreenState extends State<MapScreen> {
_isBuildingPathTrace = false;
});
},
child: Text(l10n.map_runTrace),
tooltip: "Path Trace",
icon: const Icon(Icons.arrow_forward_outlined),
),
if (_pathTrace.isNotEmpty)
ElevatedButton(
IconButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
flipPathAround: true,
),
),
);
setState(() {
_isBuildingPathTrace = false;
});
},
tooltip: "Build Return Path",
icon: const Icon(Icons.replay),
),
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: _removePath,
child: Text(l10n.map_removeLast),
tooltip: "Remove Last Point",
icon: const Icon(Icons.delete),
),
if (_pathTrace.isEmpty)
ElevatedButton(
IconButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
@ -2097,7 +2179,7 @@ class _MapScreenState extends State<MapScreen> {
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
child: Text(l10n.common_cancel),
icon: const Icon(Icons.close),
),
],
),

View file

@ -311,10 +311,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
),
ListTile(
leading: const Icon(Icons.cell_tower),
title: Text(l10n.settings_sendAdvertisement),
subtitle: Text(l10n.settings_sendAdvertisementSubtitle),
onTap: () => _sendAdvert(context, connector),
leading: const Icon(Icons.delete_outline, color: Colors.red),
title: Text("Delete All Paths"),
subtitle: Text(
"Clear all path data from contacts.",
style: TextStyle(color: Colors.red[700]),
),
onTap: () => connector.deleteAllPaths(),
),
const Divider(height: 1),
ListTile(

View file

@ -64,6 +64,10 @@ class AppSettingsService extends ChangeNotifier {
await updateSettings(_settings.copyWith(mapShowOtherNodes: value));
}
Future<void> setMapShowOverlaps(bool value) async {
await updateSettings(_settings.copyWith(mapShowOverlaps: value));
}
Future<void> setMapTimeFilterHours(double value) async {
await updateSettings(_settings.copyWith(mapTimeFilterHours: value));
}

View file

@ -565,6 +565,16 @@ class PathHistoryService extends ChangeNotifier {
_floodStats.remove(oldest);
}
}
void clearAllHistories() {
_cache.clear();
_cacheAccessOrder.clear();
_autoRotationIndex.clear();
_floodStats.clear();
_storage.clearAllPathHistories();
_version = 0;
notifyListeners();
}
}
class _DeferredPathRecord {