feat: scaffold Expo Prebuild project with all v1 screens and services
Sets up the full bincio-rec source tree: Zustand recording store with haversine stats, background GPS via expo-task-manager, BLE scan/subscribe for HR and power, GPX writer with Garmin extensions, SQLite recordings list, multipart upload to bincio-activity, React Navigation stack with bottom tabs, and build instructions in README.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { File, Directory, Paths } from 'expo-file-system';
|
||||
import { TrackPoint } from '../types';
|
||||
|
||||
function recordingsDir(): Directory {
|
||||
return new Directory(Paths.document, 'recordings');
|
||||
}
|
||||
|
||||
export function ensureRecordingsDir(): void {
|
||||
const dir = recordingsDir();
|
||||
if (!dir.exists) dir.create();
|
||||
}
|
||||
|
||||
export function saveGpx(trackPoints: TrackPoint[], title: string): string {
|
||||
ensureRecordingsDir();
|
||||
const filename = `${sanitizeFilename(title)}_${Date.now()}.gpx`;
|
||||
const file = new File(recordingsDir(), filename);
|
||||
file.write(buildGpx(trackPoints, title));
|
||||
return file.uri;
|
||||
}
|
||||
|
||||
export function buildGpx(trackPoints: TrackPoint[], title: string): string {
|
||||
const trkpts = trackPoints.map(buildTrkpt).join('\n');
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gpx version="1.1" creator="bincio-rec"
|
||||
xmlns="http://www.topografix.com/GPX/1/1"
|
||||
xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1">
|
||||
<metadata><name>${escapeXml(title)}</name></metadata>
|
||||
<trk>
|
||||
<name>${escapeXml(title)}</name>
|
||||
<trkseg>
|
||||
${trkpts}
|
||||
</trkseg>
|
||||
</trk>
|
||||
</gpx>`;
|
||||
}
|
||||
|
||||
function buildTrkpt(pt: TrackPoint): string {
|
||||
const ext = buildExtensions(pt);
|
||||
return ` <trkpt lat="${pt.lat}" lon="${pt.lon}">
|
||||
<ele>${pt.ele.toFixed(1)}</ele>
|
||||
<time>${pt.time.toISOString()}</time>${ext}
|
||||
</trkpt>`;
|
||||
}
|
||||
|
||||
function buildExtensions(pt: TrackPoint): string {
|
||||
if (pt.hr == null && pt.power == null && pt.cad == null) return '';
|
||||
const hr = pt.hr != null ? `<gpxtpx:hr>${pt.hr}</gpxtpx:hr>` : '';
|
||||
const power = pt.power != null ? `<gpxtpx:power>${pt.power}</gpxtpx:power>` : '';
|
||||
const cad = pt.cad != null ? `<gpxtpx:cad>${pt.cad}</gpxtpx:cad>` : '';
|
||||
return `
|
||||
<extensions>
|
||||
<gpxtpx:TrackPointExtension>
|
||||
${hr}${power}${cad}
|
||||
</gpxtpx:TrackPointExtension>
|
||||
</extensions>`;
|
||||
}
|
||||
|
||||
function escapeXml(s: string): string {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function sanitizeFilename(s: string): string {
|
||||
return s.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
||||
}
|
||||
Reference in New Issue
Block a user