mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-15 04:40:40 -07:00
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.
57 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|