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:
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user