mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 06:10:43 -07:00
Refactor Tauri Integration to Use Platform Context
- Replaced direct Tauri API calls with a unified platform context across multiple components, enhancing code maintainability and readability. - Removed the deprecated tauri.ts file, consolidating platform-related logic into the new PlatformContext. - Updated components such as App, AudioPlayer, and ServerSettings to utilize the new platform context for lifecycle management and server interactions. - Improved platform detection and handling for audio playback and system audio capture functionalities. - Ensured consistent error handling and user feedback across the application when interacting with platform-specific features.
This commit is contained in:
+6
-2
@@ -5,6 +5,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from '@/App';
|
||||
// Import CSS from app directory using alias so Tailwind can scan the source files
|
||||
import '@/index.css';
|
||||
import { PlatformProvider } from '@/platform/PlatformContext';
|
||||
import { tauriPlatform } from './platform';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@@ -20,8 +22,10 @@ const queryClient = new QueryClient({
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<App />
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
<PlatformProvider platform={tauriPlatform}>
|
||||
<App />
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</PlatformProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { PlatformAudio, AudioDevice } from '@/platform/types';
|
||||
|
||||
export const tauriAudio: PlatformAudio = {
|
||||
isSystemAudioSupported(): boolean {
|
||||
// This will be checked dynamically via invoke
|
||||
return true; // Tauri supports it, but actual support depends on platform
|
||||
},
|
||||
|
||||
async startSystemAudioCapture(maxDurationSecs: number): Promise<void> {
|
||||
await invoke('start_system_audio_capture', {
|
||||
maxDurationSecs,
|
||||
});
|
||||
},
|
||||
|
||||
async stopSystemAudioCapture(): Promise<Blob> {
|
||||
const base64Data = await invoke<string>('stop_system_audio_capture');
|
||||
|
||||
// Convert base64 to Blob
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return new Blob([bytes], { type: 'audio/wav' });
|
||||
},
|
||||
|
||||
async listOutputDevices(): Promise<AudioDevice[]> {
|
||||
return await invoke<AudioDevice[]>('list_audio_output_devices');
|
||||
},
|
||||
|
||||
async playToDevices(audioData: Uint8Array, deviceIds: string[]): Promise<void> {
|
||||
await invoke('play_audio_to_devices', {
|
||||
audioData: Array.from(audioData),
|
||||
deviceIds,
|
||||
});
|
||||
},
|
||||
|
||||
stopPlayback(): void {
|
||||
invoke('stop_audio_playback').catch((error) => {
|
||||
console.error('Failed to stop audio playback:', error);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { PlatformFilesystem, FileFilter } from '@/platform/types';
|
||||
|
||||
export const tauriFilesystem: PlatformFilesystem = {
|
||||
async saveFile(filename: string, blob: Blob, filters?: FileFilter[]) {
|
||||
try {
|
||||
const { save } = await import('@tauri-apps/plugin-dialog');
|
||||
const filePath = await save({
|
||||
defaultPath: filename,
|
||||
filters: filters || [],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
const { writeBinaryFile } = await import('@tauri-apps/plugin-fs');
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
await writeBinaryFile(filePath, new Uint8Array(arrayBuffer));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to use Tauri dialog, falling back to browser download:', error);
|
||||
// Fall back to browser download if Tauri dialog fails
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Platform } from '@/platform/types';
|
||||
import { tauriFilesystem } from './filesystem';
|
||||
import { tauriUpdater } from './updater';
|
||||
import { tauriAudio } from './audio';
|
||||
import { tauriLifecycle } from './lifecycle';
|
||||
import { tauriMetadata } from './metadata';
|
||||
|
||||
export const tauriPlatform: Platform = {
|
||||
filesystem: tauriFilesystem,
|
||||
updater: tauriUpdater,
|
||||
audio: tauriAudio,
|
||||
lifecycle: tauriLifecycle,
|
||||
metadata: tauriMetadata,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, emit } from '@tauri-apps/api/event';
|
||||
import type { PlatformLifecycle } from '@/platform/types';
|
||||
|
||||
class TauriLifecycle implements PlatformLifecycle {
|
||||
onServerReady?: () => void;
|
||||
|
||||
async startServer(remote = false): Promise<string> {
|
||||
try {
|
||||
const result = await invoke<string>('start_server', { remote });
|
||||
console.log('Server started:', result);
|
||||
this.onServerReady?.();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async stopServer(): Promise<void> {
|
||||
try {
|
||||
await invoke('stop_server');
|
||||
console.log('Server stopped');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async setKeepServerRunning(keepRunning: boolean): Promise<void> {
|
||||
try {
|
||||
await invoke('set_keep_server_running', { keepRunning });
|
||||
} catch (error) {
|
||||
console.error('Failed to set keep server running setting:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async setupWindowCloseHandler(): Promise<void> {
|
||||
try {
|
||||
// Listen for window close request from Rust
|
||||
await listen<null>('window-close-requested', async () => {
|
||||
// Import store here to avoid circular dependency
|
||||
const { useServerStore } = await import('@/stores/serverStore');
|
||||
const keepRunning = useServerStore.getState().keepServerRunningOnClose;
|
||||
|
||||
// Check if server was started by this app instance
|
||||
// @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 this.stopServer();
|
||||
} catch (error) {
|
||||
console.error('Failed to stop server on close:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit event back to Rust to allow close
|
||||
await emit('window-close-allowed');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to setup window close handler:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tauriLifecycle = new TauriLifecycle();
|
||||
@@ -0,0 +1,14 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import type { PlatformMetadata } from '@/platform/types';
|
||||
|
||||
export const tauriMetadata: PlatformMetadata = {
|
||||
async getVersion(): Promise<string> {
|
||||
try {
|
||||
return await getVersion();
|
||||
} catch (error) {
|
||||
console.error('Failed to get version:', error);
|
||||
return '0.1.0';
|
||||
}
|
||||
},
|
||||
isTauri: true,
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
import { relaunch } from '@tauri-apps/plugin-process';
|
||||
import { check, type Update } from '@tauri-apps/plugin-updater';
|
||||
import type { PlatformUpdater, UpdateStatus } from '@/platform/types';
|
||||
|
||||
// Check if we're on Windows (NSIS installer handles restart automatically)
|
||||
const isWindows = () => {
|
||||
return navigator.userAgent.includes('Windows');
|
||||
};
|
||||
|
||||
class TauriUpdater implements PlatformUpdater {
|
||||
private status: UpdateStatus = {
|
||||
checking: false,
|
||||
available: false,
|
||||
downloading: false,
|
||||
installing: false,
|
||||
readyToInstall: false,
|
||||
};
|
||||
|
||||
private update: Update | null = null;
|
||||
private subscribers: Set<(status: UpdateStatus) => void> = new Set();
|
||||
|
||||
private notifySubscribers() {
|
||||
this.subscribers.forEach((callback) => callback(this.status));
|
||||
}
|
||||
|
||||
subscribe(callback: (status: UpdateStatus) => void): () => void {
|
||||
this.subscribers.add(callback);
|
||||
// Immediately call with current status
|
||||
callback(this.status);
|
||||
return () => {
|
||||
this.subscribers.delete(callback);
|
||||
};
|
||||
}
|
||||
|
||||
getStatus(): UpdateStatus {
|
||||
return { ...this.status };
|
||||
}
|
||||
|
||||
async checkForUpdates(): Promise<void> {
|
||||
try {
|
||||
this.status = { ...this.status, checking: true, error: undefined };
|
||||
this.notifySubscribers();
|
||||
|
||||
const foundUpdate = await check();
|
||||
|
||||
if (foundUpdate?.available) {
|
||||
this.update = foundUpdate;
|
||||
this.status = {
|
||||
checking: false,
|
||||
available: true,
|
||||
version: foundUpdate.version,
|
||||
downloading: false,
|
||||
installing: false,
|
||||
readyToInstall: false,
|
||||
};
|
||||
} else {
|
||||
this.status = {
|
||||
checking: false,
|
||||
available: false,
|
||||
downloading: false,
|
||||
installing: false,
|
||||
readyToInstall: false,
|
||||
};
|
||||
}
|
||||
this.notifySubscribers();
|
||||
} catch (error) {
|
||||
this.status = {
|
||||
checking: false,
|
||||
available: false,
|
||||
downloading: false,
|
||||
installing: false,
|
||||
readyToInstall: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to check for updates',
|
||||
};
|
||||
this.notifySubscribers();
|
||||
}
|
||||
}
|
||||
|
||||
async downloadAndInstall(): Promise<void> {
|
||||
if (!this.update) return;
|
||||
|
||||
try {
|
||||
this.status = { ...this.status, downloading: true, error: undefined };
|
||||
this.notifySubscribers();
|
||||
|
||||
let downloadedBytes = 0;
|
||||
let totalBytes = 0;
|
||||
|
||||
await this.update.download((event) => {
|
||||
switch (event.event) {
|
||||
case 'Started':
|
||||
totalBytes = event.data.contentLength || 0;
|
||||
downloadedBytes = 0;
|
||||
this.status = {
|
||||
...this.status,
|
||||
downloading: true,
|
||||
totalBytes,
|
||||
downloadedBytes: 0,
|
||||
downloadProgress: 0,
|
||||
};
|
||||
this.notifySubscribers();
|
||||
break;
|
||||
case 'Progress': {
|
||||
downloadedBytes += event.data.chunkLength;
|
||||
const progress =
|
||||
totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : undefined;
|
||||
this.status = {
|
||||
...this.status,
|
||||
downloadedBytes,
|
||||
downloadProgress: progress,
|
||||
};
|
||||
this.notifySubscribers();
|
||||
break;
|
||||
}
|
||||
case 'Finished':
|
||||
this.status = {
|
||||
...this.status,
|
||||
downloading: false,
|
||||
readyToInstall: true,
|
||||
downloadProgress: 100,
|
||||
};
|
||||
this.notifySubscribers();
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.status = {
|
||||
...this.status,
|
||||
downloading: false,
|
||||
installing: false,
|
||||
readyToInstall: false,
|
||||
downloadProgress: undefined,
|
||||
downloadedBytes: undefined,
|
||||
totalBytes: undefined,
|
||||
error: error instanceof Error ? error.message : 'Failed to download update',
|
||||
};
|
||||
this.notifySubscribers();
|
||||
}
|
||||
}
|
||||
|
||||
async restartAndInstall(): Promise<void> {
|
||||
if (!this.update) return;
|
||||
|
||||
try {
|
||||
this.status = { ...this.status, installing: true, error: undefined };
|
||||
this.notifySubscribers();
|
||||
|
||||
await this.update.install();
|
||||
|
||||
// On Windows with NSIS, the installer handles the restart automatically.
|
||||
// On macOS/Linux, we need to manually relaunch.
|
||||
if (!isWindows()) {
|
||||
await relaunch();
|
||||
}
|
||||
} catch (error) {
|
||||
this.status = {
|
||||
...this.status,
|
||||
installing: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to install update',
|
||||
};
|
||||
this.notifySubscribers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tauriUpdater = new TauriUpdater();
|
||||
Reference in New Issue
Block a user