mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Compare commits
4 Commits
v2026.0322
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c299349b6a | ||
|
|
ab05f13de9 | ||
|
|
0e61dc5bf1 | ||
|
|
d45924d7b4 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-15 to 2026-03-22
|
||||
|
||||
- Device connectivity and routing: added Android USB serial companion support, serial compass support, GPS module controls, per-contact path size selection, and favourites support via firmware flags.
|
||||
- Location and map intelligence: added RSSI-based repeater trilateration and contact location estimates, isolated MET history, matched the official advert import/discovery flow, and refreshed map presentation by removing contact trails.
|
||||
- Telemetry and device info: surfaced self telemetry and sensor defaults, opened device info from signal and battery taps, improved signal chip behavior, fixed duplicate telemetry delivery, and restored telemetry refresh behavior.
|
||||
- Contacts and messaging: added meshcore:// contact sharing and repeater neighbour views, supported MeshCore message links, fixed DM retry behavior, corrected room poster naming, and compacted the recipient selector.
|
||||
- UI polish and platform upkeep: improved device settings layout, tightened sensor and contact presentation, expanded localization coverage, and refreshed platform dependencies and build numbers.
|
||||
|
||||
## Key PRs
|
||||
|
||||
- [#26](https://github.com/dz0ny/meshcore-sar/pull/26) Merge pull request #26 from `dz0ny/feat/sensor-telemetry-preview`
|
||||
|
||||
## 2026-03-08 to 2026-03-14
|
||||
|
||||
- Discovery and contacts: finished the discovery flow, removed artificial UI delays, added profile storage and sync, and allowed contact name overrides.
|
||||
|
||||
@@ -263,7 +263,6 @@ class MapWorkspaceProfileSection {
|
||||
final bool? showKrasFireZonesOverlay;
|
||||
final bool? showPlaceNamesOverlay;
|
||||
final bool? showMunicipalityBordersOverlay;
|
||||
final bool? showAllContactTrails;
|
||||
final bool? hideRepeatersOnMap;
|
||||
|
||||
const MapWorkspaceProfileSection({
|
||||
@@ -284,7 +283,6 @@ class MapWorkspaceProfileSection {
|
||||
this.showKrasFireZonesOverlay,
|
||||
this.showPlaceNamesOverlay,
|
||||
this.showMunicipalityBordersOverlay,
|
||||
this.showAllContactTrails,
|
||||
this.hideRepeatersOnMap,
|
||||
});
|
||||
|
||||
@@ -306,7 +304,6 @@ class MapWorkspaceProfileSection {
|
||||
showKrasFireZonesOverlay == null &&
|
||||
showPlaceNamesOverlay == null &&
|
||||
showMunicipalityBordersOverlay == null &&
|
||||
showAllContactTrails == null &&
|
||||
hideRepeatersOnMap == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -327,7 +324,6 @@ class MapWorkspaceProfileSection {
|
||||
'showKrasFireZonesOverlay': showKrasFireZonesOverlay,
|
||||
'showPlaceNamesOverlay': showPlaceNamesOverlay,
|
||||
'showMunicipalityBordersOverlay': showMunicipalityBordersOverlay,
|
||||
'showAllContactTrails': showAllContactTrails,
|
||||
'hideRepeatersOnMap': hideRepeatersOnMap,
|
||||
};
|
||||
|
||||
@@ -356,7 +352,6 @@ class MapWorkspaceProfileSection {
|
||||
showPlaceNamesOverlay: json['showPlaceNamesOverlay'] as bool?,
|
||||
showMunicipalityBordersOverlay:
|
||||
json['showMunicipalityBordersOverlay'] as bool?,
|
||||
showAllContactTrails: json['showAllContactTrails'] as bool?,
|
||||
hideRepeatersOnMap: json['hideRepeatersOnMap'] as bool?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,6 @@ class MapProvider with ChangeNotifier {
|
||||
MapCoordinateSpace _targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
String? _targetMapId;
|
||||
|
||||
final Set<String> _visibleContactPaths = {};
|
||||
|
||||
LocationTrail? _currentTrail;
|
||||
bool _isTrailVisible = true;
|
||||
final List<LocationTrail> _trailHistory = [];
|
||||
@@ -55,7 +53,6 @@ class MapProvider with ChangeNotifier {
|
||||
bool _showPlaceNamesOverlay = false;
|
||||
bool _showMunicipalityBordersOverlay = false;
|
||||
|
||||
bool _showAllContactTrails = true;
|
||||
bool _hideRepeatersOnMap = false;
|
||||
|
||||
LocationTrail? _importedTrail;
|
||||
@@ -72,8 +69,6 @@ class MapProvider with ChangeNotifier {
|
||||
bool get shouldAnimate => _shouldAnimate;
|
||||
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
|
||||
String? get targetMapId => _targetMapId;
|
||||
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
|
||||
|
||||
LocationTrail? get currentTrail => _currentTrail;
|
||||
bool get isTrailVisible => _isTrailVisible;
|
||||
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
|
||||
@@ -91,7 +86,6 @@ class MapProvider with ChangeNotifier {
|
||||
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
|
||||
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
|
||||
|
||||
bool get showAllContactTrails => _showAllContactTrails;
|
||||
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
||||
|
||||
LocationTrail? get importedTrail => _importedTrail;
|
||||
@@ -365,30 +359,6 @@ class MapProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleContactPath(String publicKeyHex) {
|
||||
if (_visibleContactPaths.contains(publicKeyHex)) {
|
||||
_visibleContactPaths.remove(publicKeyHex);
|
||||
} else {
|
||||
_visibleContactPaths.add(publicKeyHex);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isContactPathVisible(String publicKeyHex) {
|
||||
return _visibleContactPaths.contains(publicKeyHex);
|
||||
}
|
||||
|
||||
void hideAllPaths() {
|
||||
_visibleContactPaths.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void showOnlyPath(String publicKeyHex) {
|
||||
_visibleContactPaths.clear();
|
||||
_visibleContactPaths.add(publicKeyHex);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void startTrail() {
|
||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||
endTrail();
|
||||
@@ -556,12 +526,17 @@ class MapProvider with ChangeNotifier {
|
||||
Future<void> _loadInitialState() async {
|
||||
await Future.wait([
|
||||
loadOverlayState(),
|
||||
loadTrailSettings(),
|
||||
loadRepeaterVisibilitySettings(),
|
||||
_loadCustomMapState(),
|
||||
_removeDeprecatedSettings(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _removeDeprecatedSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_scopedKey('map_show_all_contact_trails'));
|
||||
}
|
||||
|
||||
Future<void> _saveOverlayState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
@@ -610,27 +585,6 @@ class MapProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> toggleAllContactTrails() async {
|
||||
_showAllContactTrails = !_showAllContactTrails;
|
||||
notifyListeners();
|
||||
await _saveTrailSettings();
|
||||
}
|
||||
|
||||
Future<void> loadTrailSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showAllContactTrails =
|
||||
prefs.getBool(_scopedKey('map_show_all_contact_trails')) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _saveTrailSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_all_contact_trails'),
|
||||
_showAllContactTrails,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setHideRepeatersOnMap(bool hide) async {
|
||||
if (_hideRepeatersOnMap == hide) return;
|
||||
_hideRepeatersOnMap = hide;
|
||||
@@ -755,7 +709,6 @@ class MapProvider with ChangeNotifier {
|
||||
'showKrasFireZonesOverlay': _showKrasFireZonesOverlay,
|
||||
'showPlaceNamesOverlay': _showPlaceNamesOverlay,
|
||||
'showMunicipalityBordersOverlay': _showMunicipalityBordersOverlay,
|
||||
'showAllContactTrails': _showAllContactTrails,
|
||||
'hideRepeatersOnMap': _hideRepeatersOnMap,
|
||||
};
|
||||
}
|
||||
@@ -792,7 +745,6 @@ class MapProvider with ChangeNotifier {
|
||||
_showPlaceNamesOverlay = json['showPlaceNamesOverlay'] as bool? ?? false;
|
||||
_showMunicipalityBordersOverlay =
|
||||
json['showMunicipalityBordersOverlay'] as bool? ?? false;
|
||||
_showAllContactTrails = json['showAllContactTrails'] as bool? ?? true;
|
||||
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import '../services/background_location_service.dart';
|
||||
import '../services/location_tracking_service.dart';
|
||||
import '../services/map_marker_service.dart';
|
||||
import '../services/message_destination_preferences.dart';
|
||||
import '../services/trail_color_service.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../widgets/map_debug_info.dart';
|
||||
import '../widgets/map/compass_widget.dart';
|
||||
@@ -873,7 +872,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (!hasCustomMap)
|
||||
ListTile(
|
||||
leading: Icon(Icons.add_photo_alternate),
|
||||
title: Text(AppLocalizations.of(context)!.loadFromGallery),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.loadFromGallery,
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Use a cave map image instead of GPS tiles',
|
||||
),
|
||||
@@ -905,7 +906,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(Icons.swap_horizontal_circle),
|
||||
title: Text(AppLocalizations.of(context)!.replaceImage),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.replaceImage,
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Pick a different map from the gallery',
|
||||
),
|
||||
@@ -941,7 +944,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (customMapConfig.isCalibrated)
|
||||
ListTile(
|
||||
leading: Icon(Icons.clear),
|
||||
title: Text(AppLocalizations.of(context)!.clearScale),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.clearScale,
|
||||
),
|
||||
onTap: () async {
|
||||
await mapProvider.clearCustomMapCalibration();
|
||||
if (!context.mounted) return;
|
||||
@@ -1277,9 +1282,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
_stopCustomMapCalibration();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.customMapScaleSaved)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.customMapScaleSaved),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1419,7 +1426,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(Icons.chat_bubble_outline),
|
||||
title: Text(AppLocalizations.of(context)!.openMessage),
|
||||
subtitle: Text(AppLocalizations.of(context)!.jumpToTheRelatedSarMessage),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.jumpToTheRelatedSarMessage,
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await _openSarMarkerMessage(
|
||||
@@ -1655,7 +1664,11 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.pleaseSelectADestinationToSendSarMarker),
|
||||
content: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.pleaseSelectADestinationToSendSarMarker,
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -1785,7 +1798,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.sarMarkerBroadcastToPublicChannel),
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.sarMarkerBroadcastToPublicChannel,
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
@@ -2514,52 +2529,6 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
);
|
||||
},
|
||||
),
|
||||
// Contact trail polylines (rendered before user trail and markers)
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, _) {
|
||||
// Determine which contacts to show trails for
|
||||
final contactsToShow = mapProvider.showAllContactTrails
|
||||
? contactsWithLocation // Show all when master toggle is ON
|
||||
: contactsWithLocation.where(
|
||||
(contact) => mapProvider.isContactPathVisible(
|
||||
contact.publicKeyHex,
|
||||
),
|
||||
); // Individual toggles
|
||||
|
||||
return PolylineLayer(
|
||||
polylines: contactsToShow
|
||||
.where(
|
||||
(contact) => contact.advertHistory.length >= 2,
|
||||
)
|
||||
.map((contact) {
|
||||
// Use TrailColorService for consistent, emoji-based colors
|
||||
final color = TrailColorService.getTrailColor(
|
||||
contact,
|
||||
);
|
||||
|
||||
return Polyline(
|
||||
points: contact.advertHistory
|
||||
.map((advert) => advert.location)
|
||||
.toList(),
|
||||
color: color.withValues(
|
||||
alpha: 0.95,
|
||||
), // More opaque for better visibility
|
||||
strokeWidth:
|
||||
4.5, // Thicker for better visibility on all map backgrounds
|
||||
borderColor: Colors.white.withValues(
|
||||
alpha: 0.6,
|
||||
), // Stronger border contrast
|
||||
borderStrokeWidth: 2.0, // Wider border
|
||||
// DASHED pattern to distinguish from solid user trail
|
||||
pattern: StrokePattern.dashed(
|
||||
segments: [8, 4],
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
// Location trail layer (rendered after paths, before drawings)
|
||||
const LocationTrailLayer(),
|
||||
],
|
||||
@@ -2604,7 +2573,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
||||
context: context,
|
||||
mapRotation: _getMapRotation(),
|
||||
userPosition: _locationService.currentPosition,
|
||||
estimatedLocations: contactsProvider.estimatedLocations,
|
||||
estimatedLocations:
|
||||
contactsProvider.estimatedLocations,
|
||||
onTap: (contact) {
|
||||
_showDetailedCompassWithContact(
|
||||
context,
|
||||
|
||||
@@ -1463,21 +1463,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, child) => SwitchListTile(
|
||||
secondary: Icon(Icons.timeline),
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!.showAllContactTrailsLabel,
|
||||
),
|
||||
subtitle: const Text(
|
||||
'Display location trails for all contacts that have history',
|
||||
),
|
||||
value: mapProvider.showAllContactTrails,
|
||||
onChanged: (value) async {
|
||||
await mapProvider.toggleAllContactTrails();
|
||||
},
|
||||
),
|
||||
),
|
||||
Consumer<MapProvider>(
|
||||
builder: (context, mapProvider, child) => SwitchListTile(
|
||||
secondary: Icon(Icons.router_outlined),
|
||||
|
||||
@@ -64,7 +64,6 @@ class MapWorkspaceSnapshotService {
|
||||
showPlaceNamesOverlay: mapProvider.showPlaceNamesOverlay,
|
||||
showMunicipalityBordersOverlay:
|
||||
mapProvider.showMunicipalityBordersOverlay,
|
||||
showAllContactTrails: mapProvider.showAllContactTrails,
|
||||
hideRepeatersOnMap: mapProvider.hideRepeatersOnMap,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../models/contact.dart';
|
||||
import '../../models/location_trail.dart';
|
||||
import '../../providers/map_provider.dart';
|
||||
import '../../providers/contacts_provider.dart';
|
||||
import '../../services/gpx_service.dart';
|
||||
import '../../services/trail_color_service.dart';
|
||||
import '../../l10n/app_localizations.dart';
|
||||
|
||||
/// Trail management controls widget
|
||||
@@ -14,17 +11,8 @@ class TrailControls extends StatelessWidget {
|
||||
|
||||
void _showTrailMenu(BuildContext context) {
|
||||
final mapProvider = Provider.of<MapProvider>(context, listen: false);
|
||||
final contactsProvider = Provider.of<ContactsProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
// Get contacts with trails (advertHistory >= 2 points)
|
||||
final contactsWithTrails = contactsProvider.contactsWithLocation
|
||||
.where((c) => c.advertHistory.length >= 2)
|
||||
.toList();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -183,110 +171,6 @@ class TrailControls extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
const Divider(),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Contact Trails Section
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.people, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.contactTrails,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Show All Contact Trails toggle
|
||||
SwitchListTile(
|
||||
secondary: Icon(Icons.route),
|
||||
title: Text(l10n.showAllContactTrails),
|
||||
subtitle: Text(
|
||||
contactsWithTrails.isEmpty
|
||||
? l10n.noContactsWithLocationHistory
|
||||
: mapProvider.showAllContactTrails
|
||||
? l10n.showingTrailsForContacts(
|
||||
contactsWithTrails.length,
|
||||
)
|
||||
: l10n.individualContactTrails,
|
||||
),
|
||||
value: mapProvider.showAllContactTrails,
|
||||
onChanged: contactsWithTrails.isNotEmpty
|
||||
? (value) {
|
||||
mapProvider.toggleAllContactTrails();
|
||||
setModalState(() {}); // Update modal UI
|
||||
}
|
||||
: null, // Disable if no contacts with trails
|
||||
),
|
||||
|
||||
// Individual contact trails (when "show all" is OFF)
|
||||
if (!mapProvider.showAllContactTrails &&
|
||||
contactsWithTrails.isNotEmpty)
|
||||
ExpansionTile(
|
||||
title: Text(l10n.individualContactTrails),
|
||||
initiallyExpanded: false,
|
||||
children: contactsWithTrails.map((contact) {
|
||||
final trailColor = TrailColorService.getTrailColor(
|
||||
contact,
|
||||
);
|
||||
final isVisible = mapProvider.isContactPathVisible(
|
||||
contact.publicKeyHex,
|
||||
);
|
||||
|
||||
return SwitchListTile(
|
||||
// Color indicator with emoji
|
||||
secondary: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (contact.roleEmoji != null)
|
||||
Text(
|
||||
contact.roleEmoji!,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: trailColor,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(child: Text(contact.displayName)),
|
||||
IconButton(
|
||||
tooltip: l10n.exportToClipboard,
|
||||
icon: const Icon(Icons.file_download_outlined),
|
||||
onPressed: () =>
|
||||
_exportContactTrail(context, contact),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
'${contact.advertHistory.length} points',
|
||||
),
|
||||
value: isVisible,
|
||||
onChanged: (value) {
|
||||
mapProvider.toggleContactPath(contact.publicKeyHex);
|
||||
setModalState(() {}); // Update modal UI
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Close button
|
||||
@@ -360,34 +244,6 @@ class TrailControls extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportContactTrail(BuildContext context, Contact contact) async {
|
||||
await _exportTrail(
|
||||
context,
|
||||
_buildContactTrail(contact),
|
||||
customName: '${contact.displayName} Trail',
|
||||
);
|
||||
}
|
||||
|
||||
LocationTrail _buildContactTrail(Contact contact) {
|
||||
final sortedHistory = [...contact.advertHistory]
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
return LocationTrail(
|
||||
id: 'contact_${contact.publicKeyHex}',
|
||||
isActive: false,
|
||||
startTime: sortedHistory.first.timestamp,
|
||||
endTime: sortedHistory.last.timestamp,
|
||||
points: sortedHistory
|
||||
.map(
|
||||
(advert) => TrailPoint(
|
||||
position: advert.location,
|
||||
timestamp: advert.timestamp,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _showClearConfirmation(
|
||||
BuildContext context,
|
||||
MapProvider mapProvider,
|
||||
|
||||
Reference in New Issue
Block a user