| 285 | // ─── File drop on URL input ─── |
| 286 | |
| 287 | function wireFileDrop() { |
| 288 | const urlWrap = document.querySelector(".url-wrap"); |
| 289 | const urlInput = document.getElementById("url"); |
| 290 | const fileInput = document.getElementById("fileInput"); |
| 291 | const filePill = document.getElementById("filePill"); |
| 292 | const fileName = document.getElementById("fileName"); |
| 293 | const fileSize = document.getElementById("fileSize"); |
| 294 | const fileClear = document.getElementById("fileClear"); |
| 295 | if (!urlWrap || !urlInput || !fileInput || !filePill) return; |
| 296 | |
| 297 | function formatBytes(n) { |
| 298 | return n < 1024 * 1024 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`; |
| 299 | } |
| 300 | |
| 301 | const MAX_UPLOAD_BYTES = 400 * 1024 * 1024; // must match server _MAX_UPLOAD_BYTES |
| 302 | |
| 303 | function applyFile(file) { |
| 304 | if (!file) return; |
| 305 | const lower = file.name.toLowerCase(); |
| 306 | if (!lower.endsWith(".mp3") && !lower.endsWith(".wav") && !lower.endsWith(".flac") && |
| 307 | !lower.endsWith(".mp4") && !lower.endsWith(".m4a")) { |
| 308 | showError("Only MP3, WAV, FLAC, MP4, and M4A files are supported."); |
| 309 | return; |
| 310 | } |
| 311 | if (file.size > MAX_UPLOAD_BYTES) { |
| 312 | showError(`File is too large (${formatBytes(file.size)}). Maximum is 400 MB.`); |
| 313 | return; |
| 314 | } |
| 315 | if (fileName) fileName.textContent = file.name; |
| 316 | if (fileSize) fileSize.textContent = formatBytes(file.size); |
| 317 | filePill.classList.remove("hidden"); |
| 318 | urlWrap.classList.add("has-file"); |
| 319 | // Cache the File object directly on the element so job.js can always |
| 320 | // retrieve it even after the browser clears fileInput.files following |
| 321 | // a fetch() submission (known WKWebView / Chromium behaviour). |
| 322 | fileInput._file = file; |
| 323 | const dt = new DataTransfer(); |
| 324 | dt.items.add(file); |
| 325 | fileInput.files = dt.files; |
| 326 | urlInput.value = ""; |
| 327 | urlInput.removeAttribute("required"); |
| 328 | } |
| 329 | |
| 330 | function clearFile() { |
| 331 | filePill.classList.add("hidden"); |
| 332 | urlWrap.classList.remove("has-file"); |
| 333 | fileInput._file = null; |
| 334 | fileInput.value = ""; |
| 335 | urlInput.setAttribute("required", ""); |
| 336 | } |
| 337 | |
| 338 | fileClear?.addEventListener("click", clearFile); |
| 339 | |
| 340 | urlWrap.addEventListener("dragover", (e) => { |
| 341 | if (!e.dataTransfer.types.includes("Files")) return; |
| 342 | e.preventDefault(); |
| 343 | e.dataTransfer.dropEffect = "copy"; |
| 344 | urlWrap.classList.add("drag-over"); |