const MAGIC=1397770288,PACKET_SIZE=24,CMD_QUIT=0,CMD_OPEN=1,CMD_EXPORT=1,RESULT_OK=0,RESULT_ERROR=1,FLAG_NONE=0,FLAG_STREAM=1;class UsbPacket{constructor(e=MAGIC,t=0,s=0,i=0,n=0,o=0){this.magic=e,this.arg2=t,this.arg3=s,this.arg4=i,this.arg5=n,this.crc32c=o}toBuffer(){const e=new ArrayBuffer(24),t=new DataView(e);return t.setUint32(0,this.magic,!0),t.setUint32(4,this.arg2,!0),t.setUint32(8,this.arg3,!0),t.setUint32(12,this.arg4,!0),t.setUint32(16,this.arg5,!0),t.setUint32(20,this.crc32c,!0),e}static fromBuffer(e){const t=new DataView(e);return new this(t.getUint32(0,!0),t.getUint32(4,!0),t.getUint32(8,!0),t.getUint32(12,!0),t.getUint32(16,!0),t.getUint32(20,!0))}calculateCrc32c(){const e=this.toBuffer(),t=new Uint8Array(e,0,20);return crc32c(0,t)}generateCrc32c(){this.crc32c=this.calculateCrc32c()}verify(){if(this.crc32c!==this.calculateCrc32c())throw new Error("CRC32C mismatch");if(this.magic!==MAGIC)throw new Error("Bad magic");return!0}}class SendPacket extends UsbPacket{static build(e,t=0,s=0){const i=new SendPacket(MAGIC,e,t,s);return i.generateCrc32c(),i}getCmd(){return this.arg2}}class ResultPacket extends UsbPacket{static build(e,t=0,s=0){const i=new ResultPacket(MAGIC,e,t,s);return i.generateCrc32c(),i}verify(){if(super.verify(),0!==this.arg2)throw new Error("Result not OK");return!0}}class SendDataPacket extends UsbPacket{static build(e,t,s){const i=Number(BigInt(e)>>32n&0xFFFFFFFFn),n=Number(0xFFFFFFFFn&BigInt(e)),o=new SendDataPacket(MAGIC,i,n,t,s);return o.generateCrc32c(),o}getOffset(){return Number(BigInt(this.arg2)<<32n|BigInt(this.arg3))}getSize(){return this.arg4}getCrc32c(){return this.arg5}}const crc32c=(()=>{const e=new Uint32Array(256);for(let t=0;t<256;t++){let s=t;for(let e=0;e<8;e++)s=1&s?s>>>1^2197175160:s>>>1;e[t]=s>>>0}return function(t,s){t^=4294967295;let i=0;const n=s.length;for(;i>>8,t=e[255&(t^s[i+1])]^t>>>8,t=e[255&(t^s[i+2])]^t>>>8,t=e[255&(t^s[i+3])]^t>>>8;for(;i>>8;return(4294967295^t)>>>0}})();class WebUSBFileTransfer{constructor(){this.device=null,this.isConnected=!1,this.endpointIn=null,this.endpointOut=null,this.fileQueue=[],this.authorizedDevices=[],this.toastTimeout=null,this.coverage=new Map,this.progressContext={current:0,total:0},this.completedCount=0,this.transferStartTime=null,this.lastUpdateTime=null,this.lastBytesTransferred=0,this.currentSpeed=0,this.speedSamples=[],this.averageSpeed=0,this.setupEventListeners(),this.checkWebUSBSupport()}async checkWebUSBSupport(){navigator.usb?await this.loadAuthorizedDevices():this.showUnsupportedSplash()}async loadAuthorizedDevices(){try{const e=await navigator.usb.getDevices();this.authorizedDevices=e.filter(e=>1406===e.vendorId&&12288===e.productId),this.showAuthorizedDevices(),this.authorizedDevices.length>0?(this.log(`Found ${this.authorizedDevices.length} previously authorized device(s)`),await this.tryAutoConnect()):this.log("No previously authorized devices found")}catch(e){this.log(`Error loading authorized devices: ${e.message}`),this.authorizedDevices=[],this.showAuthorizedDevices()}}async tryAutoConnect(){if(0!==this.authorizedDevices.length)try{const e=this.authorizedDevices[0];this.log(`Attempting to auto-connect to: ${e.productName||"Unknown Device"}`),await this.connectToDevice(e)}catch(e){this.log(`Auto-connect failed: ${e.message}`),this.showToast("Auto-connect failed. Device may be unplugged.","info",4e3)}}formatTime(e){const t=Math.floor(e/60),s=Math.floor(e%60);return`${t.toString().padStart(2,"0")}:${s.toString().padStart(2,"0")}`}showAuthorizedDevices(){const e=document.getElementById("authorizedDevices");0!==this.authorizedDevices.length?(e.style.display="block",this.updateAuthorizedDevicesUI()):e.style.display="none"}updateAuthorizedDevicesUI(){const e=document.getElementById("deviceListContainer");let t="";this.authorizedDevices.forEach((e,s)=>{const i=e.productName||"Unknown Device",n=`${e.vendorId.toString(16).padStart(4,"0")}:${e.productId.toString(16).padStart(4,"0")}`,o=this.device&&this.device===e&&this.isConnected;t+=`\n
\n
${i} ${e.serialNumber}
\n
${n}
\n \n
\n `}),e.innerHTML=t;e.querySelectorAll("button[data-device-index]:not([disabled])").forEach(e=>{e.addEventListener("click",async e=>{const t=parseInt(e.target.getAttribute("data-device-index"));await this.connectToAuthorizedDevice(t)})})}async connectToAuthorizedDevice(e){if(e<0||e>=this.authorizedDevices.length)return void this.showStatus("Invalid device index","error");const t=this.authorizedDevices[e];this.log(`Connecting to authorized device: ${t.productName||"Unknown Device"}`);try{await this.connectToDevice(t)}catch(e){this.log(`Failed to connect to authorized device: ${e.message}`),this.showStatus(`Failed to connect: ${e.message}`,"error")}}showUnsupportedSplash(){document.querySelector(".container").innerHTML='\n
\n

WebUSB File Transfer

\n

⚠️ Browser Not Supported

\n

Your browser does not support WebUSB API.

\n

To use this application, please switch to a supported browser:

\n

• Google Chrome (version 61+)
\n • Microsoft Edge (version 79+)
\n • Opera (version 48+)

\n

Firefox and Safari do not currently support WebUSB.

\n \n View Browser Compatibility Chart\n \n
\n '}setupEventListeners(){document.getElementById("connectBtn").addEventListener("click",()=>this.connectDevice()),document.getElementById("disconnectBtn").addEventListener("click",()=>this.disconnectDevice()),document.getElementById("fileInput").addEventListener("change",e=>this.handleFileSelect(e)),document.getElementById("sendBtn").addEventListener("click",()=>this.sendFile()),document.getElementById("clearLogBtn").addEventListener("click",()=>this.clearLog()),document.getElementById("copyLogBtn").addEventListener("click",()=>this.copyLog()),document.getElementById("addFilesBtn").addEventListener("click",()=>this.triggerFileInput()),document.getElementById("clearQueueBtn").addEventListener("click",()=>this.clearFileQueue()),document.getElementById("toastClose").addEventListener("click",()=>this.hideConnectionToast())}triggerFileInput(){document.getElementById("fileInput").click()}clearFileQueue(){this.fileQueue=[],document.getElementById("fileInput").value="",this.updateFileQueueUI(),this.log("File queue cleared"),this.showToast("File queue cleared","info",2e3)}handleFileSelect(e){const t=Array.from(e.target.files),s=[".nsp",".xci",".nsz",".xcz"];if(t.length>0){let e=0;for(const i of t){const t=i.name.toLowerCase();s.some(e=>t.endsWith(e))?this.fileQueue.some(e=>e.name===i.name&&e.size===i.size)||(this.fileQueue.push(i),e++):this.log(`Skipping unsupported file type: ${i.name}`)}e>0?(this.updateFileQueueUI(),this.log(`Added ${e} file(s) to queue. Total: ${this.fileQueue.length}`),this.showToast(`Added ${e} file(s) to queue`,"success",2e3)):this.showToast("No supported files were added","info",2e3)}e.target.value=""}updateFileQueueUI(){const e=document.getElementById("fileQueueList");if(document.getElementById("fileCount").textContent=`${this.fileQueue.length} file${1!==this.fileQueue.length?"s":""}`,0===this.fileQueue.length)return e.innerHTML='
No files in queue. Click "Add Files" to select files.
',document.getElementById("clearQueueBtn").disabled=!0,void(document.getElementById("sendBtn").disabled=!0);document.getElementById("clearQueueBtn").disabled=!1,document.getElementById("sendBtn").disabled=!this.isConnected;let t="",s=0;for(let e=0;e\n
${i.name}
\n
${this.formatFileSize(i.size)}
\n
\n \n
\n \n `}t+=`\n
\n
Total
\n
${this.formatFileSize(s)}
\n
\n
\n `,e.innerHTML=t;e.querySelectorAll(".btn-remove").forEach(e=>{e.addEventListener("click",e=>{const t=parseInt(e.target.getAttribute("data-index"));this.removeFileFromQueue(t)})})}removeFileFromQueue(e){if(e>=0&&e"in"===e.direction&&"bulk"===e.type)?.endpointNumber,this.endpointOut=t.endpoints.find(e=>"out"===e.direction&&"bulk"===e.type)?.endpointNumber,void 0===this.endpointIn||void 0===this.endpointOut)throw new Error("Bulk IN/OUT endpoints not found");this.isConnected=!0,this.updateUI(),this.showToast("Device connected successfully!","success",3e3),this.showConnectionToast(`Connected: ${this.device.productName||"USB Device"}`,"connect")}async disconnectDevice(){try{if(this.device)try{this.isConnected&&await this.device.close()}catch(e){this.log(`Close skipped: ${e.message}`)}finally{this.device=null,this.isConnected=!1,this.updateUI(),this.log("Device state reset after disconnect"),this.showConnectionToast("Device Disconnected","disconnect")}}catch(e){this.log(`Disconnect error: ${e.message}`),this.showToast(`Disconnect error: ${e.message}`,"error",4e3)}}async sendFile(){const e=this.fileQueue;let t=new TextEncoder;if(!e.length||!this.isConnected)return void this.showToast("Please select files and ensure device is connected","error",4e3);let s=e.map(e=>e.name).join("\n")+"\n";const i=t.encode(s);this.completedCount=0,this.showTransferProgress(e.length);try{for(this.log("Waiting for Sphaira to begin transfer"),document.getElementById("sendBtn").disabled=!0,await this.get_send_header(),await this.send_result(0,i.length),await this.write(i);;)try{const[t,s,i]=await this.get_send_header();if(0==t){await this.send_result(0),e.length>0&&(this.log(`All ${e.length} files transferred successfully`),this.showToast(`✅ All ${e.length} files transferred successfully!`,"success",5e3),this.updateTransferProgress(e.length,e.length,0,null,100));break}if(1!=t){await this.send_result(1),this.log(`❌ Unknown command (${t}) from device`),this.showToast(`❌ Transfer stopped after ${this.completedCount} of ${e.length} files (unknown command)`,"error",5e3);break}{const t=e[s];if(!t){await this.send_result(1),this.showToast(`Device requested invalid file index: ${s}`,"error",5e3),this.log(`❌ Transfer stopped: invalid file index ${s} (out of ${e.length})`);break}const i=e.length,n=s+1;this.progressContext={current:n,total:i},this.log(`Opening file [${n}/${i}]: ${t.name} (${this.formatFileSize(t.size)})`),this.showToast(`📤 Transferring file ${n} of ${i}: ${t.name}`,"info",3e3),this.updateTransferProgress(this.completedCount,i,0,t,0),this.coverage.delete(t.name),await this.send_result(0),await this.file_transfer_loop(t),this.completedCount+=1,this.showToast(`✅ File ${n} of ${i} completed`,"success",2e3),this.updateTransferProgress(this.completedCount,i,0,null,100)}}catch(t){this.log(`❌ Loop error: ${t.message}`),this.showToast(`❌ Transfer stopped after ${this.completedCount} of ${e.length} files`,"error",5e3);break}}catch(e){this.log(`Transfer error: ${e.message}`),this.showToast(`Transfer failed: ${e.message}`,"error",5e3)}finally{document.getElementById("sendBtn").disabled=!1,setTimeout(()=>{this.hideTransferProgress()},3e3)}}async read(e){const t=await this.device.transferIn(this.endpointIn,e);if(t.status&&"ok"!==t.status)throw new Error(`USB transferIn failed: ${t.status}`);if(!t.data)throw new Error("transferIn returned no data");return t}async write(e){const t=await this.device.transferOut(this.endpointOut,e);if(t.status&&"ok"!==t.status)throw new Error(`USB transferOut failed: ${t.status}`);return t}async get_send_header(){const e=await this.read(24),t=e.data.buffer.slice(e.data.byteOffset,e.data.byteOffset+24),s=SendPacket.fromBuffer(t);return s.verify(),[s.getCmd(),s.arg3,s.arg4]}async get_send_data_header(){const e=await this.read(24),t=e.data.buffer.slice(e.data.byteOffset,e.data.byteOffset+24),s=SendDataPacket.fromBuffer(t);return s.verify(),[s.getOffset(),s.getSize(),s.getCrc32c()]}async send_result(e,t=0,s=0){const i=ResultPacket.build(e,t,s);await this.write(i.toBuffer())}async file_transfer_loop(e){this.disableFileControls(!0),this.transferStartTime=Date.now(),this.lastUpdateTime=null,this.lastBytesTransferred=0,this.currentSpeed=0,this.speedSamples=[],this.averageSpeed=0;try{for(;;){const[t,s,i]=await this.get_send_data_header();if(0===t&&0===s){await this.send_result(0),this.log("Transfer complete"),this.markCoverage(e,Math.max(0,e.size-1),1);break}const n=e.slice(t,t+s),o=new Uint8Array(await n.arrayBuffer()),a=crc32c(0,o)>>>0;await this.send_result(0,o.length,a),await this.write(o),this.markCoverage(e,t,s)}}catch(e){this.log(`File loop error: ${e.message}`),this.showToast(`File transfer aborted: ${e.message}`,"error",4e3)}finally{this.disableFileControls(!1)}}disableFileControls(e){document.getElementById("addFilesBtn").disabled=e||!this.isConnected,document.getElementById("clearQueueBtn").disabled=e||0===this.fileQueue.length,document.querySelectorAll(".btn-remove").forEach(t=>t.disabled=e)}markCoverage(e,t,s){const i=65536;let n=this.coverage.get(e.name);if(n||(n=new Set,this.coverage.set(e.name,n)),s>0){const e=Math.floor(t/i),o=Math.floor((t+s-1)/i);for(let t=e;t<=o;t++)n.add(t)}const o=Math.min(n.size*i,e.size),a=e.size>0?Math.min(100,Math.floor(o/e.size*100)):100;return this.progressContext.total>0&&this.updateTransferProgress(this.completedCount,this.progressContext.total,o,e,a),a}updateUI(){document.getElementById("connectBtn").disabled=this.isConnected,document.getElementById("disconnectBtn").disabled=!this.isConnected,document.getElementById("addFilesBtn").disabled=!this.isConnected,document.getElementById("sendBtn").disabled=!this.isConnected||0===this.fileQueue.length,this.authorizedDevices.length>0&&this.updateAuthorizedDevicesUI()}showStatus(e,t){this.log(`[${t.toUpperCase()}] ${e}`)}log(e){const t=document.getElementById("logDiv"),s=(new Date).toLocaleTimeString();t.textContent+=`[${s}] ${e}\n`,t.scrollTop=t.scrollHeight}clearLog(){document.getElementById("logDiv").textContent="",this.log("Log cleared")}copyLog(){const e=document.getElementById("logDiv");navigator.clipboard.writeText(e.textContent).then(()=>{this.log("Log copied to clipboard")}).catch(e=>{this.log(`Failed to copy log: ${e}`)})}formatFileSize(e){if(0===e)return"0 Bytes";const t=["Bytes","KB","MB","GB"],s=Math.floor(Math.log(e)/Math.log(1024)),i=parseFloat((e/Math.pow(1024,s)).toFixed(2));return s>=2&&i<10?i.toFixed(1)+" "+t[s]:i+" "+t[s]}showConnectionToast(e,t="connect"){const s=document.getElementById("connectionToast"),i=document.getElementById("toastMessage"),n=s.querySelector(".toast-icon");this.toastTimeout&&clearTimeout(this.toastTimeout),i.textContent=e,"connect"===t?(n.textContent="🔗",s.className="connection-toast"):(n.textContent="🔌",s.className="connection-toast disconnect"),s.classList.add("show"),this.toastTimeout=setTimeout(()=>{this.hideConnectionToast()},4e3)}hideConnectionToast(){document.getElementById("connectionToast").classList.remove("show"),this.toastTimeout&&(clearTimeout(this.toastTimeout),this.toastTimeout=null)}showToast(e,t="info",s=4e3){const i=document.getElementById("connectionToast"),n=document.getElementById("toastMessage"),o=i.querySelector(".toast-icon");this.toastTimeout&&clearTimeout(this.toastTimeout),n.textContent=e;o.textContent={info:"ℹ️",success:"✅",error:"❌",connect:"🔗",disconnect:"🔌"}[t]||"ℹ️",i.className=`connection-toast ${t}`,i.classList.add("show"),this.toastTimeout=setTimeout(()=>{this.hideConnectionToast()},s)}showTransferProgress(e){document.getElementById("transferProgress").style.display="block",this.updateTransferProgress(0,e,0,null,0)}updateTransferProgress(e,t,s,i,n){this.updateProgressStats(s,i),this.updateProgressUI(e,t,s,i,n)}updateProgressStats(e,t){const s=Date.now();let i=0;if(t&&(i=t.size),this.lastUpdateTime){const t=(s-this.lastUpdateTime)/1e3;if(t>.1){const i=e-this.lastBytesTransferred;this.currentSpeed=i/t,this.speedSamples.push(this.currentSpeed),this.speedSamples.length>10&&this.speedSamples.shift(),this.averageSpeed=this.speedSamples.reduce((e,t)=>e+t,0)/this.speedSamples.length,this.lastUpdateTime=s,this.lastBytesTransferred=e}}else this.lastUpdateTime=s,this.lastBytesTransferred=e}updateProgressUI(e,t,s,i,n){document.getElementById("progressCounter").textContent=`${e} / ${t}`,this.updateProgressTitle(e,t,i),this.updateTimeAndSpeedInfo(s,i),this.updateProgressBar(n),document.getElementById("progressPercentage").textContent=`${Math.round(n)}%`}updateProgressTitle(e,t,s){const i=document.getElementById("progressTitle");if(s){const e=s.name.length>100?s.name.slice(0,97)+"...":s.name;i.textContent=`📄 ${e}`,document.getElementById("transferProgress").style.display="block"}else e===t&&t>0?(i.textContent="✅ All files completed!",setTimeout(()=>{document.getElementById("transferProgress").style.display="none"},3e3)):i.textContent="Waiting for next file..."}updateTimeAndSpeedInfo(e,t){const s=Date.now();let i=0;t&&(i=t.size);const n=(s-this.transferStartTime)/1e3;let o=0;if(this.averageSpeed>0){o=(i-e)/this.averageSpeed}document.getElementById("timeSpent").textContent=this.formatTime(n),document.getElementById("timeRemaining").textContent=o>0?this.formatTime(o):"--:--",document.getElementById("dataTransferred").textContent=this.formatFileSize(e),document.getElementById("currentSpeed").textContent=`${this.formatFileSize(this.averageSpeed)}/s`,document.getElementById("transferSpeed").textContent=`${this.formatFileSize(this.averageSpeed)}/s`}updateProgressBar(e){const t=document.getElementById("fileProgressBar");t&&(t.style.width=`${e}%`)}hideTransferProgress(){document.getElementById("transferProgress").style.display="none"}}let app;window.addEventListener("load",async()=>{app=new WebUSBFileTransfer}),navigator.usb?.addEventListener("disconnect",async e=>{console.log("USB device disconnected:",e.device),app?.device&&e.device===app.device&&(app.disconnectDevice(),await app.loadAuthorizedDevices(),await app.tryAutoConnect())}),navigator.usb?.addEventListener("connect",async e=>{console.log("USB device connected:",e.device),app&&(await app.loadAuthorizedDevices(),await app.tryAutoConnect())});