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;
}