import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, Plus, Printer, ExternalLink, AlertTriangle, Info, FileText, ShieldCheck, Copy, Check, Download } from 'lucide-react'; import { multiVirtualPrinterApi, virtualPrinterApi } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { Toggle } from './Toggle'; import { useToast } from '../contexts/ToastContext'; import { copyTextToClipboard, downloadTextFile } from '../utils/clipboard'; import { VirtualPrinterCard } from './VirtualPrinterCard'; import { VirtualPrinterAddDialog } from './VirtualPrinterAddDialog'; export function VirtualPrinterList() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [showAddDialog, setShowAddDialog] = useState(false); const { data, isLoading } = useQuery({ queryKey: ['virtual-printers'], queryFn: multiVirtualPrinterApi.list, refetchInterval: 10000, }); const { data: globalSettings } = useQuery({ queryKey: ['virtual-printer-settings'], queryFn: virtualPrinterApi.getSettings, }); const archiveNameSourceMutation = useMutation({ mutationFn: (source: 'metadata' | 'filename') => virtualPrinterApi.updateSettings({ archive_name_source: source }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['virtual-printer-settings'] }); showToast(t('virtualPrinter.toast.updated')); }, onError: (error: Error) => { showToast(error.message || t('virtualPrinter.toast.failedToUpdate'), 'error'); }, }); const useFilename = globalSettings?.archive_name_source === 'filename'; // Shared CA certificate — the slicer imports it once to trust every VP's // TLS connection. Generated on demand by the backend, never changes. const { data: caCert } = useQuery({ queryKey: ['vp-ca-certificate'], queryFn: multiVirtualPrinterApi.getCaCertificate, staleTime: Infinity, }); const [caCopied, setCaCopied] = useState(false); const handleCopyCert = async () => { if (!caCert) return; const ok = await copyTextToClipboard(caCert.pem); if (ok) { setCaCopied(true); showToast(t('virtualPrinter.caCert.copied')); setTimeout(() => setCaCopied(false), 2000); } else { showToast(t('virtualPrinter.toast.copyFailed'), 'error'); } }; const handleDownloadCert = () => { if (!caCert) return; downloadTextFile(caCert.pem, 'bambuddy-virtual-printer-ca.crt', 'application/x-pem-file'); }; if (isLoading) { return ( ); } const printers = data?.printers || []; const models = data?.models || {}; return (
{/* Top row - Setup Required (1 col) + How it works (2 cols) */}

{t('virtualPrinter.setupRequired.title')}

{t('virtualPrinter.setupRequired.description')}

{t('virtualPrinter.setupRequired.readGuide')}

{t('virtualPrinter.howItWorks.title')}

  • {t('virtualPrinter.howItWorks.step1')}
  • {t('virtualPrinter.howItWorks.step2')}
  • {t('virtualPrinter.howItWorks.step3')}
{/* Global VP behavior settings — two side-by-side cards, not full width */}
{/* Slicer CA certificate — shared by every VP, imported into the slicer's trust store once instead of fetching it from the CLI. */}

{t('virtualPrinter.caCert.title')}

{t('virtualPrinter.caCert.description')}

{caCert && (

{t('virtualPrinter.caCert.fingerprint')}: {caCert.fingerprint_sha256}

)}
{/* Archive name source */}

{t('virtualPrinter.archiveNameSource.title')}

{t('virtualPrinter.archiveNameSource.metadata')} archiveNameSourceMutation.mutate(checked ? 'filename' : 'metadata')} disabled={archiveNameSourceMutation.isPending} /> {t('virtualPrinter.archiveNameSource.filename')}

{t('virtualPrinter.archiveNameSource.description')}

{/* Header with add button */}

{t('virtualPrinter.list.title')}

({printers.length})
{/* Printer cards - 3 column grid */} {printers.length === 0 ? (

{t('virtualPrinter.list.empty')}

) : (
{printers.map((printer) => ( ))}
)} {showAddDialog && ( setShowAddDialog(false)} /> )}
); }