mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-17 13:50:39 -07:00
Exposes the existing remote server mode through a checkbox in Server Connection settings. When enabled, the server binds to 0.0.0.0 instead of 127.0.0.1, making it accessible from other devices on the network. The plumbing already existed (Rust sidecar passes --host 0.0.0.0 when remote=true, serverStore has mode state, Python backend accepts --host), but the UI hardcoded startServer(false). This wires it up. Closes #104
179 lines
6.6 KiB
TypeScript
179 lines
6.6 KiB
TypeScript
import { RouterProvider } from '@tanstack/react-router';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import voiceboxLogo from '@/assets/voicebox-logo.png';
|
|
import ShinyText from '@/components/ShinyText';
|
|
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
|
|
import { useAutoUpdater } from '@/hooks/useAutoUpdater';
|
|
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
|
import { cn } from '@/lib/utils/cn';
|
|
import { usePlatform } from '@/platform/PlatformContext';
|
|
import { router } from '@/router';
|
|
import { useServerStore } from '@/stores/serverStore';
|
|
|
|
const LOADING_MESSAGES = [
|
|
'Warming up tensors...',
|
|
'Calibrating synthesizer engine...',
|
|
'Initializing voice models...',
|
|
'Loading neural networks...',
|
|
'Preparing audio pipelines...',
|
|
'Optimizing waveform generators...',
|
|
'Tuning frequency analyzers...',
|
|
'Building voice embeddings...',
|
|
'Configuring text-to-speech cores...',
|
|
'Syncing audio buffers...',
|
|
'Establishing model connections...',
|
|
'Preprocessing training data...',
|
|
'Validating voice samples...',
|
|
'Compiling inference engines...',
|
|
'Mapping phoneme sequences...',
|
|
'Aligning prosody parameters...',
|
|
'Activating speech synthesis...',
|
|
'Fine-tuning acoustic models...',
|
|
'Preparing voice cloning matrices...',
|
|
'Initializing Qwen TTS framework...',
|
|
];
|
|
|
|
function App() {
|
|
const platform = usePlatform();
|
|
const [serverReady, setServerReady] = useState(false);
|
|
const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
|
|
const serverStartingRef = useRef(false);
|
|
|
|
// Automatically check for app updates on startup and show toast notifications
|
|
useAutoUpdater({ checkOnMount: true, showToast: true });
|
|
|
|
// Sync stored setting to Rust on startup
|
|
useEffect(() => {
|
|
if (platform.metadata.isTauri) {
|
|
const keepRunning = useServerStore.getState().keepServerRunningOnClose;
|
|
platform.lifecycle.setKeepServerRunning(keepRunning).catch((error) => {
|
|
console.error('Failed to sync initial setting to Rust:', error);
|
|
});
|
|
}
|
|
// Empty dependency array - platform is stable from context, only run once
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [platform.metadata.isTauri, platform.lifecycle]);
|
|
|
|
// Setup lifecycle callbacks
|
|
useEffect(() => {
|
|
platform.lifecycle.onServerReady = () => {
|
|
setServerReady(true);
|
|
};
|
|
// Empty dependency array - platform is stable from context, only run once
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [platform.lifecycle]);
|
|
|
|
// Setup window close handler and auto-start server when running in Tauri (production only)
|
|
useEffect(() => {
|
|
if (!platform.metadata.isTauri) {
|
|
setServerReady(true); // Web assumes server is running
|
|
return;
|
|
}
|
|
|
|
// Setup window close handler to check setting and stop server if needed
|
|
// This works in both dev and prod, but will only stop server if it was started by the app
|
|
platform.lifecycle.setupWindowCloseHandler().catch((error) => {
|
|
console.error('Failed to setup window close handler:', error);
|
|
});
|
|
|
|
// Only auto-start server in production mode
|
|
// In dev mode, user runs server separately
|
|
if (!import.meta.env?.PROD) {
|
|
console.log('Dev mode: Skipping auto-start of server (run it separately)');
|
|
setServerReady(true); // Mark as ready so UI doesn't show loading screen
|
|
// Mark that server was not started by app (so we don't try to stop it on close)
|
|
// @ts-expect-error - adding property to window
|
|
window.__voiceboxServerStartedByApp = false;
|
|
return;
|
|
}
|
|
|
|
// Auto-start server in production
|
|
if (serverStartingRef.current) {
|
|
return;
|
|
}
|
|
|
|
serverStartingRef.current = true;
|
|
const isRemote = useServerStore.getState().mode === 'remote';
|
|
console.log(`Production mode: Starting bundled server... (remote: ${isRemote})`);
|
|
|
|
platform.lifecycle
|
|
.startServer(isRemote)
|
|
.then((serverUrl) => {
|
|
console.log('Server is ready at:', serverUrl);
|
|
// Update the server URL in the store with the dynamically assigned port
|
|
useServerStore.getState().setServerUrl(serverUrl);
|
|
setServerReady(true);
|
|
// Mark that we started the server (so we know to stop it on close)
|
|
// @ts-expect-error - adding property to window
|
|
window.__voiceboxServerStartedByApp = true;
|
|
})
|
|
.catch((error) => {
|
|
console.error('Failed to auto-start server:', error);
|
|
serverStartingRef.current = false;
|
|
// @ts-expect-error - adding property to window
|
|
window.__voiceboxServerStartedByApp = false;
|
|
});
|
|
|
|
// Cleanup: stop server on actual unmount (not StrictMode remount)
|
|
// Note: Window close is handled separately in Tauri Rust code
|
|
return () => {
|
|
// Window close event handles server shutdown based on setting
|
|
serverStartingRef.current = false;
|
|
};
|
|
// Empty dependency array - platform is stable from context, only run once
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [platform.metadata.isTauri, platform.lifecycle]);
|
|
|
|
// Cycle through loading messages every 3 seconds
|
|
useEffect(() => {
|
|
if (!platform.metadata.isTauri || serverReady) {
|
|
return;
|
|
}
|
|
|
|
const interval = setInterval(() => {
|
|
setLoadingMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length);
|
|
}, 3000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [serverReady, platform.metadata.isTauri]);
|
|
|
|
// Show loading screen while server is starting in Tauri
|
|
if (platform.metadata.isTauri && !serverReady) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'min-h-screen bg-background flex items-center justify-center',
|
|
TOP_SAFE_AREA_PADDING,
|
|
)}
|
|
>
|
|
<TitleBarDragRegion />
|
|
<div className="text-center space-y-6">
|
|
<div className="flex justify-center relative">
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<div className="w-48 h-48 rounded-full bg-accent/20 blur-3xl" />
|
|
</div>
|
|
<img
|
|
src={voiceboxLogo}
|
|
alt="Voicebox"
|
|
className="w-48 h-48 object-contain animate-fade-in-scale relative z-10"
|
|
/>
|
|
</div>
|
|
<div className="animate-fade-in-delayed">
|
|
<ShinyText
|
|
text={LOADING_MESSAGES[loadingMessageIndex]}
|
|
className="text-lg font-medium text-muted-foreground"
|
|
speed={2}
|
|
color="hsl(var(--muted-foreground))"
|
|
shineColor="hsl(var(--foreground))"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <RouterProvider router={router} />;
|
|
}
|
|
|
|
export default App;
|