diff --git a/AppIcon.icon/Assets/Voicebox.png b/AppIcon.icon/Assets/Voicebox.png new file mode 100644 index 00000000..7a630b05 Binary files /dev/null and b/AppIcon.icon/Assets/Voicebox.png differ diff --git a/AppIcon.icon/icon.json b/AppIcon.icon/icon.json new file mode 100644 index 00000000..dece1804 --- /dev/null +++ b/AppIcon.icon/icon.json @@ -0,0 +1,36 @@ +{ + "fill" : "automatic", + "groups" : [ + { + "layers" : [ + { + "blend-mode" : "normal", + "glass" : true, + "image-name" : "Voicebox.png", + "name" : "Voicebox", + "position" : { + "scale" : 0.37, + "translation-in-points" : [ + -0.12338405356129378, + 297.27705195772353 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index 0a885860..975b1447 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -437,7 +437,7 @@ projects ## 💡 Development Workflow -1. **Start backend**: `bun run dev:backend` (or via Tauri) +1. **Start backend**: `bun run dev:server` (or via Tauri) 2. **Start frontend**: `bun run dev` (Tauri) or `bun run dev:web` (web) 3. **Generate API client**: `bun run generate:api` (after backend changes) 4. **Build server binary**: `bun run build:server` (for Tauri bundling) diff --git a/Icon Exports/Icon-iOS-ClearDark-1024x1024@1x.png b/Icon Exports/Icon-iOS-ClearDark-1024x1024@1x.png new file mode 100644 index 00000000..bb040146 Binary files /dev/null and b/Icon Exports/Icon-iOS-ClearDark-1024x1024@1x.png differ diff --git a/Icon Exports/Icon-iOS-ClearLight-1024x1024@1x.png b/Icon Exports/Icon-iOS-ClearLight-1024x1024@1x.png new file mode 100644 index 00000000..90b06cf2 Binary files /dev/null and b/Icon Exports/Icon-iOS-ClearLight-1024x1024@1x.png differ diff --git a/Icon Exports/Icon-iOS-Dark-1024x1024@1x.png b/Icon Exports/Icon-iOS-Dark-1024x1024@1x.png new file mode 100644 index 00000000..abb7034f Binary files /dev/null and b/Icon Exports/Icon-iOS-Dark-1024x1024@1x.png differ diff --git a/Icon Exports/Icon-iOS-Default-1024x1024@1x.png b/Icon Exports/Icon-iOS-Default-1024x1024@1x.png new file mode 100644 index 00000000..c058745c Binary files /dev/null and b/Icon Exports/Icon-iOS-Default-1024x1024@1x.png differ diff --git a/Icon Exports/Icon-iOS-TintedDark-1024x1024@1x.png b/Icon Exports/Icon-iOS-TintedDark-1024x1024@1x.png new file mode 100644 index 00000000..c24218b1 Binary files /dev/null and b/Icon Exports/Icon-iOS-TintedDark-1024x1024@1x.png differ diff --git a/Icon Exports/Icon-iOS-TintedLight-1024x1024@1x.png b/Icon Exports/Icon-iOS-TintedLight-1024x1024@1x.png new file mode 100644 index 00000000..912ff991 Binary files /dev/null and b/Icon Exports/Icon-iOS-TintedLight-1024x1024@1x.png differ diff --git a/app/src/App.tsx b/app/src/App.tsx index ee0820d0..c3c7cb3c 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -16,45 +16,57 @@ function App() { const [activeTab, setActiveTab] = useState('profiles'); const [serverReady, setServerReady] = useState(false); - // Setup window close handler and auto-start server when running in Tauri + // Setup window close handler and auto-start server when running in Tauri (production only) useEffect(() => { if (!isTauri()) { 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 setupWindowCloseHandler().catch((error) => { console.error('Failed to setup window close handler:', error); }); - // Auto-start server + // 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 (serverStarting) { return; } serverStarting = true; - console.log('Running in Tauri, starting bundled server...'); + console.log('Production mode: Starting bundled server...'); startServer(false) .then(() => { console.log('Server is ready'); 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); serverStarting = 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 () => { - // In dev mode, React StrictMode causes remounts, so we skip cleanup - // In production, window close event handles server shutdown based on setting - if (import.meta.env?.PROD) { - // Only stop if setting says to stop (handled by window close event) - // This cleanup is mainly for React remounts in dev mode - serverStarting = false; - } + // Window close event handles server shutdown based on setting + serverStarting = false; }; }, []); diff --git a/app/src/components/ServerSettings/ConnectionForm.tsx b/app/src/components/ServerSettings/ConnectionForm.tsx index 75cfff3e..9ce7d605 100644 --- a/app/src/components/ServerSettings/ConnectionForm.tsx +++ b/app/src/components/ServerSettings/ConnectionForm.tsx @@ -1,5 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; +import { useEffect } from 'react'; import * as z from 'zod'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; @@ -37,8 +38,16 @@ export function ConnectionForm() { }, }); + // Sync form with store when serverUrl changes externally + useEffect(() => { + form.reset({ serverUrl }); + }, [serverUrl, form]); + + const { isDirty } = form.formState; + function onSubmit(data: ConnectionFormValues) { setServerUrl(data.serverUrl); + form.reset(data); // Reset form state after successful submission toast({ title: 'Server URL updated', description: `Connected to ${data.serverUrl}`, @@ -68,7 +77,9 @@ export function ConnectionForm() { )} /> - + {isDirty && ( + + )} diff --git a/app/src/lib/tauri.ts b/app/src/lib/tauri.ts index 39cf7fc7..792f7a7e 100644 --- a/app/src/lib/tauri.ts +++ b/app/src/lib/tauri.ts @@ -62,8 +62,14 @@ export async function setupWindowCloseHandler(): Promise { const { useServerStore } = await import('@/stores/serverStore'); const keepRunning = useServerStore.getState().keepServerRunningOnClose; - if (!keepRunning) { - // Stop server before closing + // Check if server was started by this app instance + // In dev mode, serverStartedByApp will be false, so we won't try to stop a separately-run server + // We need to access the module-level variable - this is a bit hacky but works + // @ts-expect-error - accessing module-level variable from another module + const serverStartedByApp = window.__voiceboxServerStartedByApp ?? false; + + if (!keepRunning && serverStartedByApp) { + // Stop server before closing (only if we started it) try { await stopServer(); } catch (error) { diff --git a/backend/main.py b/backend/main.py index 06c8298c..766357d4 100644 --- a/backend/main.py +++ b/backend/main.py @@ -706,6 +706,7 @@ async def trigger_model_download(request: models.ModelDownloadRequest): async def startup_event(): """Run on application startup.""" print("voicebox API starting up...") + database.init_db() print(f"Database initialized at {database._db_path}") print(f"GPU available: {torch.cuda.is_available()}") diff --git a/package.json b/package.json index 876ad0e3..a3f73730 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "dev": "cd tauri && bun run tauri dev", "dev:web": "cd web && bun run dev", - "dev:backend": "source backend/venv/bin/activate && uvicorn backend.main:app --reload --port 8000", + "dev:server": "source backend/venv/bin/activate && uvicorn backend.main:app --reload --port 8000", "build": "cd tauri && bun run tauri build", "build:web": "cd web && bun run build", "generate:api": "./scripts/generate-api.sh", diff --git a/tauri/src-tauri/Info.plist b/tauri/src-tauri/Info.plist new file mode 100644 index 00000000..f3ca2e7d --- /dev/null +++ b/tauri/src-tauri/Info.plist @@ -0,0 +1,10 @@ + + + + + CFBundleIconFile + voicebox + CFBundleIconName + voicebox + + diff --git a/tauri/src-tauri/build.rs b/tauri/src-tauri/build.rs index d860e1e6..33576712 100644 --- a/tauri/src-tauri/build.rs +++ b/tauri/src-tauri/build.rs @@ -1,3 +1,56 @@ +use std::process::Command; + fn main() { + // Compile macOS Liquid Glass icon + #[cfg(target_os = "macos")] + { + let project_root = env!("CARGO_MANIFEST_DIR"); + // AppIcon.icon is in the root, two levels up from src-tauri + let icon_source = format!("{}/../../AppIcon.icon", project_root); + let gen_dir = format!("{}/gen", project_root); + + std::fs::create_dir_all(&gen_dir).expect("Failed to create gen directory"); + + if std::path::Path::new(&icon_source).exists() { + println!("cargo:rerun-if-changed={}", icon_source); + println!("cargo:rerun-if-changed={}/icon.json", icon_source); + println!("cargo:rerun-if-changed={}/Assets", icon_source); + + let partial_plist = format!("{}/partial.plist", gen_dir); + let output = Command::new("xcrun") + .args([ + "actool", + "--compile", &gen_dir, + "--output-format", "human-readable-text", + "--output-partial-info-plist", &partial_plist, + "--app-icon", "voicebox", + "--include-all-app-icons", + "--target-device", "mac", + "--minimum-deployment-target", "11.0", + "--platform", "macosx", + &icon_source, + ]) + .output(); + + match output { + Ok(output) => { + if !output.status.success() { + eprintln!("actool stderr: {}", String::from_utf8_lossy(&output.stderr)); + eprintln!("actool stdout: {}", String::from_utf8_lossy(&output.stdout)); + panic!("actool failed to compile icon"); + } + println!("Successfully compiled icon to {}", gen_dir); + } + Err(e) => { + eprintln!("Failed to execute xcrun actool: {}", e); + eprintln!("Make sure you have Xcode Command Line Tools installed"); + panic!("Icon compilation failed"); + } + } + } else { + println!("cargo:warning=Icon source not found at {}, skipping icon compilation", icon_source); + } + } + tauri_build::build() } diff --git a/tauri/src-tauri/gen/Assets.car b/tauri/src-tauri/gen/Assets.car new file mode 100644 index 00000000..6dd5f3dd Binary files /dev/null and b/tauri/src-tauri/gen/Assets.car differ diff --git a/tauri/src-tauri/gen/partial.plist b/tauri/src-tauri/gen/partial.plist new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/tauri/src-tauri/gen/partial.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/tauri/src-tauri/tauri.conf.json b/tauri/src-tauri/tauri.conf.json index 88270043..1db2972e 100644 --- a/tauri/src-tauri/tauri.conf.json +++ b/tauri/src-tauri/tauri.conf.json @@ -24,8 +24,12 @@ ], "macOS": { "frameworks": [], - "minimumSystemVersion": "10.15" - } + "minimumSystemVersion": "11.0", + "infoPlist": "Info.plist" + }, + "resources": [ + "gen/**/*" + ] }, "app": { "security": {