Files
voicebox/mobile/app/(tabs)/_layout.tsx
T
James Pine f4d21504e3 Mobile companion app + paired-device backend
New iOS-first companion (Expo SDK 54 + NativeWind v4) with three tabs:
Captures (the hero — floating gold mic, live mic-meter waveform,
expand-row playback), Generate (profile picker + speak + autoplay +
recent), and Voices (searchable profile list).

Pairing (V0): backend mints a one-time token, mobile scans/pastes the
voicebox:// URL, server returns a long-lived bearer it stores only as a
SHA-256 hash. Bearer-or-loopback auth applied to every user-data router
so binding 0.0.0.0 doesn't leak existing endpoints. Loopback callers
(the desktop app) keep their friction-free access.

Desktop Settings → Mobile pane: live host picker (LAN / Tailscale auto-
detected via the App-bundle binary path on macOS), QR rendering,
5-minute expiry countdown, copyable URL fallback, paired-device list
with revoke. Auto-closes when a new device pairs.

just dev now binds the backend to 0.0.0.0 so paired phones can reach
it — and just setup-python pins mlx-audio==0.4.1 + mlx-lm so fresh
Apple Silicon worktrees get a working STT path on first install.
2026-04-25 17:09:32 -07:00

57 lines
1.7 KiB
TypeScript

import { Redirect, Tabs } from 'expo-router';
import { Mic, PenTool, Users } from 'lucide-react-native';
import { View } from 'react-native';
import { colors } from '@/lib/colors';
import { useSession } from '@/lib/session';
export default function TabsLayout() {
const { session, hydrated } = useSession();
// Wait for SecureStore hydration before deciding where to send the user.
// Without this we'd briefly render the tabs against a null session and
// every authenticated query would fire with no bearer.
if (!hydrated) {
return <View style={{ flex: 1, backgroundColor: colors.background }} />;
}
if (!session) return <Redirect href="/welcome" />;
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle: {
backgroundColor: colors.card,
borderTopColor: colors.border,
borderTopWidth: 1,
},
tabBarActiveTintColor: colors.accent,
tabBarInactiveTintColor: colors.mutedForeground,
tabBarLabelStyle: { fontSize: 11, fontWeight: '600' },
sceneStyle: { backgroundColor: colors.background },
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Captures',
tabBarIcon: ({ color, size }) => <Mic size={size - 2} color={color} />,
}}
/>
<Tabs.Screen
name="generate"
options={{
title: 'Generate',
tabBarIcon: ({ color, size }) => <PenTool size={size - 2} color={color} />,
}}
/>
<Tabs.Screen
name="voices"
options={{
title: 'Voices',
tabBarIcon: ({ color, size }) => <Users size={size - 2} color={color} />,
}}
/>
</Tabs>
);
}