Add GPX export functionality and related UI components

This commit is contained in:
Winston Lowe
2026-02-05 13:38:49 -08:00
parent a54cc78691
commit 8b1228bf8d
4 changed files with 250 additions and 1 deletions
+165
View File
@@ -0,0 +1,165 @@
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';
import 'package:share_plus/share_plus.dart';
class ContactExport {
final String name;
final double lat;
final double lon;
final String desc;
final double? ele;
ContactExport({
required this.name,
required this.lat,
required this.lon,
required this.desc,
this.ele,
});
}
const int GpxExportFailed = -1;
const int GpxExportSuccess = 1;
const int GpxExportNoContacts = 2;
const int GpxExportCancelled = 3;
const int GpxExportNotAvailable = 4;
class GpxExport {
MeshCoreConnector _connector;
List<ContactExport> _contacts = [];
GpxExport(this._connector);
void _addContact(String name, double lat, double lon, String desc, [double? ele]) {
_contacts.add(ContactExport(
name: name.trim(),
lat: lat,
lon: lon,
desc: desc.trim(),
ele: ele,
));
}
void addRepeaters() {
final contacts = _connector.contacts;
final repeaters = contacts.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom).toList();
for (var repeater in repeaters) {
if (repeater.latitude == null || repeater.longitude == null) {
continue;
}
_addContact(
repeater.name,
repeater.latitude ?? 0.0,
repeater.longitude ?? 0.0,
"Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}",
);
}
}
void addContacts() {
final contacts = _connector.contacts;
final repeaters = contacts.where((c) => c.type == advTypeChat).toList();
for (var repeater in repeaters) {
if (repeater.latitude == null || repeater.longitude == null) {
continue;
}
_addContact(
repeater.name,
repeater.latitude ?? 0.0,
repeater.longitude ?? 0.0,
"Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}",
);
}
}
void addAll() {
final contacts = _connector.contacts;
for (var repeater in contacts.toList()) {
if (repeater.latitude == null || repeater.longitude == null) {
continue;
}
_addContact(
repeater.name,
repeater.latitude ?? 0.0,
repeater.longitude ?? 0.0,
"Type: ${repeater.typeLabel}\nPublic Key: ${repeater.publicKeyHex}",
);
}
}
Future<int> exportGPX() async {
if (_contacts.isEmpty) {
debugPrint("No repeaters to export nothing to share.");
return GpxExportNoContacts;
}
try {
// 1. Build GPX content (your existing logic unchanged here)
final gpx = Gpx()
..version = '1.1'
..creator = 'meshcore-open Repeater Exporter'
..metadata = Metadata(
name: 'Meshcore Repeaters',
desc: 'Repeater & room locations exported from meshcore-open',
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,
)).toList();
final xml = GpxWriter().asString(gpx, pretty: true);
// 2. Save to file
final dir = await getApplicationDocumentsDirectory();
final timestamp = DateTime.now().toUtc().toIso8601String()
.replaceAll(':', '-')
.replaceAll('.', '-')
.split('T')
.join('_');
final path = '${dir.path}/meshcore_repeaters_$timestamp.gpx';
final file = File(path);
await file.writeAsString(xml);
// 3. Modern share call (2025+ style)
final result = await SharePlus.instance.share(
ShareParams(
text: 'Repeater locations exported from meshcore-open app as GPX file.',
subject: 'Meshcore Repeaters GPX Export',
files: [XFile(path)],
// Optional: sharePositionOrigin: ... (if you want iPad popover positioning)
),
);
// 4. Handle result
switch (result.status) {
case ShareResultStatus.success:
debugPrint('Share successful user completed the action.');
return GpxExportSuccess;
case ShareResultStatus.dismissed:
debugPrint('Share sheet was dismissed / cancelled by user.');
return GpxExportCancelled;
case ShareResultStatus.unavailable:
debugPrint('Sharing is not available on this platform / context.');
return GpxExportNotAvailable;
}
// Optional cleanup (uncomment if you don't want to keep the file)
// await file.delete();
} catch (e, stack) {
debugPrint('Export or share failed: $e\n$stack');
// → here you could show a SnackBar / AlertDialog in real UI code
}
return GpxExportFailed;
}
}