import Card, { GridCard } from "@/components/Card"; import { useEffect, useRef, useState } from "react"; import { Button } from "@components/Button"; import LogoBlueIcon from "@/assets/logo-blue.svg"; import LogoWhiteIcon from "@/assets/logo-white.svg"; import Modal from "@components/Modal"; import { useRTCStore, } from "../hooks/stores"; import { cx } from "../cva.config"; import { LuCheck, LuUpload, } from "react-icons/lu"; import { formatters } from "@/utils"; import { PlusCircleIcon } from "@heroicons/react/20/solid"; import AutoHeight from "./AutoHeight"; import { useJsonRpc } from "../hooks/useJsonRpc"; import notifications from "../notifications"; import { isOnDevice } from "../main"; import { ViewHeader } from "./MountMediaDialog"; export default function UploadPluginModal({ open, setOpen, }: { open: boolean; setOpen: (open: boolean) => void; }) { return ( setOpen(false)}> ); } function Dialog({ setOpen }: { setOpen: (open: boolean) => void }) { return (
JetKVM Logo JetKVM Logo { setOpen(false) }} onCancelUpload={() => { setOpen(false) }} />
); } // This is pretty much a copy-paste from the UploadFileView component in the MountMediaDialog just with the media terminology changed and the rpc method changed. // TODO: refactor to a shared component function UploadFileView({ onBack, onCancelUpload, }: { onBack: () => void; onCancelUpload: () => void; }) { const [uploadState, setUploadState] = useState<"idle" | "uploading" | "success">( "idle", ); const [uploadProgress, setUploadProgress] = useState(0); const [uploadedFileName, setUploadedFileName] = useState(null); const [uploadedFileSize, setUploadedFileSize] = useState(null); const [uploadSpeed, setUploadSpeed] = useState(null); const [fileError, setFileError] = useState(null); const [uploadError, setUploadError] = useState(null); const [send] = useJsonRpc(); const rtcDataChannelRef = useRef(null); useEffect(() => { const ref = rtcDataChannelRef.current; return () => { if (ref) { ref.onopen = null; ref.onerror = null; ref.onmessage = null; ref.onclose = null; ref.close(); } }; }, []); function handleWebRTCUpload( file: File, alreadyUploadedBytes: number, dataChannel: string, ) { const rtcDataChannel = useRTCStore .getState() .peerConnection?.createDataChannel(dataChannel); if (!rtcDataChannel) { console.error("Failed to create data channel for file upload"); notifications.error("Failed to create data channel for file upload"); setUploadState("idle"); console.log("Upload state set to 'idle'"); return; } rtcDataChannelRef.current = rtcDataChannel; const lowWaterMark = 256 * 1024; const highWaterMark = 1 * 1024 * 1024; rtcDataChannel.bufferedAmountLowThreshold = lowWaterMark; let lastUploadedBytes = alreadyUploadedBytes; let lastUpdateTime = Date.now(); const speedHistory: number[] = []; rtcDataChannel.onmessage = e => { try { const { AlreadyUploadedBytes, Size } = JSON.parse(e.data) as { AlreadyUploadedBytes: number; Size: number; }; const now = Date.now(); const timeDiff = (now - lastUpdateTime) / 1000; // in seconds const bytesDiff = AlreadyUploadedBytes - lastUploadedBytes; if (timeDiff > 0) { const instantSpeed = bytesDiff / timeDiff; // bytes per second // Add to speed history, keeping last 5 readings speedHistory.push(instantSpeed); if (speedHistory.length > 5) { speedHistory.shift(); } // Calculate average speed const averageSpeed = speedHistory.reduce((a, b) => a + b, 0) / speedHistory.length; setUploadSpeed(averageSpeed); setUploadProgress((AlreadyUploadedBytes / Size) * 100); } lastUploadedBytes = AlreadyUploadedBytes; lastUpdateTime = now; } catch (e) { console.error("Error processing RTC Data channel message:", e); } }; rtcDataChannel.onopen = () => { let pauseSending = false; // Pause sending when the buffered amount is high const chunkSize = 4 * 1024; // 4KB chunks let offset = alreadyUploadedBytes; const sendNextChunk = () => { if (offset >= file.size) { rtcDataChannel.close(); setUploadState("success"); return; } if (pauseSending) return; const chunk = file.slice(offset, offset + chunkSize); chunk.arrayBuffer().then(buffer => { rtcDataChannel.send(buffer); if (rtcDataChannel.bufferedAmount >= highWaterMark) { pauseSending = true; } offset += buffer.byteLength; console.log(`Chunk sent: ${offset} / ${file.size} bytes`); sendNextChunk(); }); }; sendNextChunk(); rtcDataChannel.onbufferedamountlow = () => { console.log("RTC Data channel buffered amount low"); pauseSending = false; // Now the data channel is ready to send more data sendNextChunk(); }; }; rtcDataChannel.onerror = error => { console.error("RTC Data channel error:", error); notifications.error(`Upload failed: ${error}`); setUploadState("idle"); console.log("Upload state set to 'idle'"); }; } async function handleHttpUpload( file: File, alreadyUploadedBytes: number, dataChannel: string, ) { const uploadUrl = `${import.meta.env.VITE_SIGNAL_API}/storage/upload?uploadId=${dataChannel}`; const xhr = new XMLHttpRequest(); xhr.open("POST", uploadUrl, true); let lastUploadedBytes = alreadyUploadedBytes; let lastUpdateTime = Date.now(); const speedHistory: number[] = []; xhr.upload.onprogress = event => { if (event.lengthComputable) { const totalUploaded = alreadyUploadedBytes + event.loaded; const totalSize = file.size; const now = Date.now(); const timeDiff = (now - lastUpdateTime) / 1000; // in seconds const bytesDiff = totalUploaded - lastUploadedBytes; if (timeDiff > 0) { const instantSpeed = bytesDiff / timeDiff; // bytes per second // Add to speed history, keeping last 5 readings speedHistory.push(instantSpeed); if (speedHistory.length > 5) { speedHistory.shift(); } // Calculate average speed const averageSpeed = speedHistory.reduce((a, b) => a + b, 0) / speedHistory.length; setUploadSpeed(averageSpeed); setUploadProgress((totalUploaded / totalSize) * 100); } lastUploadedBytes = totalUploaded; lastUpdateTime = now; } }; xhr.onload = () => { if (xhr.status === 200) { setUploadState("success"); } else { console.error("Upload error:", xhr.statusText); setUploadError(xhr.statusText); setUploadState("idle"); } }; xhr.onerror = () => { console.error("XHR error:", xhr.statusText); setUploadError(xhr.statusText); setUploadState("idle"); }; // Prepare the data to send const blob = file.slice(alreadyUploadedBytes); // Send the file data xhr.send(blob); } const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { // Reset the upload error when a new file is selected setUploadError(null); setFileError(null); console.log(`File selected: ${file.name}, size: ${file.size} bytes`); setUploadedFileName(file.name); setUploadedFileSize(file.size); setUploadState("uploading"); console.log("Upload state set to 'uploading'"); send("pluginStartUpload", { filename: file.name, size: file.size }, resp => { console.log("pluginStartUpload response:", resp); if ("error" in resp) { console.error("Upload error:", resp.error.message); setUploadError(resp.error.data || resp.error.message); setUploadState("idle"); console.log("Upload state set to 'idle'"); return; } const { alreadyUploadedBytes, dataChannel } = resp.result as { alreadyUploadedBytes: number; dataChannel: string; }; console.log( `Already uploaded bytes: ${alreadyUploadedBytes}, Data channel: ${dataChannel}`, ); if (isOnDevice) { handleHttpUpload(file, alreadyUploadedBytes, dataChannel); } else { handleWebRTCUpload(file, alreadyUploadedBytes, dataChannel); } }); } }; return (
{ if (uploadState === "idle") { document.getElementById("file-upload")?.click(); } }} className="block select-none" >
{uploadState === "idle" && (

Click to select a file

Supported formats: TAR, TAR.GZ

)} {uploadState === "uploading" && (

Uploading {formatters.truncateMiddle(uploadedFileName, 30)}

{formatters.bytes(uploadedFileSize || 0)}

Uploading... {uploadSpeed !== null ? `${formatters.bytes(uploadSpeed)}/s` : "Calculating..."}
)} {uploadState === "success" && (

Upload successful

{formatters.truncateMiddle(uploadedFileName, 40)} has been uploaded

)}
{fileError &&

{fileError}

}
{/* Display upload error if present */} {uploadError && (
Error: {uploadError}
)}
{uploadState === "uploading" ? (
); }