Files
pi-mobile/apps/mobile/app/index.tsx
T
owenandCursor bb9999de58 Package Pi Mobile for developer distribution.
Publish the hub as pi-mobile-hub with CLI flags and LAN pairing auth, add mobile deep-link pairing and EAS build config, and expand gitignore for Expo/EAS artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 14:54:09 -05:00

294 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { router } from "expo-router";
import { useEffect } from "react";
import {
ActivityIndicator,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import { useSocket } from "@/contexts/SocketContext";
import { colors, monoFont } from "@/lib/theme";
import type { SavedComputer } from "@/lib/types";
function SavedComputerRow({
computer,
connecting,
onConnect,
onRemove,
}: {
computer: SavedComputer;
connecting: boolean;
onConnect: () => void;
onRemove: () => void;
}) {
return (
<View style={styles.savedRow}>
<Pressable
style={({ pressed }) => [styles.savedRowMain, pressed && styles.savedRowPressed]}
disabled={connecting}
onPress={onConnect}
>
<Text style={styles.savedName} numberOfLines={1}>
{computer.name}
</Text>
<Text style={styles.savedAddress}>
{computer.host}:{computer.port}
</Text>
</Pressable>
<Pressable
style={({ pressed }) => [styles.removeButton, pressed && styles.removeButtonPressed]}
disabled={connecting}
onPress={onRemove}
hitSlop={8}
>
<Text style={styles.removeButtonText}>×</Text>
</Pressable>
</View>
);
}
export default function ConnectScreen() {
const {
host,
port,
computerName,
token,
connected,
connecting,
error,
savedComputers,
setHost,
setPort,
setComputerName,
setToken,
connect,
connectToSavedComputer,
removeSavedComputerEntry,
} = useSocket();
useEffect(() => {
if (connected) {
router.replace("/sessions");
}
}, [connected]);
return (
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.container}
keyboardShouldPersistTaps="handled"
>
<Text style={styles.brand}>Pi Mobile</Text>
<Text style={styles.subtitle}>
Remote control for Pi agents on your computer. Scan the QR code from pi-mobile-hub, or enter
your computer details manually.
</Text>
<View style={styles.card}>
<Text style={styles.label}>Computer name</Text>
<TextInput
value={computerName}
onChangeText={setComputerName}
placeholder="My MacBook"
placeholderTextColor="#666"
autoCapitalize="words"
autoCorrect={false}
style={styles.input}
/>
<Text style={styles.label}>Computer IP</Text>
<TextInput
value={host}
onChangeText={setHost}
placeholder="192.168.1.10"
placeholderTextColor="#666"
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
/>
<Text style={styles.label}>Port</Text>
<TextInput
value={port}
onChangeText={setPort}
placeholder="8787"
placeholderTextColor="#666"
keyboardType="number-pad"
style={styles.input}
/>
<Text style={styles.label}>Pairing code</Text>
<TextInput
value={token}
onChangeText={setToken}
placeholder="From pi-mobile-hub terminal"
placeholderTextColor="#666"
autoCapitalize="none"
autoCorrect={false}
style={styles.input}
/>
{error ? <Text style={styles.error}>{error}</Text> : null}
<Pressable
style={[styles.button, connecting && styles.buttonDisabled]}
disabled={connecting || !host.trim()}
onPress={() => void connect()}
>
{connecting ? (
<ActivityIndicator color="#0a0a0a" />
) : (
<Text style={styles.buttonText}>Connect</Text>
)}
</Pressable>
</View>
{savedComputers.length > 0 ? (
<View style={styles.savedSection}>
<Text style={styles.savedSectionTitle}>Saved computers</Text>
{savedComputers.map((computer) => (
<SavedComputerRow
key={computer.id}
computer={computer}
connecting={connecting}
onConnect={() => void connectToSavedComputer(computer)}
onRemove={() => void removeSavedComputerEntry(computer.id)}
/>
))}
</View>
) : (
<Text style={styles.savedHint}>
Successful connections are saved here so you can reconnect with one tap.
</Text>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
scroll: {
flex: 1,
},
container: {
padding: 24,
gap: 16,
paddingBottom: 40,
},
brand: {
fontSize: 36,
fontWeight: "700",
color: "#f5f5f5",
marginTop: 24,
},
subtitle: {
color: "#a3a3a3",
fontSize: 16,
lineHeight: 22,
},
card: {
marginTop: 24,
gap: 12,
},
label: {
color: "#d4d4d4",
fontSize: 14,
fontWeight: "600",
},
input: {
backgroundColor: "#171717",
borderWidth: 1,
borderColor: "#262626",
borderRadius: 12,
color: "#f5f5f5",
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 16,
},
button: {
marginTop: 8,
backgroundColor: "#f5f5f5",
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
minHeight: 48,
},
buttonDisabled: {
opacity: 0.7,
},
buttonText: {
color: "#0a0a0a",
fontSize: 16,
fontWeight: "700",
},
error: {
color: "#f87171",
fontSize: 14,
},
savedSection: {
marginTop: 8,
gap: 10,
},
savedSectionTitle: {
color: colors.textSecondary,
fontSize: 13,
fontWeight: "700",
textTransform: "uppercase",
letterSpacing: 0.6,
fontFamily: monoFont,
},
savedRow: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.surface,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
overflow: "hidden",
},
savedRowMain: {
flex: 1,
paddingHorizontal: 14,
paddingVertical: 12,
gap: 2,
},
savedRowPressed: {
backgroundColor: colors.surfaceDeep,
},
savedName: {
color: colors.text,
fontSize: 16,
fontWeight: "600",
},
savedAddress: {
color: colors.textSecondary,
fontSize: 13,
fontFamily: monoFont,
},
removeButton: {
paddingHorizontal: 16,
paddingVertical: 12,
alignSelf: "stretch",
justifyContent: "center",
borderLeftWidth: 1,
borderLeftColor: colors.border,
},
removeButtonPressed: {
backgroundColor: colors.surfaceDeep,
},
removeButtonText: {
color: colors.textMuted,
fontSize: 22,
fontWeight: "300",
lineHeight: 22,
},
savedHint: {
marginTop: 8,
color: colors.textMuted,
fontSize: 14,
lineHeight: 20,
},
});