diff --git a/mobile/app/(tabs)/import.tsx b/mobile/app/(tabs)/import.tsx index abf250b..459679a 100644 --- a/mobile/app/(tabs)/import.tsx +++ b/mobile/app/(tabs)/import.tsx @@ -2,8 +2,9 @@ import * as DocumentPicker from 'expo-document-picker'; import * as FileSystem from 'expo-file-system/legacy'; import { useSQLiteContext } from 'expo-sqlite'; import { useState } from 'react'; -import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { insertActivity } from '@/db/queries'; +import { PyodideWebView } from '@/extraction/PyodideWebView'; import { extractFile } from '@/extraction/extractActivity'; const FIT_EXTENSIONS = ['.fit', '.fit.gz']; @@ -129,6 +130,13 @@ export default function ImportScreen() { } return ( + + {/* Hidden WebView for Pyodide — mounted here so it lives inside the tab + (Expo Router keeps tabs mounted after first visit, preserving Pyodide state). + The 1×1 container clips it out of the scroll layout entirely. */} + + + Import @@ -192,6 +200,7 @@ export default function ImportScreen() { + ); } @@ -246,7 +255,9 @@ function arrayBufferToBase64(buf: ArrayBuffer): string { // ── Styles ─────────────────────────────────────────────────────────────────── const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#09090b' }, + screen: { flex: 1, backgroundColor: '#09090b' }, + hiddenEngine: { position: 'absolute', width: 1, height: 1, overflow: 'hidden' }, + container: { flex: 1 }, content: { padding: 16, paddingTop: 60, paddingBottom: 40 }, header: { color: '#fff', fontSize: 22, fontWeight: '700', marginBottom: 12 }, body: { color: '#a1a1aa', fontSize: 14, lineHeight: 20, marginBottom: 24 }, diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 120bdd0..a2c9de5 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -1,24 +1,13 @@ import { Stack } from 'expo-router'; import { SQLiteProvider } from 'expo-sqlite'; import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, View } from 'react-native'; import { migrateDb } from '@/db'; -import { PyodideWebView } from '@/extraction/PyodideWebView'; export default function RootLayout() { return ( - - {/* Hidden WebView: starts loading Pyodide immediately so the runtime - is warm by the time the user opens the Import tab. */} - - - - - - + + + + ); } - -const styles = StyleSheet.create({ - root: { flex: 1 }, -}); diff --git a/mobile/extraction/PyodideWebView.tsx b/mobile/extraction/PyodideWebView.tsx index 222ef8b..f4994f5 100644 --- a/mobile/extraction/PyodideWebView.tsx +++ b/mobile/extraction/PyodideWebView.tsx @@ -10,9 +10,12 @@ const PY_INSTALL_PACKAGES = [ 'await micropip.install(["fitdecode", "gpxpy"])', ].join('\n'); +// emfs:// is Pyodide's Emscripten-FS URL scheme — the only reliable way to +// install a wheel from bytes without an http/https URL (blob: URLs are not +// recognised by micropip and cause an InvalidRequirement parse error). const PY_INSTALL_WHEEL = [ 'import micropip', - 'await micropip.install(_blobUrl, deps=False)', + 'await micropip.install("emfs:///tmp/bincio.whl", deps=False)', ].join('\n'); const PY_EXTRACT = [ @@ -109,16 +112,14 @@ window._bincioExtract = async function(params) { if (initError) throw new Error(initError); // Install bincio wheel on first extraction. - // Wheel bytes arrive pre-fetched from the React Native side as base64, - // so the WebView never needs to make an HTTP request (avoids ATS blocks). + // Wheel bytes arrive pre-fetched from React Native (avoids ATS/HTTP issues). + // Write to Pyodide's Emscripten FS so micropip can install via emfs:// URL + // (blob: URLs are not recognised by micropip — they cause an InvalidRequirement error). if (!wheelReady) { post({ type: 'progress', msg: 'Loading Bincio…' }); var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); }); - var wheelBlob = new Blob([wheelBytes]); - var blobUrl = URL.createObjectURL(wheelBlob); - pyodide.globals.set('_blobUrl', blobUrl); + pyodide.FS.writeFile('/tmp/bincio.whl', wheelBytes); await pyodide.runPythonAsync(_PY_INSTALL_WHEEL); - URL.revokeObjectURL(blobUrl); wheelReady = true; }