From 00874549edc7f8fa2793aa4c34417881f07889cf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 18 Nov 2025 18:49:50 +0000 Subject: [PATCH] Deploy minified webusb build --- assets/icon_16.png | Bin 0 -> 502 bytes assets/icon_180.png | Bin 0 -> 20260 bytes assets/icon_32.png | Bin 0 -> 1304 bytes assets/icon_48.png | Bin 0 -> 2421 bytes index.css | 1 + index.html | 1 + index.js | 1 + 7 files changed, 3 insertions(+) create mode 100644 assets/icon_16.png create mode 100644 assets/icon_180.png create mode 100644 assets/icon_32.png create mode 100644 assets/icon_48.png create mode 100644 index.css create mode 100644 index.html create mode 100644 index.js diff --git a/assets/icon_16.png b/assets/icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..1531c5ccd9b19fc7eefe4cc5fde080e19611c814 GIT binary patch literal 502 zcmVls7XXYR49?nk3DV_K@f#sb=Umu%&=oK{DZI%AV5g4a)KoS(58rx zoKtWOE&wrt42KpG5mx|-wbllMEU=B)o!Rc`DuUSyIHQuP_o<$q9@V2UPBP|1-~YEk zRdu-{=AK}Q04#3++@t$$mUME?<~TzumiiW7ag@ce_9KKToCr zs2+_~@#OJFb*czKEgR=To@a+gN2}HF?3vRaK7Lxea;eM*uikA^p8&$wo$pzaJla@) zv$+)%1i5o95RauvQHen3FaJVl_n60i97{NCc#y6qFO?FatFM*1E;aU~V;7 sbQT?HFc2n*pfM-`WiUBx2`<6KKX2~Jc;M_TN&o-=07*qoM6N<$f&+NiV*mgE literal 0 HcmV?d00001 diff --git a/assets/icon_180.png b/assets/icon_180.png new file mode 100644 index 0000000000000000000000000000000000000000..07d97af8c01fa9babd0af7097087cf2cb8b11f82 GIT binary patch literal 20260 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 literal 0 HcmV?d00001 diff --git a/assets/icon_32.png b/assets/icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..43e7123d20a8764b0ca57e9c9c0fa34f0beb8305 GIT binary patch literal 1304 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 literal 0 HcmV?d00001 diff --git a/index.css b/index.css new file mode 100644 index 0000000..044f890 --- /dev/null +++ b/index.css @@ -0,0 +1 @@ +@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(360deg)}}@keyframes pulse{0%,to{transform:scale(1)}50%{transform:scale(1.1)}}.download-spinner-text-wrap{display:flex;align-items:center;flex:1;min-width:0}#downloadSpinnerText{overflow-x:auto;white-space:nowrap;display:block;min-width:0;flex:1;scrollbar-width:none}#downloadSpinnerText::-webkit-scrollbar{display:none}#downloadSpinnerSpeed{flex-shrink:0;margin-left:12px;color:#32ffcf}.spinner{width:32px;height:32px;border:4px solid #163951;border-top:4px solid #32ffcf;border-radius:50%;animation:spin 1s linear infinite;margin-right:15px;display:inline-block;vertical-align:middle}.custom-select{background:linear-gradient(45deg,#32ffcf,#5cbeff);color:#111f28;border:0;padding:14px 26px;border-radius:8px;cursor:pointer;font-size:16px;font-weight:600;transition:transform .2s,box-shadow .2s;margin:5px;min-height:48px;appearance:none;-webkit-appearance:none;-moz-appearance:none;box-shadow:0 5px 15px rgba(50,255,207,.08)}.custom-select:focus{outline:0;box-shadow:0 0 0 2px #32ffcf}.custom-select option{background:#143144;color:#32ffcf;font-weight:600;font-size:16px}body{font-family:"Segoe UI",Tahoma,Geneva,Verdana,sans-serif;max-width:800px;margin:0 auto;padding:20px;background:linear-gradient(135deg,#111f28 0,#0b1519 100%);min-height:100dvh;min-height:-webkit-fill-available;color:#fbfbfb}.container{background:#143144;border-radius:15px;padding:20px;box-shadow:0 20px 40px rgba(0,0,0,.3);border:1px solid #335e77}@media (max-width:768px){body{padding:12px}.container{padding:15px;border-radius:12px}}.unsupported-splash{text-align:center;padding:30px 15px;background:#122430;border-radius:15px;border:2px solid #fa3a5d}.unsupported-splash h2{color:#fa3a5d;font-size:1.8em;margin-bottom:15px}.unsupported-splash p{font-size:1em;line-height:1.5;margin-bottom:12px;color:#bed0d6}.browser-link,button{background:linear-gradient(45deg,#32ffcf,#5cbeff);color:#111f28;border-radius:8px;font-weight:600;transition:transform .2s,box-shadow .2s}.browser-link{display:inline-block;padding:12px 24px;text-decoration:none;margin-top:15px}.browser-link:hover,button:hover{transform:translateY(-2px);box-shadow:0 5px 15px rgba(50,255,207,.4)}h1{text-align:center;margin-bottom:25px;background:linear-gradient(45deg,#32ffcf,#69ff8f);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;font-size:2.2em}@media (max-width:768px){h1{font-size:1.8em;margin-bottom:20px}.unsupported-splash{padding:20px 12px}.unsupported-splash h2{font-size:1.5em}}.section{margin-bottom:20px;padding:15px;border-radius:10px;background:#122430;border-left:4px solid #32ffcf}@media (max-width:768px){.section{padding:12px;margin-bottom:15px}}button{border:0;padding:14px 26px;cursor:pointer;font-size:16px;margin:5px;min-height:48px}@media (max-width:768px){button{width:100%;margin:8px 0;padding:16px}}button:disabled{background:#335e77;color:#bed0d6;cursor:not-allowed;transform:none}.device-info,.status{padding:10px;border-radius:5px;margin:10px 0;font-size:.95em}.status.success{background-color:rgba(105,255,143,.2);color:#69ff8f;border:1px solid #69ff8f}.status.error{background-color:rgba(250,58,93,.2);color:#fa3a5d;border:1px solid #fa3a5d}.status.info{background-color:rgba(92,190,255,.2);color:#5cbeff;border:1px solid #5cbeff}.device-info{background:#0b1519;padding:12px;border-radius:8px;border:1px solid #163951;color:#bed0d6;overflow-wrap:break-word}.transfer-text{color:#32ffcf;font-weight:600}.log{background:#071013;color:#bed0d6;padding:12px;border-radius:8px;font-family:"Courier New",monospace;max-height:200px;overflow-y:auto;margin-top:12px;border:1px solid #163951;white-space:pre-wrap;font-size:.9em}h3{color:#32ffcf;margin-top:0;font-size:1.3em}@media (max-width:768px){h3{font-size:1.2em}}.log-controls{display:flex;justify-content:flex-end;margin-top:10px;flex-wrap:wrap}.log-btn{background:#163951;color:#bed0d6;padding:8px 12px;font-size:14px;margin:4px;min-height:36px}.file-queue{margin-top:15px;background:#0b1519;border-radius:8px;padding:12px;border:1px solid #163951}.file-item{display:flex;justify-content:space-between;align-items:center;padding:10px;border-bottom:1px solid #163951;flex-wrap:wrap}.file-item:last-child{border-bottom:none}.file-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;margin-right:8px}.file-size{color:#bed0d6;margin:0 8px;font-size:.9em;white-space:nowrap}.file-actions{display:flex;gap:8px;flex-shrink:0}.queue-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;flex-wrap:wrap}.queue-title{font-weight:600;color:#32ffcf;margin-right:10px}.queue-count{background:#163951;color:#bed0d6;padding:4px 10px;border-radius:10px;font-size:.9em}.btn-remove{background:#ff3232;color:#fff;border:0;border-radius:4px;padding:6px 10px;cursor:pointer;font-size:13px;min-height:32px}.file-controls{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap}@media (max-width:768px){.file-controls{flex-direction:column}.file-controls button{width:100%}}.btn-add{background:linear-gradient(45deg,#69ff8f,#32ffcf)}.btn-clear{background:linear-gradient(45deg,#ff3232,#ff5c5c)}.device-list{background:#0b1519;border-radius:8px;padding:12px;margin:10px 0;border:1px solid #163951}.device-list-item{display:flex;justify-content:space-between;align-items:center;padding:10px;border-bottom:1px solid #163951;margin-bottom:8px;flex-wrap:wrap}.device-list-item:last-child{border-bottom:none;margin-bottom:0}.device-id,.device-name{overflow:hidden;text-overflow:ellipsis}.device-name{flex:1;color:#bed0d6;min-width:0;margin-right:8px}.device-id{color:#5cbeff;font-size:.85em;margin-right:10px;max-width:40%}.hidden{display:none}.connection-toast{position:fixed;top:20px;right:20px;background:linear-gradient(45deg,#32ffcf,#69ff8f);color:#111f28;padding:15px 20px;border-radius:10px;box-shadow:0 10px 30px rgba(50,255,207,.3);font-weight:600;font-size:14px;z-index:1000;transform:translateX(400px);transition:transform .3s ease-in-out,opacity .3s ease-in-out;opacity:0;display:flex;align-items:center;gap:10px;border:2px solid rgba(50,255,207,.5);max-width:350px}@media (max-width:768px){.connection-toast{top:10px;right:10px;left:10px;max-width:none;padding:12px 16px}}.connection-toast.show{transform:translateX(0);opacity:1}.connection-toast.disconnect{background:linear-gradient(45deg,#ff3232,#ff5c5c);color:#fff;box-shadow:0 10px 30px rgba(255,50,50,.3);border:2px solid rgba(255,50,50,.5)}.connection-toast.info{background:linear-gradient(45deg,#5cbeff,#32ffcf);color:#111f28;box-shadow:0 10px 30px rgba(92,190,255,.3);border:2px solid rgba(92,190,255,.5)}.connection-toast.success{background:linear-gradient(45deg,#69ff8f,#32ffcf);color:#111f28;box-shadow:0 10px 30px rgba(105,255,143,.3);border:2px solid rgba(105,255,143,.5)}.connection-toast.error{background:linear-gradient(45deg,#ff3232,#ff5c5c);color:#fff;box-shadow:0 10px 30px rgba(255,50,50,.3);border:2px solid rgba(255,50,50,.5)}.toast-icon{font-size:18px;animation:pulse 2s infinite}.toast-close{background:0 0;border:0;color:inherit;font-size:18px;cursor:pointer;padding:0;margin:0 0 0 10px;opacity:.7;transition:opacity .2s;flex-shrink:0}.toast-close:hover{opacity:1}.progress-bar-container{margin:10px 0}.progress-bar{height:8px;background:#163951;border-radius:4px;overflow:hidden;margin-bottom:4px}.progress-bar-fill{height:100%;background:linear-gradient(90deg,#32ffcf,#5cbeff);transition:width .3s ease}.time-info{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:.85em;margin-top:8px}.time-info div{display:flex;justify-content:space-between}@media (max-width:768px) and (orientation:portrait){.device-list-item{flex-direction:column;align-items:flex-start}.device-id{margin:5px 0;max-width:100%}.file-item{flex-direction:column;align-items:flex-start}.file-actions{margin-top:8px;width:100%;justify-content:flex-end}}@media (max-width:768px) and (orientation:landscape){.container{padding:12px}h1{font-size:1.6em}.section,body{padding:10px}} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..ae28997 --- /dev/null +++ b/index.html @@ -0,0 +1 @@ +Sphaira WebUSB File Transfer

Sphaira WebUSB File Transfer

🔗 Device Connected

Step 1: Connect USB Device

Step 2: Select Mode

Step 3: Select Files to Transfer

File Queue 0 files

Step 4: Transfer Files

Logs

\ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..9bb21de --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +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,n=0,i=0,o=0){this.magic=e,this.arg2=t,this.arg3=s,this.arg4=n,this.arg5=i,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 n=new SendPacket(MAGIC,e,t,s);return n.generateCrc32c(),n}getCmd(){return this.arg2}}class ResultPacket extends UsbPacket{static build(e,t=0,s=0){const n=new ResultPacket(MAGIC,e,t,s);return n.generateCrc32c(),n}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 n=Number(BigInt(e)>>32n&0xFFFFFFFFn),i=Number(0xFFFFFFFFn&BigInt(e)),o=new SendDataPacket(MAGIC,n,i,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 n=0;const i=s.length;for(;n>>8,t=e[255&(t^s[n+1])]^t>>>8,t=e[255&(t^s[n+2])]^t>>>8,t=e[255&(t^s[n+3])]^t>>>8;for(;n>>8;return(4294967295^t)>>>0}})();class WebUSBFileTransfer{maybeStartDownloadLoop(){"download"===document.getElementById("modeSelect").value&&this.isConnected&&this.selectedDownloadDirHandle&&!this.downloadLoopActive&&this.startDownloadLoop()}async startDownloadLoop(){if(!this.downloadLoopActive){if(this.downloadLoopActive=!0,!this.selectedDownloadDirHandle)return this.showToast("No download folder selected.","error",4e3),void(this.downloadLoopActive=!1);if(!this.isConnected)return this.showToast("Device not connected.","error",4e3),void(this.downloadLoopActive=!1);this.log("Starting download command loop...");try{for(;;){const[e,t,s]=await this.get_send_header();if(0===e){await this.send_result(0),this.log("Received CMD_QUIT, exiting download loop.");break}if(1!==e){await this.send_result(1),this.log(`Unknown command (${e}), exiting.`);break}{await this.send_result(0);const e=new Uint8Array(await this.read(t).then(e=>e.data.buffer)),s=new TextDecoder("utf-8").decode(e);this.log(`Receiving file: ${s}`);const n=await this.createFileInDir(this.selectedDownloadDirHandle,s);console.log(`Created file handle for: ${s}`),await this.send_result(0),console.log("Acknowledged file creation, starting data transfer..."),await this.downloadFileData(n)}}}catch(e){this.log("Download loop error: "+e.message),this.showToast("Download failed: "+e.message,"error",5e3)}finally{this.downloadLoopActive=!1}}}sanitizePathSegment(e){return e&&"."!==e&&".."!==e?e.replace(/[\\/:*?"<>|]/g,"_"):null}async createFileInDir(e,t){this.log(`Creating file in directory: ${t}`);const s=t.split("/");let n=e;for(let e=0;e>>0;if(c!==u){this.log(`CRC32C mismatch at offset ${e}: want ${c}, got ${u}`),await this.send_result(1);continue}e===s?(await t.write(h),s+=h.length):await t.write({type:"write",position:e,data:h}),n=Math.max(n,e+h.length);const g=Date.now();if(g-o>200){const e=(g-i)/1e3,t=e>0?n/e:0;this.updateDownloadSpinner(r,t),o=g,a=n}await this.send_result(0)}this.updateDownloadSpinner(r,0,!0),await t.close(),this.hideDownloadSpinner(),this.log("File written successfully.")}showDownloadSpinner(e,t){const s=document.getElementById("downloadSpinner");s&&(this.updateDownloadSpinner(e,t),s.style.display="flex")}updateDownloadSpinner(e,t,s=!1){const n=document.getElementById("downloadSpinnerText"),i=document.getElementById("downloadSpinnerSpeed");n&&(s?(n.textContent=`Finishing write for "${e}"`,n.scrollLeft=0,i&&(i.textContent="")):(n.textContent=`Receiving "${e}"`,i&&(i.textContent=t>0?`${this.formatFileSize(t)}/s`:"")))}hideDownloadSpinner(){const e=document.getElementById("downloadSpinner");e&&(e.style.display="none")}async handleDirectoryPicker(){if(window.showDirectoryPicker)try{const e=await window.showDirectoryPicker();this.selectedDownloadDirHandle=e,document.getElementById("selectedFolderName").textContent=`Selected: ${e.name}`,this.log(`Selected download folder: ${e.name}`),this.maybeStartDownloadLoop()}catch(e){"AbortError"!==e.name&&this.showToast("Failed to select folder: "+e.message,"error",5e3)}else this.showToast("Your browser does not support the File System Access API.","error",5e3)}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 n=e.productName||"Unknown Device",i=`${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
${n} ${e.serialNumber}
\n
${i}
\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()),document.getElementById("modeSelect").addEventListener("change",e=>this.handleModeChange(e)),document.getElementById("pickFolderBtn").addEventListener("click",async()=>{await this.handleDirectoryPicker()})}handleModeChange(e){const t=e.target.value,s=document.getElementById("uploadStep3Section"),n=document.getElementById("uploadStep4Section"),i=document.getElementById("downloadStep3Section"),o=document.getElementById("downloadStep4Section");"upload"===t?(s.style.display="",n.style.display="",i.style.display="none",o.style.display="none"):(s.style.display="none",n.style.display="none",i.style.display="",o.style.display="")}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 n of t){const t=n.name.toLowerCase();s.some(e=>t.endsWith(e))?this.fileQueue.some(e=>e.name===n.name&&e.size===n.size)||(this.fileQueue.push(n),e++):this.log(`Skipping unsupported file type: ${n.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
${n.name}
\n
${this.formatFileSize(n.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"),this.maybeStartDownloadLoop()}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 n=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,n.length),await this.write(n);;)try{const[t,s,n]=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 n=e.length,i=s+1;this.progressContext={current:i,total:n},this.log(`Opening file [${i}/${n}]: ${t.name} (${this.formatFileSize(t.size)})`),this.showToast(`📤 Transferring file ${i} of ${n}: ${t.name}`,"info",3e3),this.updateTransferProgress(this.completedCount,n,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 ${i} of ${n} completed`,"success",2e3),this.updateTransferProgress(this.completedCount,n,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 n=ResultPacket.build(e,t,s);await this.write(n.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,n]=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 i=e.slice(t,t+s),o=new Uint8Array(await i.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 n=65536;let i=this.coverage.get(e.name);if(i||(i=new Set,this.coverage.set(e.name,i)),s>0){const e=Math.floor(t/n),o=Math.floor((t+s-1)/n);for(let t=e;t<=o;t++)i.add(t)}const o=Math.min(i.size*n,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)),n=parseFloat((e/Math.pow(1024,s)).toFixed(2));return s>=2&&n<10?n.toFixed(1)+" "+t[s]:n+" "+t[s]}showConnectionToast(e,t="connect"){const s=document.getElementById("connectionToast"),n=document.getElementById("toastMessage"),i=s.querySelector(".toast-icon");this.toastTimeout&&clearTimeout(this.toastTimeout),n.textContent=e,"connect"===t?(i.textContent="🔗",s.className="connection-toast"):(i.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 n=document.getElementById("connectionToast"),i=document.getElementById("toastMessage"),o=n.querySelector(".toast-icon");this.toastTimeout&&clearTimeout(this.toastTimeout),i.textContent=e;o.textContent={info:"â„šī¸",success:"✅",error:"❌",connect:"🔗",disconnect:"🔌"}[t]||"â„šī¸",n.className=`connection-toast ${t}`,n.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,n,i){this.updateProgressStats(s,n),this.updateProgressUI(e,t,s,n,i)}updateProgressStats(e,t){const s=Date.now();let n=0;if(t&&(n=t.size),this.lastUpdateTime){const t=(s-this.lastUpdateTime)/1e3;if(t>.1){const n=e-this.lastBytesTransferred;this.currentSpeed=n/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,n,i){document.getElementById("progressCounter").textContent=`${e} / ${t}`,this.updateProgressTitle(e,t,n),this.updateTimeAndSpeedInfo(s,n),this.updateProgressBar(i),document.getElementById("progressPercentage").textContent=`${Math.round(i)}%`}updateProgressTitle(e,t,s){const n=document.getElementById("progressTitle");if(s){const e=s.name.length>100?s.name.slice(0,97)+"...":s.name;n.textContent=`📄 ${e}`,document.getElementById("transferProgress").style.display="block"}else e===t&&t>0?(n.textContent="✅ All files completed!",setTimeout(()=>{document.getElementById("transferProgress").style.display="none"},3e3)):n.textContent="Waiting for next file..."}updateTimeAndSpeedInfo(e,t){const s=Date.now();let n=0;t&&(n=t.size);const i=(s-this.transferStartTime)/1e3;let o=0;if(this.averageSpeed>0){o=(n-e)/this.averageSpeed}document.getElementById("timeSpent").textContent=this.formatTime(i),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())}); \ No newline at end of file