feat: bulk operations in Saved tab (export, upload, delete, merge)

Select button in header enters selection mode. Cards show a checkbox and
become tappable. Header updates to show N selected + Cancel.

Bulk actions (bottom bar):
- Export: sequential Sharing.shareAsync for each selected file
- Upload: sequential upload with N/total progress text
- Delete: confirm then delete all selected at once
- Merge: parses each GPX file, combines track points sorted by time,
  prompts for title via Modal, saves as new recording

parseGpxFile() added to gpx.ts: reads file via expo-file-system,
extracts trkpt elements including hr/power/cad extensions via regex.
This commit is contained in:
Davide Scaini
2026-06-04 00:08:06 +02:00
parent cb74135c6c
commit 41a2435cc2
2 changed files with 306 additions and 65 deletions
+21
View File
@@ -55,6 +55,27 @@ function buildExtensions(pt: TrackPoint): string {
</extensions>`;
}
export async function parseGpxFile(fileUri: string): Promise<TrackPoint[]> {
const content = await new File(fileUri).text();
const points: TrackPoint[] = [];
const re = /<trkpt\s+lat="([^"]+)"\s+lon="([^"]+)">([\s\S]*?)<\/trkpt>/g;
let m;
while ((m = re.exec(content)) !== null) {
const lat = parseFloat(m[1]);
const lon = parseFloat(m[2]);
const inner = m[3];
const ele = parseFloat(inner.match(/<ele>([\d.-]+)<\/ele>/)?.[1] ?? '0');
const timeStr = inner.match(/<time>([^<]+)<\/time>/)?.[1] ?? '';
const hr = parseInt(inner.match(/<gpxtpx:hr>(\d+)<\/gpxtpx:hr>/)?.[1] ?? '') || undefined;
const power = parseInt(inner.match(/<gpxtpx:power>(\d+)<\/gpxtpx:power>/)?.[1] ?? '') || undefined;
const cad = parseInt(inner.match(/<gpxtpx:cad>(\d+)<\/gpxtpx:cad>/)?.[1] ?? '') || undefined;
if (!isNaN(lat) && !isNaN(lon)) {
points.push({ lat, lon, ele, time: new Date(timeStr), hr, power, cad });
}
}
return points;
}
function escapeXml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}