2026-02-05 13:38:49 -08:00
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
|
import 'package:gpx/gpx.dart';
|
|
|
|
|
|
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
|
|
|
|
|
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
|
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
|
|
import 'dart:io';
|
2026-02-22 06:54:27 -08:00
|
|
|
|
import '../utils/platform_info.dart';
|
2026-02-05 13:38:49 -08:00
|
|
|
|
|
|
|
|
|
|
import 'package:share_plus/share_plus.dart';
|
|
|
|
|
|
|
|
|
|
|
|
class ContactExport {
|
|
|
|
|
|
final String name;
|
|
|
|
|
|
final double lat;
|
|
|
|
|
|
final double lon;
|
|
|
|
|
|
final String desc;
|
|
|
|
|
|
final double? ele;
|
2026-03-25 18:30:27 -07:00
|
|
|
|
final String url;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
ContactExport({
|
|
|
|
|
|
required this.name,
|
|
|
|
|
|
required this.lat,
|
|
|
|
|
|
required this.lon,
|
|
|
|
|
|
required this.desc,
|
2026-03-25 18:30:27 -07:00
|
|
|
|
required this.url,
|
2026-02-05 13:38:49 -08:00
|
|
|
|
this.ele,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 13:46:05 -08:00
|
|
|
|
const int gpxExportFailed = -1;
|
|
|
|
|
|
const int gpxExportSuccess = 1;
|
|
|
|
|
|
const int gpxExportNoContacts = 2;
|
|
|
|
|
|
const int gpxExportCancelled = 3;
|
|
|
|
|
|
const int gpxExportNotAvailable = 4;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
|
|
|
|
|
|
class GpxExport {
|
2026-02-05 13:46:05 -08:00
|
|
|
|
final MeshCoreConnector _connector;
|
|
|
|
|
|
final List<ContactExport> _contacts = [];
|
2026-02-05 13:38:49 -08:00
|
|
|
|
|
|
|
|
|
|
GpxExport(this._connector);
|
|
|
|
|
|
|
2026-02-07 11:07:57 -08:00
|
|
|
|
void _addContact(
|
|
|
|
|
|
String name,
|
|
|
|
|
|
double lat,
|
|
|
|
|
|
double lon,
|
2026-03-25 18:30:27 -07:00
|
|
|
|
String url,
|
2026-02-07 11:07:57 -08:00
|
|
|
|
String desc, [
|
|
|
|
|
|
double? ele,
|
|
|
|
|
|
]) {
|
|
|
|
|
|
_contacts.add(
|
|
|
|
|
|
ContactExport(
|
|
|
|
|
|
name: name.trim(),
|
|
|
|
|
|
lat: lat,
|
|
|
|
|
|
lon: lon,
|
|
|
|
|
|
desc: desc.trim(),
|
|
|
|
|
|
ele: ele,
|
2026-03-25 18:30:27 -07:00
|
|
|
|
url: url,
|
2026-02-07 11:07:57 -08:00
|
|
|
|
),
|
|
|
|
|
|
);
|
2026-02-05 13:38:49 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void addRepeaters() {
|
2026-03-26 16:46:01 -07:00
|
|
|
|
final contacts = _connector.allContacts.where(
|
|
|
|
|
|
(c) => c.type == advTypeRepeater || c.type == advTypeRoom,
|
|
|
|
|
|
);
|
2026-02-08 12:26:49 -08:00
|
|
|
|
for (var contact in contacts) {
|
|
|
|
|
|
if (contact.latitude == null || contact.longitude == null) {
|
2026-02-05 13:38:49 -08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-25 18:30:27 -07:00
|
|
|
|
final url = contact.rawPacket != null
|
|
|
|
|
|
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
|
|
|
|
|
: "";
|
2026-02-05 13:38:49 -08:00
|
|
|
|
_addContact(
|
2026-02-08 12:26:49 -08:00
|
|
|
|
contact.name,
|
|
|
|
|
|
contact.latitude!,
|
|
|
|
|
|
contact.longitude!,
|
|
|
|
|
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
2026-03-25 18:30:27 -07:00
|
|
|
|
url,
|
2026-02-05 13:38:49 -08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void addContacts() {
|
2026-03-26 16:46:01 -07:00
|
|
|
|
final contacts = _connector.allContacts.where((c) => c.type == advTypeChat);
|
2026-02-08 12:26:49 -08:00
|
|
|
|
for (var contact in contacts) {
|
|
|
|
|
|
if (contact.latitude == null || contact.longitude == null) {
|
2026-02-05 13:38:49 -08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-25 18:30:27 -07:00
|
|
|
|
final url = contact.rawPacket != null
|
|
|
|
|
|
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
|
|
|
|
|
: "";
|
2026-02-05 13:38:49 -08:00
|
|
|
|
_addContact(
|
2026-02-08 12:26:49 -08:00
|
|
|
|
contact.name,
|
|
|
|
|
|
contact.latitude!,
|
|
|
|
|
|
contact.longitude!,
|
|
|
|
|
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
2026-03-25 18:30:27 -07:00
|
|
|
|
url,
|
2026-02-05 13:38:49 -08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void addAll() {
|
2026-03-26 16:46:01 -07:00
|
|
|
|
final contacts = _connector.allContacts;
|
2026-03-25 18:30:27 -07:00
|
|
|
|
for (var contact in contacts) {
|
2026-02-08 12:26:49 -08:00
|
|
|
|
if (contact.latitude == null || contact.longitude == null) {
|
2026-02-05 13:38:49 -08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-25 18:30:27 -07:00
|
|
|
|
final url = contact.rawPacket != null
|
|
|
|
|
|
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
|
|
|
|
|
|
: "";
|
2026-02-05 13:38:49 -08:00
|
|
|
|
_addContact(
|
2026-02-08 12:26:49 -08:00
|
|
|
|
contact.name,
|
|
|
|
|
|
contact.latitude ?? 0.0,
|
|
|
|
|
|
contact.longitude ?? 0.0,
|
|
|
|
|
|
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
|
2026-03-25 18:30:27 -07:00
|
|
|
|
url,
|
2026-02-05 13:38:49 -08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 19:45:02 -08:00
|
|
|
|
Future<int> exportGPX(
|
|
|
|
|
|
String name,
|
|
|
|
|
|
String description,
|
|
|
|
|
|
String filename,
|
|
|
|
|
|
String shareText,
|
|
|
|
|
|
String subject,
|
|
|
|
|
|
) async {
|
2026-02-22 06:54:27 -08:00
|
|
|
|
if (PlatformInfo.isWeb) {
|
|
|
|
|
|
debugPrint("GPX export is not supported on Web.");
|
|
|
|
|
|
return gpxExportNotAvailable;
|
|
|
|
|
|
}
|
2026-02-05 13:38:49 -08:00
|
|
|
|
if (_contacts.isEmpty) {
|
|
|
|
|
|
debugPrint("No repeaters to export – nothing to share.");
|
2026-02-05 13:46:05 -08:00
|
|
|
|
return gpxExportNoContacts;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. Build GPX content (your existing logic – unchanged here)
|
|
|
|
|
|
final gpx = Gpx()
|
|
|
|
|
|
..version = '1.1'
|
2026-02-07 19:45:02 -08:00
|
|
|
|
..creator = 'meshcore-open exporter'
|
2026-02-05 13:38:49 -08:00
|
|
|
|
..metadata = Metadata(
|
2026-02-07 19:45:02 -08:00
|
|
|
|
name: name,
|
|
|
|
|
|
desc: description,
|
2026-02-07 11:07:57 -08:00
|
|
|
|
time: DateTime.now().toUtc(),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
gpx.wpts = _contacts
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(c) => Wpt(
|
|
|
|
|
|
lat: c.lat,
|
|
|
|
|
|
lon: c.lon,
|
|
|
|
|
|
ele: c.ele,
|
|
|
|
|
|
name: c.name,
|
|
|
|
|
|
desc: c.desc,
|
2026-03-25 18:30:27 -07:00
|
|
|
|
extensions: {
|
|
|
|
|
|
"meshcore": {"url": c.url},
|
|
|
|
|
|
},
|
2026-02-07 11:07:57 -08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
.toList();
|
2026-02-05 13:38:49 -08:00
|
|
|
|
|
|
|
|
|
|
final xml = GpxWriter().asString(gpx, pretty: true);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Save to file
|
|
|
|
|
|
final dir = await getApplicationDocumentsDirectory();
|
2026-02-07 11:07:57 -08:00
|
|
|
|
final timestamp = DateTime.now()
|
|
|
|
|
|
.toUtc()
|
|
|
|
|
|
.toIso8601String()
|
2026-02-05 13:38:49 -08:00
|
|
|
|
.replaceAll(':', '-')
|
|
|
|
|
|
.replaceAll('.', '-')
|
|
|
|
|
|
.split('T')
|
|
|
|
|
|
.join('_');
|
2026-02-07 19:45:02 -08:00
|
|
|
|
|
|
|
|
|
|
final path = '${dir.path}/$filename$timestamp.gpx';
|
2026-02-05 13:38:49 -08:00
|
|
|
|
|
|
|
|
|
|
final file = File(path);
|
|
|
|
|
|
await file.writeAsString(xml);
|
|
|
|
|
|
|
|
|
|
|
|
final result = await SharePlus.instance.share(
|
2026-02-08 12:26:49 -08:00
|
|
|
|
ShareParams(text: shareText, subject: subject, files: [XFile(path)]),
|
2026-02-05 13:38:49 -08:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-08 12:26:49 -08:00
|
|
|
|
await file.delete();
|
|
|
|
|
|
|
2026-02-05 13:38:49 -08:00
|
|
|
|
switch (result.status) {
|
|
|
|
|
|
case ShareResultStatus.success:
|
|
|
|
|
|
debugPrint('Share successful – user completed the action.');
|
2026-02-05 13:46:05 -08:00
|
|
|
|
return gpxExportSuccess;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
case ShareResultStatus.dismissed:
|
|
|
|
|
|
debugPrint('Share sheet was dismissed / cancelled by user.');
|
2026-02-05 13:46:05 -08:00
|
|
|
|
return gpxExportCancelled;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
case ShareResultStatus.unavailable:
|
|
|
|
|
|
debugPrint('Sharing is not available on this platform / context.');
|
2026-02-05 13:46:05 -08:00
|
|
|
|
return gpxExportNotAvailable;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e, stack) {
|
|
|
|
|
|
debugPrint('Export or share failed: $e\n$stack');
|
|
|
|
|
|
}
|
2026-02-05 13:46:05 -08:00
|
|
|
|
return gpxExportFailed;
|
2026-02-05 13:38:49 -08:00
|
|
|
|
}
|
2026-02-07 11:07:57 -08:00
|
|
|
|
}
|