import { useState, useEffect, useCallback } from "react"; import { BeamBoxUploader } from "../lib/core/beambox-uploader.ts"; import { logger, LogEventType } from "../lib/utils/logger.ts"; import { BeamBoxError } from "../lib/utils/errors.ts"; import type { ConnectionStep } from "../components/index.ts"; import { basename } from "node:path"; import { updateStepStatus } from "../utils/app-utils.ts"; import type { UploadOptions } from "../cli/types.ts"; export function useUpload(options: UploadOptions, verbose: boolean) { const [status, setStatus] = useState< "connecting" | "uploading" | "success" | "error" >("connecting"); const [message, setMessage] = useState("Initializing..."); const [progress, setProgress] = useState(0); const [currentFileIndex, setCurrentFileIndex] = useState(0); const [totalFiles, setTotalFiles] = useState(1); const [uploadSteps, setUploadSteps] = useState([ { id: "image-info", label: "Sending image info", status: "pending" }, { id: "data", label: "Transferring image data", status: "pending" }, { id: "complete", label: "Finalizing", status: "pending" }, ]); const [connectionSteps, setConnectionSteps] = useState([ { id: "scan", label: "Scanning for device", status: "pending" }, { id: "connect", label: "Connecting to device", status: "pending" }, { id: "discover", label: "Discovering characteristics", status: "pending" }, { id: "wait-status", label: "Waiting for device status", status: "pending", }, ]); useEffect(() => { const unsubscribe = logger.onLog((entry) => { const eventType = entry.eventType; switch (eventType) { case LogEventType.SCAN_START: setConnectionSteps((prev) => updateStepStatus(prev, "scan", "active"), ); break; case LogEventType.DEVICE_FOUND: setConnectionSteps((prev) => updateStepStatus(prev, "scan", "complete", "connect"), ); break; case LogEventType.CONNECT_START: setConnectionSteps((prev) => updateStepStatus(prev, "connect", "active"), ); break; case LogEventType.CONNECTED: setConnectionSteps((prev) => updateStepStatus(prev, "connect", "complete", "discover"), ); break; case LogEventType.DISCOVER_CHAR: setConnectionSteps((prev) => updateStepStatus(prev, "discover", "complete", "wait-status"), ); break; case LogEventType.STATUS_WAIT: setConnectionSteps((prev) => updateStepStatus(prev, "wait-status", "active"), ); break; case LogEventType.STATUS_RECEIVED: setConnectionSteps((prev) => updateStepStatus(prev, "wait-status", "complete"), ); break; case LogEventType.IMAGE_INFO_SEND: setUploadSteps((prev) => updateStepStatus(prev, "image-info", "active"), ); break; case LogEventType.DATA_SEND_START: setUploadSteps((prev) => updateStepStatus(prev, "image-info", "complete", "data"), ); break; case LogEventType.DATA_SEND_COMPLETE: setUploadSteps((prev) => updateStepStatus(prev, "data", "complete", "complete"), ); break; } }); return unsubscribe; }, []); useEffect(() => { const runUpload = async () => { let uploader: BeamBoxUploader | null = null; try { const [width, height] = options.size.split("x").map(Number); const targetSize: [number, number] = [width!, height!]; const [animationWidth, animationHeight] = options.animationSize .split("x") .map(Number); const animationSize: [number, number] = [ animationWidth!, animationHeight!, ]; const packetDelaySeconds = options.packetDelay / 1000.0; const isBulk = options.isBulk && options.images && options.images.length > 1; const imagesToUpload = isBulk ? options.images! : []; if (isBulk) { setTotalFiles(imagesToUpload.length); } uploader = new BeamBoxUploader( options.address, packetDelaySeconds, undefined, undefined, undefined, verbose, ); setStatus("connecting"); setMessage("Connecting to BeamBox device..."); const connected = await uploader.connect(); if (!connected) { throw new Error("Failed to connect to device"); } setMessage("Connected successfully!"); await new Promise((resolve) => setTimeout(resolve, 500)); setStatus("uploading"); if (isBulk) { for (let i = 0; i < imagesToUpload.length; i++) { const imagePath = imagesToUpload[i]; if (!imagePath) continue; const fileName = basename(imagePath); setCurrentFileIndex(i + 1); setMessage( `Uploading ${i + 1}/${imagesToUpload.length}: ${fileName}`, ); setUploadSteps([ { id: "image-info", label: "Sending image info", status: "pending", }, { id: "data", label: "Transferring image data", status: "pending", }, { id: "complete", label: "Finalizing", status: "pending" }, ]); setProgress(0); const success = await uploader.uploadImageFromFile( imagePath, targetSize, animationSize, (prog: number, status?: string) => { setProgress(prog); if (status) { setMessage(`${i + 1}/${imagesToUpload.length}: ${status}`); } }, ); if (!success) { throw new Error(`Failed to upload ${fileName}`); } setUploadSteps((prev) => updateStepStatus(prev, "complete", "complete"), ); if (i < imagesToUpload.length - 1) { await new Promise((resolve) => setTimeout(resolve, 1000)); } } await new Promise((resolve) => setTimeout(resolve, 2000)); setStatus("success"); setMessage( `All ${imagesToUpload.length} images uploaded successfully!`, ); } else { setMessage("Preparing upload..."); let success: boolean; if (options.test) { success = await uploader.uploadCheckerboard( targetSize, 8, (prog: number, status?: string) => { setProgress(prog); if (status) { setMessage(status); } }, ); } else if (options.image) { success = await uploader.uploadImageFromFile( options.image, targetSize, animationSize, (prog: number, status?: string) => { setProgress(prog); if (status) { setMessage(status); } }, ); } else { throw new Error("No image provided"); } await new Promise((resolve) => setTimeout(resolve, 2000)); if (success) { setUploadSteps((prev) => updateStepStatus(prev, "complete", "complete"), ); setStatus("success"); setMessage("Upload completed successfully!"); } else { setStatus("error"); setMessage("Upload failed"); } } } catch (error) { setStatus("error"); if (error instanceof BeamBoxError) { setMessage(`Upload failed: ${error.message}`); } else { setMessage(`Upload failed: ${error}`); } } finally { if (uploader) { await uploader.disconnect(); } } }; runUpload(); }, [options, verbose]); return { status, message, progress, currentFileIndex, totalFiles, uploadSteps, connectionSteps, }; }