feat: Phase 1 — FIT/GPX/TCX extraction via Pyodide in hidden WebView
- extraction/PyodideWebView.tsx: hidden WebView (1×1 px, off-screen) that
bootstraps Pyodide v0.26.4 from jsDelivr CDN on app startup; loads lxml,
pyyaml, micropip, fitdecode, gpxpy automatically; installs the bincio wheel
lazily on the first extraction call via a blob URL (avoids startup delay)
- extraction/extractActivity.ts: typed bridge — extractFile(filename, base64,
wheelUrl, onStatus) injects JS into the WebView, tracks pending promises by
request ID, resolves with { id, detail, timeseries, geojson, sourceHash }
- app/_layout.tsx: mounts <PyodideWebView> outside SQLiteProvider at root so
the runtime warms up as soon as the app opens
- app/(tabs)/import.tsx: replaces the placeholder alert with real extraction;
reads files as base64, calls extractFile with a progress callback, stores
detail_json + timeseries_json + geojson + real SHA-256 source_hash; resolves
wheel URL via GET /api/wheel/version with fallback to /api/wheel/download;
falls back to bincio.org if no instance is configured
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { createRef } from 'react';
|
||||
import type WebView from 'react-native-webview';
|
||||
import type { WebViewMessageEvent } from 'react-native-webview';
|
||||
|
||||
export type ExtractionResult = {
|
||||
id: string;
|
||||
detail: object;
|
||||
timeseries: object | null;
|
||||
geojson: object | null;
|
||||
sourceHash: string;
|
||||
};
|
||||
|
||||
type Pending = {
|
||||
resolve: (r: ExtractionResult) => void;
|
||||
reject: (e: Error) => void;
|
||||
onStatus: (msg: string) => void;
|
||||
};
|
||||
|
||||
export const pyodideRef = createRef<WebView>();
|
||||
|
||||
const pending = new Map<string, Pending>();
|
||||
let reqCounter = 0;
|
||||
let isExtracting = false;
|
||||
|
||||
export function handleWebViewMessage(e: WebViewMessageEvent): void {
|
||||
let msg: Record<string, unknown>;
|
||||
try { msg = JSON.parse(e.nativeEvent.data); } catch { return; }
|
||||
|
||||
const reqId = msg.reqId as string | undefined;
|
||||
const p = reqId ? pending.get(reqId) : undefined;
|
||||
|
||||
switch (msg.type) {
|
||||
case 'result':
|
||||
if (p) {
|
||||
pending.delete(reqId!);
|
||||
p.resolve({
|
||||
id: msg.id as string,
|
||||
detail: msg.detail as object,
|
||||
timeseries: (msg.timeseries as object | null) ?? null,
|
||||
geojson: (msg.geojson as object | null) ?? null,
|
||||
sourceHash: msg.sourceHash as string,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'error':
|
||||
if (p) {
|
||||
pending.delete(reqId!);
|
||||
p.reject(new Error(msg.message as string));
|
||||
}
|
||||
break;
|
||||
case 'progress':
|
||||
p?.onStatus(msg.msg as string);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractFile(
|
||||
filename: string,
|
||||
base64: string,
|
||||
wheelUrl: string,
|
||||
onStatus: (msg: string) => void = () => {},
|
||||
): Promise<ExtractionResult> {
|
||||
if (isExtracting) return Promise.reject(new Error('Another extraction is already in progress'));
|
||||
|
||||
const webview = pyodideRef.current;
|
||||
if (!webview) return Promise.reject(new Error('Extraction engine not ready — restart the app'));
|
||||
|
||||
isExtracting = true;
|
||||
const reqId = String(++reqCounter);
|
||||
const args = JSON.stringify({ reqId, filename, base64, wheelUrl });
|
||||
|
||||
return new Promise<ExtractionResult>((resolve, reject) => {
|
||||
pending.set(reqId, {
|
||||
resolve: (r) => { isExtracting = false; resolve(r); },
|
||||
reject: (e) => { isExtracting = false; reject(e); },
|
||||
onStatus,
|
||||
});
|
||||
webview.injectJavaScript(`window._bincioExtract(${args}); true;`);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user