mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-18 06:10:43 -07:00
Refactor UI components for improved layout consistency and responsiveness
- Updated App and AudioTab components to utilize TOP_SAFE_AREA_PADDING and BOTTOM_SAFE_AREA_PADDING constants for better layout adjustments. - Enhanced HistoryTable and ModelManagement components for improved visual consistency. - Streamlined import statements and component structure across various files for better organization and readability.
This commit is contained in:
+8
-3
@@ -11,6 +11,7 @@ import { Sidebar } from '@/components/Sidebar';
|
||||
import { TitleBarDragRegion } from '@/components/TitleBarDragRegion';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import { VoicesTab } from '@/components/VoicesTab/VoicesTab';
|
||||
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import { MODEL_DISPLAY_NAMES, useRestoreActiveTasks } from '@/lib/hooks/useRestoreActiveTasks';
|
||||
import {
|
||||
@@ -20,9 +21,8 @@ import {
|
||||
setupWindowCloseHandler,
|
||||
startServer,
|
||||
} from '@/lib/tauri';
|
||||
import { useServerStore } from '@/stores/serverStore';
|
||||
import { TOP_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { useServerStore } from '@/stores/serverStore';
|
||||
|
||||
// Track if server is starting to prevent duplicate starts
|
||||
let serverStarting = false;
|
||||
@@ -140,7 +140,12 @@ function App() {
|
||||
// Show loading screen while server is starting in Tauri
|
||||
if (isTauri() && !serverReady) {
|
||||
return (
|
||||
<div className={cn('min-h-screen bg-background flex items-center justify-center', TOP_SAFE_AREA_PADDING)}>
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Edit, Plus, Trash2, Speaker, CheckCircle2, Check } from 'lucide-react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Check, CheckCircle2, Edit, Plus, Speaker, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -18,14 +21,11 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
|
||||
interface AudioDevice {
|
||||
id: string;
|
||||
@@ -69,8 +69,7 @@ export function AudioTab() {
|
||||
});
|
||||
|
||||
const createChannel = useMutation({
|
||||
mutationFn: (data: { name: string; device_ids: string[] }) =>
|
||||
apiClient.createChannel(data),
|
||||
mutationFn: (data: { name: string; device_ids: string[] }) => apiClient.createChannel(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['channels'] });
|
||||
setCreateDialogOpen(false);
|
||||
@@ -144,12 +143,18 @@ export function AudioTab() {
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-full min-h-0">
|
||||
{/* Left Column - Channels */}
|
||||
<div className={cn('flex flex-col min-h-0 overflow-y-auto', isPlayerVisible && BOTTOM_SAFE_AREA_PADDING)}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col min-h-0 overflow-y-auto',
|
||||
isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,
|
||||
)}
|
||||
>
|
||||
{allChannels.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md">
|
||||
<Speaker className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground mb-4">
|
||||
No audio channels yet. Create your first channel to route voices to specific devices.
|
||||
No audio channels yet. Create your first channel to route voices to specific
|
||||
devices.
|
||||
</p>
|
||||
<Button onClick={() => setCreateDialogOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
@@ -187,29 +192,27 @@ export function AudioTab() {
|
||||
Output Devices
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{channel.device_ids.length > 0 ? (
|
||||
channel.device_ids.map((deviceId) => {
|
||||
const device = allDevices.find((d) => d.id === deviceId);
|
||||
return (
|
||||
<Badge
|
||||
key={deviceId}
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
{device?.name || deviceId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
(() => {
|
||||
const defaultDevice = allDevices.find((d) => d.is_default);
|
||||
return defaultDevice ? (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{defaultDevice.name}
|
||||
</Badge>
|
||||
) : null;
|
||||
})()
|
||||
)}
|
||||
{channel.device_ids.length > 0
|
||||
? channel.device_ids.map((deviceId) => {
|
||||
const device = allDevices.find((d) => d.id === deviceId);
|
||||
return (
|
||||
<Badge
|
||||
key={deviceId}
|
||||
variant="outline"
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
{device?.name || deviceId}
|
||||
</Badge>
|
||||
);
|
||||
})
|
||||
: (() => {
|
||||
const defaultDevice = allDevices.find((d) => d.is_default);
|
||||
return defaultDevice ? (
|
||||
<Badge variant="outline" className="text-xs font-normal">
|
||||
{defaultDevice.name}
|
||||
</Badge>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -259,7 +262,12 @@ export function AudioTab() {
|
||||
</div>
|
||||
|
||||
{/* Right Column - Available Devices */}
|
||||
<div className={cn('flex flex-col min-h-0 overflow-y-auto', isPlayerVisible && BOTTOM_SAFE_AREA_PADDING)}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col min-h-0 overflow-y-auto',
|
||||
isPlayerVisible && BOTTOM_SAFE_AREA_PADDING,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 mb-4">
|
||||
<h3 className="text-lg font-semibold">Available Devices</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -279,22 +287,23 @@ export function AudioTab() {
|
||||
(selectedChannel.device_ids.length === 0
|
||||
? device.is_default
|
||||
: selectedChannel.device_ids.includes(device.id));
|
||||
const canToggle = selectedChannelId && selectedChannel && !selectedChannel.is_default;
|
||||
|
||||
const canToggle =
|
||||
selectedChannelId && selectedChannel && !selectedChannel.is_default;
|
||||
|
||||
const handleDeviceClick = () => {
|
||||
if (!canToggle || !selectedChannel) return;
|
||||
|
||||
|
||||
const currentDeviceIds = selectedChannel.device_ids;
|
||||
const newDeviceIds = isConnected
|
||||
? currentDeviceIds.filter((id) => id !== device.id)
|
||||
: [...currentDeviceIds, device.id];
|
||||
|
||||
|
||||
updateChannel.mutate({
|
||||
channelId: selectedChannelId,
|
||||
data: { device_ids: newDeviceIds },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
key={device.id}
|
||||
@@ -314,9 +323,7 @@ export function AudioTab() {
|
||||
<div
|
||||
className={cn(
|
||||
'h-4 w-4 rounded border-2 flex items-center justify-center shrink-0',
|
||||
isConnected
|
||||
? 'bg-accent border-accent'
|
||||
: 'border-muted-foreground/30',
|
||||
isConnected ? 'bg-accent border-accent' : 'border-muted-foreground/30',
|
||||
)}
|
||||
>
|
||||
{isConnected && <Check className="h-3 w-3 text-accent-foreground" />}
|
||||
@@ -335,9 +342,7 @@ export function AudioTab() {
|
||||
<div className="flex flex-col items-center justify-center py-12 border-2 border-dashed border-muted rounded-md">
|
||||
<CheckCircle2 className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<p className="text-muted-foreground text-center">
|
||||
{isTauri()
|
||||
? 'No audio devices found'
|
||||
: 'Audio device selection requires Tauri'}
|
||||
{isTauri() ? 'No audio devices found' : 'Audio device selection requires Tauri'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -355,31 +360,32 @@ export function AudioTab() {
|
||||
/>
|
||||
|
||||
{/* Edit Channel Dialog */}
|
||||
{editingChannel && (() => {
|
||||
const channel = channels?.find((c) => c.id === editingChannel);
|
||||
return channel ? (
|
||||
<EditChannelDialog
|
||||
open={!!editingChannel}
|
||||
onOpenChange={(open) => !open && setEditingChannel(null)}
|
||||
channel={channel}
|
||||
devices={devices || []}
|
||||
profiles={profiles || []}
|
||||
channelVoices={channelVoices?.profile_ids || []}
|
||||
onUpdate={(name, deviceIds) => {
|
||||
updateChannel.mutate({
|
||||
channelId: editingChannel,
|
||||
data: { name, device_ids: deviceIds },
|
||||
});
|
||||
}}
|
||||
onSetVoices={(profileIds) => {
|
||||
setChannelVoices.mutate({
|
||||
channelId: editingChannel,
|
||||
profileIds,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
{editingChannel &&
|
||||
(() => {
|
||||
const channel = channels?.find((c) => c.id === editingChannel);
|
||||
return channel ? (
|
||||
<EditChannelDialog
|
||||
open={!!editingChannel}
|
||||
onOpenChange={(open) => !open && setEditingChannel(null)}
|
||||
channel={channel}
|
||||
devices={devices || []}
|
||||
profiles={profiles || []}
|
||||
channelVoices={channelVoices?.profile_ids || []}
|
||||
onUpdate={(name, deviceIds) => {
|
||||
updateChannel.mutate({
|
||||
channelId: editingChannel,
|
||||
data: { name, device_ids: deviceIds },
|
||||
});
|
||||
}}
|
||||
onSetVoices={(profileIds) => {
|
||||
setChannelVoices.mutate({
|
||||
channelId: editingChannel,
|
||||
profileIds,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -396,9 +402,7 @@ function ChannelVoicesList({ channelId }: { channelId: string }) {
|
||||
});
|
||||
|
||||
const voiceNames =
|
||||
voices?.profile_ids
|
||||
.map((id) => profiles?.find((p) => p.id === id)?.name)
|
||||
.filter(Boolean) || [];
|
||||
voices?.profile_ids.map((id) => profiles?.find((p) => p.id === id)?.name).filter(Boolean) || [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -422,12 +426,7 @@ interface CreateChannelDialogProps {
|
||||
onCreate: (name: string, deviceIds: string[]) => void;
|
||||
}
|
||||
|
||||
function CreateChannelDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
devices,
|
||||
onCreate,
|
||||
}: CreateChannelDialogProps) {
|
||||
function CreateChannelDialog({ open, onOpenChange, devices, onCreate }: CreateChannelDialogProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [selectedDevices, setSelectedDevices] = useState<string[]>([]);
|
||||
|
||||
@@ -492,7 +491,9 @@ function CreateChannelDialog({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))}
|
||||
onClick={() =>
|
||||
setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -562,11 +563,7 @@ function EditChannelDialog({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="edit-channel-name">Channel Name</Label>
|
||||
<Input
|
||||
id="edit-channel-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Input id="edit-channel-name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Output Devices</Label>
|
||||
@@ -602,7 +599,9 @@ function EditChannelDialog({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))}
|
||||
onClick={() =>
|
||||
setSelectedDevices(selectedDevices.filter((id) => id !== deviceId))
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -646,7 +645,9 @@ function EditChannelDialog({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSelectedVoices(selectedVoices.filter((id) => id !== profileId))}
|
||||
onClick={() =>
|
||||
setSelectedVoices(selectedVoices.filter((id) => id !== profileId))
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import {
|
||||
useDeleteGeneration,
|
||||
useExportGeneration,
|
||||
@@ -27,7 +28,6 @@ import {
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { formatDate, formatDuration } from '@/lib/utils/format';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
|
||||
// OLD TABLE-BASED COMPONENT - REMOVED (can be found in git history)
|
||||
// This is the new alternate history view with fixed height rows
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { CheckCircle2, Download, Loader2, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2, Download, CheckCircle2, Trash2 } from 'lucide-react';
|
||||
import { ModelProgress } from './ModelProgress';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -18,6 +11,13 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import { ModelProgress } from './ModelProgress';
|
||||
|
||||
export function ModelManagement() {
|
||||
const { toast } = useToast();
|
||||
@@ -197,8 +197,8 @@ export function ModelManagement() {
|
||||
{modelToDelete?.sizeMb && (
|
||||
<>
|
||||
{' '}
|
||||
This will free up {formatSize(modelToDelete.sizeMb)} of disk space. The model
|
||||
will need to be re-downloaded if you want to use it again.
|
||||
This will free up {formatSize(modelToDelete.sizeMb)} of disk space. The model will
|
||||
need to be re-downloaded if you want to use it again.
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
@@ -244,13 +244,7 @@ interface ModelItemProps {
|
||||
formatSize: (sizeMb?: number) => string;
|
||||
}
|
||||
|
||||
function ModelItem({
|
||||
model,
|
||||
onDownload,
|
||||
onDelete,
|
||||
isDownloading,
|
||||
formatSize,
|
||||
}: ModelItemProps) {
|
||||
function ModelItem({ model, onDownload, onDelete, isDownloading, formatSize }: ModelItemProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -17,15 +17,15 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import type { VoiceProfileResponse } from '@/lib/api/types';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import { useHistory } from '@/lib/hooks/useHistory';
|
||||
import { useDeleteProfile, useProfileSamples, useProfiles } from '@/lib/hooks/useProfiles';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
import { ProfileForm } from '@/components/VoiceProfiles/ProfileForm';
|
||||
import { BOTTOM_SAFE_AREA_PADDING } from '@/lib/constants/ui';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
|
||||
export function VoicesTab() {
|
||||
const { data: profiles, isLoading } = useProfiles();
|
||||
|
||||
Reference in New Issue
Block a user