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