import { useState, useCallback } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { useMachineStore } from '../stores/useMachineStore'; import { usePatternStore } from '../stores/usePatternStore'; import { useUIStore } from '../stores/useUIStore'; import { convertPesToPen, type PesPatternData } from '../formats/import/pesImporter'; import { canUploadPattern, getMachineStateCategory } from '../utils/machineStateHelpers'; import { PatternInfoSkeleton } from './SkeletonLoader'; import { PatternInfo } from './PatternInfo'; import { ArrowUpTrayIcon, CheckCircleIcon, DocumentTextIcon, FolderOpenIcon } from '@heroicons/react/24/solid'; import { createFileService } from '../platform'; import type { IFileService } from '../platform/interfaces/IFileService'; export function FileUpload() { // Machine store const { isConnected, machineStatus, uploadProgress, isUploading, machineInfo, resumeAvailable, resumeFileName, uploadPattern, } = useMachineStore( useShallow((state) => ({ isConnected: state.isConnected, machineStatus: state.machineStatus, uploadProgress: state.uploadProgress, isUploading: state.isUploading, machineInfo: state.machineInfo, resumeAvailable: state.resumeAvailable, resumeFileName: state.resumeFileName, uploadPattern: state.uploadPattern, })) ); // Pattern store const { pesData: pesDataProp, currentFileName, patternOffset, patternUploaded, setPattern, } = usePatternStore( useShallow((state) => ({ pesData: state.pesData, currentFileName: state.currentFileName, patternOffset: state.patternOffset, patternUploaded: state.patternUploaded, setPattern: state.setPattern, })) ); // UI store const { pyodideReady, pyodideProgress, pyodideLoadingStep, initializePyodide, } = useUIStore( useShallow((state) => ({ pyodideReady: state.pyodideReady, pyodideProgress: state.pyodideProgress, pyodideLoadingStep: state.pyodideLoadingStep, initializePyodide: state.initializePyodide, })) ); const [localPesData, setLocalPesData] = useState(null); const [fileName, setFileName] = useState(''); const [fileService] = useState(() => createFileService()); // Use prop pesData if available (from cached pattern), otherwise use local state const pesData = pesDataProp || localPesData; // Use currentFileName from App state, or local fileName, or resumeFileName for display const displayFileName = currentFileName || fileName || resumeFileName || ''; const [isLoading, setIsLoading] = useState(false); const handleFileChange = useCallback( async (event?: React.ChangeEvent) => { setIsLoading(true); try { // Wait for Pyodide if it's still loading if (!pyodideReady) { console.log('[FileUpload] Waiting for Pyodide to finish loading...'); await initializePyodide(); console.log('[FileUpload] Pyodide ready'); } let file: File | null = null; // In Electron, use native file dialogs if (fileService.hasNativeDialogs()) { file = await fileService.openFileDialog({ accept: '.pes' }); } else { // In browser, use the input element file = event?.target.files?.[0] || null; } if (!file) { setIsLoading(false); return; } const data = await convertPesToPen(file); setLocalPesData(data); setFileName(file.name); setPattern(data, file.name); } catch (err) { alert( `Failed to load PES file: ${ err instanceof Error ? err.message : 'Unknown error' }` ); } finally { setIsLoading(false); } }, [fileService, setPattern, pyodideReady, initializePyodide] ); const handleUpload = useCallback(() => { if (pesData && displayFileName) { uploadPattern(pesData.penData, pesData, displayFileName, patternOffset); } }, [pesData, displayFileName, uploadPattern, patternOffset]); // Check if pattern (with offset) fits within hoop bounds const checkPatternFitsInHoop = useCallback(() => { if (!pesData || !machineInfo) { return { fits: true, error: null }; } const { bounds } = pesData; const { maxWidth, maxHeight } = machineInfo; // Calculate pattern bounds with offset applied const patternMinX = bounds.minX + patternOffset.x; const patternMaxX = bounds.maxX + patternOffset.x; const patternMinY = bounds.minY + patternOffset.y; const patternMaxY = bounds.maxY + patternOffset.y; // Hoop bounds (centered at origin) const hoopMinX = -maxWidth / 2; const hoopMaxX = maxWidth / 2; const hoopMinY = -maxHeight / 2; const hoopMaxY = maxHeight / 2; // Check if pattern exceeds hoop bounds const exceedsLeft = patternMinX < hoopMinX; const exceedsRight = patternMaxX > hoopMaxX; const exceedsTop = patternMinY < hoopMinY; const exceedsBottom = patternMaxY > hoopMaxY; if (exceedsLeft || exceedsRight || exceedsTop || exceedsBottom) { const directions = []; if (exceedsLeft) directions.push(`left by ${((hoopMinX - patternMinX) / 10).toFixed(1)}mm`); if (exceedsRight) directions.push(`right by ${((patternMaxX - hoopMaxX) / 10).toFixed(1)}mm`); if (exceedsTop) directions.push(`top by ${((hoopMinY - patternMinY) / 10).toFixed(1)}mm`); if (exceedsBottom) directions.push(`bottom by ${((patternMaxY - hoopMaxY) / 10).toFixed(1)}mm`); return { fits: false, error: `Pattern exceeds hoop bounds: ${directions.join(', ')}. Adjust pattern position in preview.` }; } return { fits: true, error: null }; }, [pesData, machineInfo, patternOffset]); const boundsCheck = checkPatternFitsInHoop(); const borderColor = pesData ? 'border-orange-600 dark:border-orange-500' : 'border-gray-400 dark:border-gray-600'; const iconColor = pesData ? 'text-orange-600 dark:text-orange-400' : 'text-gray-600 dark:text-gray-400'; return (

Pattern File

{pesData && displayFileName ? (

{displayFileName}

) : (

No pattern loaded

)}
{resumeAvailable && resumeFileName && (

Cached: "{resumeFileName}"

)} {isLoading && } {!isLoading && pesData && (
)}
{pesData && canUploadPattern(machineStatus) && !patternUploaded && uploadProgress < 100 && ( )}
{/* Pyodide initialization progress indicator - shown when initializing or waiting */} {!pyodideReady && pyodideProgress > 0 && (
{isLoading && !pyodideReady ? 'Please wait - initializing Python environment...' : pyodideLoadingStep || 'Initializing Python environment...'} {pyodideProgress.toFixed(0)}%

{isLoading && !pyodideReady ? 'File dialog will open automatically when ready' : 'This only happens once on first use'}

)} {/* Error/warning messages with smooth transition - placed after buttons */}
{pesData && !canUploadPattern(machineStatus) && (
Cannot upload while {getMachineStateCategory(machineStatus)}
)} {pesData && boundsCheck.error && (
Pattern too large: {boundsCheck.error}
)}
{isUploading && uploadProgress < 100 && (
Uploading {uploadProgress > 0 ? uploadProgress.toFixed(1) + '%' : 'Starting...'}
)}
); }