diff --git a/app/src/App.tsx b/app/src/App.tsx index 8168494a..c4cd85b7 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,17 +1,16 @@ import { useEffect, useState } from 'react'; import voiceboxLogo from '@/assets/voicebox-logo.png'; -import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer'; +import { AppFrame } from '@/components/AppFrame/AppFrame'; +import { AudioTab } from '@/components/AudioTab/AudioTab'; +import { MainEditor } from '@/components/MainEditor/MainEditor'; +import { ModelsTab } from '@/components/ModelsTab/ModelsTab'; +import { ServerTab } from '@/components/ServerTab/ServerTab'; // import { GenerationForm } from '@/components/Generation/GenerationForm'; -import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox'; -import { HistoryTable } from '@/components/History/HistoryTable'; import ShinyText from '@/components/ShinyText'; import { Sidebar } from '@/components/Sidebar'; import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; import { Toaster } from '@/components/ui/toaster'; -import { ProfileList } from '@/components/VoiceProfiles/ProfileList'; import { VoicesTab } from '@/components/VoicesTab/VoicesTab'; -import { AudioTab } from '@/components/AudioTab/AudioTab'; -import { ServerTab } from '@/components/ServerTab/ServerTab'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks'; import { @@ -21,7 +20,6 @@ import { setupWindowCloseHandler, startServer, } from '@/lib/tauri'; -import { usePlayerStore } from '@/stores/playerStore'; import { useServerStore } from '@/stores/serverStore'; // Track if server is starting to prevent duplicate starts @@ -54,7 +52,6 @@ function App() { const [activeTab, setActiveTab] = useState('main'); const [serverReady, setServerReady] = useState(false); const [loadingMessageIndex, setLoadingMessageIndex] = useState(0); - const audioUrl = usePlayerStore((state) => state.audioUrl); // Monitor active downloads/generations and show toasts for them const activeDownloads = useRestoreActiveTasks(); @@ -169,48 +166,21 @@ function App() { } return ( -
- +
- {activeTab === 'main' && ( - // Main view: Profiles top left, Generator bottom left, History right -
- {/* Left Column */} -
- {/* Profiles - Top Left */} -
- -
- - {/* Generator - Bottom Left */} - {/*
- -
*/} -
- - {/* Right Column - History */} -
- -
- - {/* Floating Generate Box */} - -
- )} + {activeTab === 'main' && } {activeTab === 'voices' && } {activeTab === 'audio' && } {activeTab === 'server' && } + {activeTab === 'models' && }
- {/* Audio Player - always visible except on server tab */} - {activeTab !== 'server' && } - {/* Show download toasts for any active downloads (from anywhere) */} {activeDownloads.map((download) => { const displayName = MODEL_DISPLAY_NAMES[download.model_name] || download.model_name; @@ -224,7 +194,7 @@ function App() { })} -
+ ); } diff --git a/app/src/components/AppFrame/AppFrame.tsx b/app/src/components/AppFrame/AppFrame.tsx new file mode 100644 index 00000000..6b72b3e3 --- /dev/null +++ b/app/src/components/AppFrame/AppFrame.tsx @@ -0,0 +1,16 @@ +import { TitleBarDragRegion } from '@/components/TitleBarDragRegion'; +import { AudioPlayer } from '@/components/AudioPlayer/AudioPlayer'; + +interface AppFrameProps { + children: React.ReactNode; +} + +export function AppFrame({ children }: AppFrameProps) { + return ( +
+ + {children} + +
+ ); +} diff --git a/app/src/components/AudioPlayer/AudioPlayer.tsx b/app/src/components/AudioPlayer/AudioPlayer.tsx index 185a232f..742f9361 100644 --- a/app/src/components/AudioPlayer/AudioPlayer.tsx +++ b/app/src/components/AudioPlayer/AudioPlayer.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api/core'; -import { Pause, Play, Repeat, Volume2, VolumeX } from 'lucide-react'; +import { Pause, Play, Repeat, Volume2, VolumeX, X } from 'lucide-react'; import { useEffect, useMemo, useRef, useState } from 'react'; import WaveSurfer from 'wavesurfer.js'; import { Button } from '@/components/ui/button'; @@ -28,6 +28,7 @@ export function AudioPlayer() { setVolume, toggleLoop, clearRestartFlag, + reset, } = usePlayerStore(); // Check if profile has assigned channels (for native audio routing) @@ -234,7 +235,7 @@ export function AudioPlayer() { runtimeChannels ) { console.log('Attempting native audio playback...'); - + // Stop any existing native playback first if (isUsingNativePlaybackRef.current) { try { @@ -244,7 +245,7 @@ export function AudioPlayer() { console.error('Failed to stop existing playback:', error); } } - + try { // Collect all device IDs from assigned channels const assignedChannels = runtimeChannels.filter((ch: any) => @@ -267,7 +268,12 @@ export function AudioPlayer() { const currentVolume = usePlayerStore.getState().volume; mediaElement.volume = currentVolume; mediaElement.muted = false; - console.log('WaveSurfer unmuted for normal playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted); + console.log( + 'WaveSurfer unmuted for normal playback - volume:', + mediaElement.volume, + 'muted:', + mediaElement.muted, + ); } } else { const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids); @@ -288,24 +294,29 @@ export function AudioPlayer() { deviceIds: deviceIds, }); console.log('play_audio_to_devices completed successfully, result:', result); - + // Mark that we're using native playback isUsingNativePlaybackRef.current = true; - + // Mute WaveSurfer's audio element to prevent UI audio output // Keep WaveSurfer running for visualization const mediaElement = wavesurfer.getMediaElement(); if (mediaElement) { mediaElement.volume = 0; mediaElement.muted = true; - console.log('WaveSurfer muted for native playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted); + console.log( + 'WaveSurfer muted for native playback - volume:', + mediaElement.volume, + 'muted:', + mediaElement.muted, + ); } - + // Start WaveSurfer playback for visualization (muted) wavesurfer.play().catch((error) => { console.error('Failed to start WaveSurfer visualization:', error); }); - + setIsPlaying(true); console.log('Auto-playing via native audio routing - SUCCESS'); return; @@ -329,7 +340,12 @@ export function AudioPlayer() { const currentVolume = usePlayerStore.getState().volume; mediaElement.volume = currentVolume; mediaElement.muted = false; - console.log('WaveSurfer unmuted after native playback failure - volume:', mediaElement.volume, 'muted:', mediaElement.muted); + console.log( + 'WaveSurfer unmuted after native playback failure - volume:', + mediaElement.volume, + 'muted:', + mediaElement.muted, + ); } // Fall through to WaveSurfer playback } @@ -342,7 +358,12 @@ export function AudioPlayer() { const currentVolume = usePlayerStore.getState().volume; mediaElement.volume = currentVolume; mediaElement.muted = false; - console.log('WaveSurfer unmuted for normal playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted); + console.log( + 'WaveSurfer unmuted for normal playback - volume:', + mediaElement.volume, + 'muted:', + mediaElement.muted, + ); } } @@ -373,7 +394,12 @@ export function AudioPlayer() { const currentVolume = usePlayerStore.getState().volume; mediaElement.volume = currentVolume; mediaElement.muted = false; - console.log('Playing (normal mode) - volume:', mediaElement.volume, 'muted:', mediaElement.muted); + console.log( + 'Playing (normal mode) - volume:', + mediaElement.volume, + 'muted:', + mediaElement.muted, + ); } } }); @@ -497,7 +523,7 @@ export function AudioPlayer() { } })(); } - + // Reset native playback flag when loading new audio // Also unmute WaveSurfer if it was muted if (isUsingNativePlaybackRef.current) { @@ -667,17 +693,17 @@ export function AudioPlayer() { wavesurferRef.current.pause(); return; } - + // Play: trigger native playback try { // Stop any existing native playback first try { await invoke('stop_audio_playback'); - } catch (error) { + } catch (_error) { // Ignore errors when stopping (might not be playing) console.log('No existing playback to stop'); } - + // Collect all device IDs from assigned channels const assignedChannels = channels.filter((ch) => profileChannels.channel_ids.includes(ch.id), @@ -697,21 +723,21 @@ export function AudioPlayer() { // Mark that we're using native playback isUsingNativePlaybackRef.current = true; - + // Mute WaveSurfer and start it for visualization const mediaElement = wavesurferRef.current.getMediaElement(); if (mediaElement) { mediaElement.volume = 0; mediaElement.muted = true; } - + // Start WaveSurfer for visualization (muted) wavesurferRef.current.play().catch((error) => { console.error('Failed to start WaveSurfer visualization:', error); setIsPlaying(false); setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`); }); - + return; } } catch (error) { @@ -733,7 +759,7 @@ export function AudioPlayer() { mediaElement.volume = volume; } } - + wavesurferRef.current.play().catch((error) => { console.error('Failed to play:', error); setIsPlaying(false); @@ -752,6 +778,22 @@ export function AudioPlayer() { setVolume(value[0] / 100); }; + const handleClose = () => { + // Stop any native playback + if (isUsingNativePlaybackRef.current && isTauri()) { + invoke('stop_audio_playback').catch((error) => { + console.error('Failed to stop native playback:', error); + }); + } + // Stop WaveSurfer + if (wavesurferRef.current) { + wavesurferRef.current.pause(); + wavesurferRef.current.seekTo(0); + } + // Reset player state + reset(); + }; + // Don't render if no audio if (!audioUrl) { return null; @@ -832,6 +874,17 @@ export function AudioPlayer() { className="flex-1" /> + + {/* Close Button */} + diff --git a/app/src/components/History/HistoryTable.tsx b/app/src/components/History/HistoryTable.tsx index 79faa64d..b333318f 100644 --- a/app/src/components/History/HistoryTable.tsx +++ b/app/src/components/History/HistoryTable.tsx @@ -33,7 +33,7 @@ import { usePlayerStore } from '@/stores/playerStore'; // NEW ALTERNATE HISTORY VIEW - FIXED HEIGHT ROWS export function HistoryTable() { - const [page, setPage] = useState(0); + const [page, _setPage] = useState(0); const [isScrolled, setIsScrolled] = useState(false); const scrollRef = useRef(null); const fileInputRef = useRef(null); @@ -143,7 +143,7 @@ export function HistoryTable() { const history = historyData?.items || []; const total = historyData?.total || 0; - const hasMore = history.length === limit && (page + 1) * limit < total; + const _hasMore = history.length === limit && (page + 1) * limit < total; return (
@@ -176,8 +176,8 @@ export function HistoryTable() {
{history.map((gen) => { @@ -242,7 +242,9 @@ export function HistoryTable() { - handlePlay(gen.id, gen.text, gen.profile_id)}> + handlePlay(gen.id, gen.text, gen.profile_id)} + > Play @@ -275,24 +277,6 @@ export function HistoryTable() { ); })}
- - {(total > limit || page > 0) && ( -
- -
- Page {page + 1} • {total} total -
- -
- )} )} diff --git a/app/src/components/MainEditor/MainEditor.tsx b/app/src/components/MainEditor/MainEditor.tsx new file mode 100644 index 00000000..fd766265 --- /dev/null +++ b/app/src/components/MainEditor/MainEditor.tsx @@ -0,0 +1,34 @@ +import { ProfileList } from '@/components/VoiceProfiles/ProfileList'; +import { HistoryTable } from '@/components/History/HistoryTable'; +import { FloatingGenerateBox } from '@/components/Generation/FloatingGenerateBox'; +import { usePlayerStore } from '@/stores/playerStore'; + +export function MainEditor() { + const audioUrl = usePlayerStore((state) => state.audioUrl); + + return ( + // Main view: Profiles top left, Generator bottom left, History right +
+ {/* Left Column */} +
+ {/* Profiles - Top Left */} +
+ +
+ + {/* Generator - Bottom Left */} + {/*
+ +
*/} +
+ + {/* Right Column - History */} +
+ +
+ + {/* Floating Generate Box */} + +
+ ); +} diff --git a/app/src/components/ModelsTab/ModelsTab.tsx b/app/src/components/ModelsTab/ModelsTab.tsx new file mode 100644 index 00000000..3c6ebda7 --- /dev/null +++ b/app/src/components/ModelsTab/ModelsTab.tsx @@ -0,0 +1,9 @@ +import { ModelManagement } from '@/components/ServerSettings/ModelManagement'; + +export function ModelsTab() { + return ( +
+ +
+ ); +} diff --git a/app/src/components/ServerTab/ServerTab.tsx b/app/src/components/ServerTab/ServerTab.tsx index 78e556ae..6534e3c4 100644 --- a/app/src/components/ServerTab/ServerTab.tsx +++ b/app/src/components/ServerTab/ServerTab.tsx @@ -1,5 +1,4 @@ import { ConnectionForm } from '@/components/ServerSettings/ConnectionForm'; -import { ModelManagement } from '@/components/ServerSettings/ModelManagement'; import { ServerStatus } from '@/components/ServerSettings/ServerStatus'; import { UpdateStatus } from '@/components/ServerSettings/UpdateStatus'; import { isTauri } from '@/lib/tauri'; @@ -12,7 +11,6 @@ export function ServerTab() {
{isTauri() && } -
Created by{' '}