1let uploading_count = 0; 2let uploaded_count = 0; 3let uploadFailed_count = 0; 4const maxFileSize = 64 * 1024 * 1024; // 64 MB 5function updateCounts() { 6 if (uploading_count < 0) uploading_count = 0; 7 if (uploaded_count < 0) uploaded_count = 0; 8 if (uploadFailed_count < 0) uploadFailed_count = 0; 9 document.getElementById('uploading_count').textContent = uploading_count; 10 document.getElementById('uploaded_count').textContent = uploaded_count; 11 document.getElementById('uploadFailed_count').textContent = uploadFailed_count; 12} 13 14const allowedTypes = [ 15 'image/', 16 'video/', 17 'audio/', 18 'application/pdf', 19 'text/plain' 20]; 21function getR18Cookie() { 22 const m = document.cookie.match(/(^| )nsfw=([^;]+)/); 23 return m ? m[2] : null; 24} 25 26function setR18Cookie(val) { 27 if (val === 'auto') { 28 document.cookie = 'nsfw=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 29 } else { 30 const exp = new Date(); 31 exp.setFullYear(exp.getFullYear() + 1); 32 document.cookie = `nsfw=${val}; expires=${exp.toUTCString()}; path=/;`; 33 } 34} 35 36function getR18Value() { 37 const el = document.getElementById('r18Switch'); 38 if (!el) return '0'; 39 if (el.tagName === 'SELECT') { 40 return el.value === 'on' ? '1' : '0'; 41 } 42 return el.checked ? '1' : '0'; 43} 44// nsfwModel 45const NSFW_SUPPORTED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp']; 46const NSFW_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime']; 47 48 49let nsfwWorker = null; 50let workerReqId = 0; 51const pendingWorkerRequests = new Map(); 52 53function getNsfwWorker() { 54 if (!nsfwWorker) { 55 nsfwWorker = new Worker('/js/nsfw-worker.js'); 56 57 nsfwWorker.onmessage = (e) => { 58 const { id, predictions, error } = e.data; 59 const pending = pendingWorkerRequests.get(id); 60 if (!pending) return; 61 pendingWorkerRequests.delete(id); 62 if (error) pending.reject(new Error(error)); 63 else pending.resolve(predictions); 64 }; 65 66 nsfwWorker.onerror = (e) => { 67 for (const [, pending] of pendingWorkerRequests) { 68 pending.reject(new Error(e.message || 'NSFW worker error')); 69 } 70 pendingWorkerRequests.clear(); 71 }; 72 } 73 return nsfwWorker; 74} 75 76function classifyBitmapInWorker(bitmap) { 77 return new Promise((resolve, reject) => { 78 if (typeof Worker === 'undefined') { 79 reject(new Error('Worker not supported')); 80 return; 81 } 82 const id = ++workerReqId; 83 pendingWorkerRequests.set(id, { resolve, reject }); 84 getNsfwWorker().postMessage({ id, cmd: 'classify', bitmap }, [bitmap]); 85 }); 86} 87 88async function extractVideoFrame(file, timeRatios = [0.1, 0.3, 0.5]) { 89 return new Promise((resolve) => { 90 const video = document.createElement('video'); 91 const url = URL.createObjectURL(file); 92 video.muted = true; 93 video.preload = 'metadata'; 94 video.src = url; 95 96 const canvases = []; 97 let idx = 0; 98 99 const seekNext = () => { 100 if (idx >= timeRatios.length) { 101 URL.revokeObjectURL(url); 102 resolve(canvases); 103 return; 104 } 105 video.currentTime = Math.max(1, video.duration * timeRatios[idx]); 106 }; 107 108 video.addEventListener('loadedmetadata', () => seekNext()); 109 110 video.addEventListener('seeked', () => { 111 const canvas = document.createElement('canvas'); 112 canvas.width = video.videoWidth; 113 canvas.height = video.videoHeight; 114 canvas.getContext('2d').drawImage(video, 0, 0); 115 canvases.push(canvas); 116 idx++; 117 seekNext(); 118 }); 119 120 video.addEventListener('error', () => { 121 URL.revokeObjectURL(url); 122 resolve(canvases); 123 }); 124 125 setTimeout(() => { 126 URL.revokeObjectURL(url); 127 resolve(canvases); 128 }, 15000); 129 }); 130} 131 132async function detectNsfwSilently(file) { 133 try { 134 if (NSFW_SUPPORTED_TYPES.includes(file.type)) { 135 let bitmap; 136 try { 137 bitmap = await createImageBitmap(file); 138 } catch (e) { 139 return false; 140 } 141 142 const predictions = await classifyBitmapInWorker(bitmap); 143 const scoreOf = name => predictions.find(p => p.className === name)?.probability || 0; 144 const pornScore = scoreOf('Porn'); 145 const sexyScore = scoreOf('Sexy'); 146 const hentaiScore = scoreOf('Hentai'); 147 const riskyScore = pornScore + hentaiScore; 148 //const isNsfw = riskyScore > 0.9 || pornScore > 0.5; 149 const isNsfw = pornScore > 0.5; 150 151 console.log(`Porn: ${pornScore.toFixed(3)}\tSexy: ${sexyScore.toFixed(3)}\tHentai: ${hentaiScore.toFixed(3)}\t=> ${isNsfw ? 'NSFW' : 'Safe'}`); 152 153 return isNsfw; 154 155 } else if (NSFW_VIDEO_TYPES.includes(file.type)) { 156 const frames = await extractVideoFrame(file); 157 if (!frames.length) return false; 158 159 let maxRisky = 0; 160 let maxPorn = 0; 161 for (const canvas of frames) { 162 const bitmap = await createImageBitmap(canvas); 163 const predictions = await classifyBitmapInWorker(bitmap); 164 const scoreOf = name => predictions.find(p => p.className === name)?.probability || 0; 165 const pornScore = scoreOf('Porn'); 166 const risky = pornScore + scoreOf('Hentai'); 167 if (risky > maxRisky) maxRisky = risky; 168 if (pornScore > maxPorn) maxPorn = pornScore; 169 } 170 //const isNsfwVideo = maxRisky > 0.75 || maxPorn > 0.5; 171 const isNsfwVideo = maxPorn > 0.5;
172 console.log('NSFW video max risky score:', maxRisky.toFixed(3), '- Porn max:', maxPorn.toFixed(3), '-', isNsfwVideo ? 'NSFW' : 'Safe'); 173 return isNsfwVideo; 174 175 } else { 176 return false; // audio/PDF/TXT 177 } 178 } catch (e) { 179 console.warn('NSFW check failed, skipping:', e); 180 return false; 181 } 182} 183 184if (document.cookie.includes('a_t=')) { 185 fetch('/auth/getuser') 186 .then(response => { 187 if (response.status === 200) { 188 return response.json(); 189 } 190 return null; 191 }) 192 .then(data => { 193 if (data && data.username) { 194 const loginLink = document.querySelector('a[href="/login"]'); 195 const loginDiv = loginLink.querySelector('.info-container'); 196 if (loginDiv) { 197 const wrapper = document.createElement('div'); 198 wrapper.className = loginDiv.className; 199 wrapper.setAttribute('data-v-3c03dd8c', ''); 200 wrapper.style.left = '20px'; 201 wrapper.style.borderRadius = '50%'; 202 wrapper.style.display = 'inline-flex'; 203 wrapper.style.alignItems = 'center'; 204 wrapper.style.justifyContent = 'center'; 205 wrapper.style.overflow = 'visible'; 206 207 const img = document.createElement('img'); 208 img.src = `/avatar/${data.avatar}`; 209 img.style.width = '100%'; 210 img.style.height = '100%'; 211 img.style.borderRadius = '50%'; 212 img.style.display = 'block'; 213 img.style.position = 'static'; 214 img.style.transform = 'none'; 215 img.style.margin = '0'; 216 img.style.padding = '0'; 217 218 wrapper.appendChild(img); 219 220 const noticeCount = parseInt(data.notice, 10); 221 if (!isNaN(noticeCount) && noticeCount > 0) { 222 if (!document.getElementById('avatar-notice-style')) { 223 const style = document.createElement('style'); 224 style.id = 'avatar-notice-style'; 225 style.textContent = ` 226 .urusai-wrapper-badge { 227 position: absolute !important; 228 top: -4px !important; 229 right: -6px !important; 230 background-color: #ef4444 !important; 231 color: #ffffff !important; 232 font-size: 10px !important; 233 font-weight: bold !important; 234 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important; 235 line-height: 1 !important; 236 padding: 2px 5px !important; 237 min-width: 12px !important; 238 height: 12px !important; 239 border-radius: 10px !important; 240 border: 1.5px solid #ffffff !important; 241 z-index: 99999 !important; 242 pointer-events: none !important; 243 display: flex !important; 244 align-items: center !important; 245 justify-content: center !important; 246 white-space: nowrap !important; 247 transform: none !important; 248 } 249 html.dark .urusai-wrapper-badge { 250 border-color: #1a1a1a !important; 251 } 252 `; 253 document.head.appendChild(style); 254 } 255 256 const badge = document.createElement('div'); 257 badge.className = 'urusai-wrapper-badge'; 258 badge.textContent = noticeCount > 99 ? '99+' : noticeCount; 259 wrapper.appendChild(badge); 260 } 261 262 loginDiv.replaceWith(wrapper); 263 loginLink.href = `/u/${data.username}`; 264 } 265 } 266 }) 267 .catch(err => { 268 console.error('Fetch error:', err); 269 }); 270} 271function isValidFileType(file) { 272 return allowedTypes.some(type => { 273 if (type.endsWith('/')) { 274 return file.type.startsWith(type); 275 } else { 276 return file.type === type; 277 } 278 }); 279} 280async function getFileSHA256(file) { 281 const arrayBuffer = await file.arrayBuffer(); 282 const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer); 283 const hashArray = Array.from(new Uint8Array(hashBuffer)); 284 const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); 285 return hashHex; 286} 287 288function escapeHTML(str) { 289 return str.replace(/&/g, '&') 290 .replace(/</g, '<') 291 .replace(/>/g, '>') 292 .replace(/"/g, '"') 293 .replace(/'/g, '''); 294} 295 296function uploadFileWithProgress(url, formData, onProgress, retryCount = 0) { 297 return new Promise((resolve, reject) => { 298 const xhr = new XMLHttpRequest(); 299 xhr.open('POST', url); 300 xhr.withCredentials = true; 301 302 xhr.upload.onprogress = event => { 303 if (event.lengthComputable) { 304 onProgress(event.loaded / event.total * 100); 305 } 306 }; 307 308 xhr.onload = () => { 309 if (xhr.status >= 200 && xhr.status < 300) { 310 try { 311 const resp = JSON.parse(xhr.responseText); 312 resolve(resp); 313 } catch { 314 reject(new Error(langData['parsing_failed'] || 'Response parsing failed')); 315 } 316 } else { 317 //reject(new Error(`HTTP Error ${xhr.status}`)); 318 /*try { 319 const resp = JSON.parse(xhr.responseText); 320 const msg = (xhr.status === 400 && resp.code === 'SHA256_MISMATCH') 321 ? (langData['sha256_mismatch'] || resp.message) 322 : (resp.message || `HTTP Error ${xhr.status}`); 323 const err = new Error(msg); 324 err.code = resp.code; 325 reject(err); 326 } catch { 327 reject(new Error(`HTTP Error ${xhr.status}`)); 328 }*/ 329 try { 330 const resp = JSON.parse(xhr.responseText); 331 const isShaMismatch = xhr.status === 400 && resp.code === 'SHA256_MISMATCH'; 332 const msg = isShaMismatch 333 ? (langData['sha256_mismatch'] || resp.message || `HTTP Error ${xhr.status}`) 334 : (resp.message || `HTTP Error ${xhr.status}`); 335 336 if (isShaMismatch && retryCount < 2) { 337 uploadFileWithProgress(url, formData, onProgress, retryCount + 1) 338 .then(resolve) 339 .catch(reject); 340 } else { 341 const err = new Error(msg); 342 err.code = resp.code; 343 reject(err); 344 } 345 } catch { 346 reject(new Error(`HTTP Error ${xhr.status}`)); 347 } 348 } 349 }; 350 351 xhr.onerror = () => reject(new Error(langData['error_network'] || 'Network error or upload restricted. Please try again later.')); 352 353 xhr.send(formData); 354 }); 355} 356 357function generateInputRow(label, value) { 358/*if (label === 'URL' && langData['url_ptt']) { 359 label = langData['url_ptt']; 360}*/ 361 return `
362 <div class="upload-list-item-url-row"> 363 <div class="el-input el-input--default el-input-group el-input-group--prepend"> 364 <div class="el-input-group__prepend" style="width: 80px; overflow: hidden;">${label}</div> 365 <div class="el-input__wrapper"> 366 <input class="el-input__inner" type="text" readonly autocomplete="off" tabindex="0" value="${escapeHTML(value)}"> 367 </div> 368 </div> 369 </div> 370 `; 371} 372 373function generatePreviewHTML(d, mimeType) { 374 const url = d.url_direct; 375 if (mimeType.startsWith('image/')) { 376 return `<img style="width: 20vw; border-radius: 20px;" src="${url}">`; 377 } else if (mimeType.startsWith('video/')) { 378 return `<video controls style="width: 20vw; border-radius: 20px;"><source src="${url}" type="${mimeType}">Video</video>`; 379 } else if (mimeType.startsWith('audio/')) { 380 return `<audio controls style="width: 20vw; border-radius: 20px;"><source src="${url}" type="${mimeType}">Audio</audio>`; 381 } else if (mimeType === 'application/pdf') { 382 return `<img style="width: 20vw; border-radius: 20px;" src="/img/pdf.png">`; 383 } else if (mimeType === 'text/plain') { 384 return `<img style="width: 20vw; border-radius: 20px;" src="/img/txt.png">`; 385 } else { 386 return `<img style="width: 20vw; border-radius: 20px;" src="/img/others.png">`; 387 } 388} 389 390function generateBBCode(d, mimeType) { 391 const preview = d.url_preview; 392 const direct = d.url_direct; 393 if (mimeType.startsWith('image/')) { 394 return `[url=${preview}][img]${direct}[/img][/url]`; 395 } else if (mimeType.startsWith('video/')) { 396 return `[url=${preview}][video]${direct}[/video][/url]`; 397 } else if (mimeType.startsWith('audio/')) { 398 return `[url=${preview}][audio]${direct}[/audio][/url]`; 399 } else if (mimeType === 'application/pdf') { 400 return `[url=${preview}]${direct}[/url]`; 401 } else if (mimeType === 'text/plain') { 402 return `[url=${preview}]${direct}[/url]`; 403 } else { 404 return `[url=${preview}]${direct}[/url]`; 405 } 406} 407 408function generateHTMLCode(d, mimeType) { 409 const preview = d.url_preview; 410 const direct = d.url_direct; 411 if (mimeType.startsWith('image/')) { 412 return `<a href="${preview}" target="_blank"><img src="${direct}"></a>`; 413 } else if (mimeType.startsWith('video/')) { 414 return `<a href="${preview}"><video controls><source src="${direct}" type="${mimeType}"></video></a>`; 415 } else if (mimeType.startsWith('audio/')) { 416 return `<a href="${preview}"><audio controls><source src="${direct}" type="${mimeType}"></audio></a>`; 417 } else if (mimeType === 'application/pdf') { 418 return `<a href="${preview}"><iframe src="${direct}" type="application/pdf"></iframe></a>`; 419 } else if (mimeType === 'text/plain') { 420 return `<a href="${preview}" target="_blank">${direct}</a>`; 421 } else { 422 return `<a href="${preview}" target="_blank">${direct}</a>`; 423 } 424} 425 426function createUploadCard(data, file) { 427 const resultDiv = document.getElementById('result'); 428 if (!resultDiv) return; 429 const d = data.data; 430 const mimeType = file.type; 431 432 const card = document.createElement('div'); 433 card.classList.add('upload-list-item'); 434 card.setAttribute('span', '8'); 435 card.setAttribute('data-v-616bcf87', ''); 436 card.setAttribute('data-fileid', d.id); 437 card.innerHTML = ` 438 <a href="${d.url_preview}" target="_blank" class="upload-list-item-preview">${generatePreviewHTML(d, mimeType)}</a> 439 <div class="upload-list-item-content">
440 <span class="el-text is-truncated upload-list-item-name">${file.name}</span> 441 <div class="upload-list-item-url"> 442 ${generateInputRow("Preview", d.url_preview)} 443 ${generateInputRow("URL", d.url_direct)} 444 ${generateInputRow("BBCode", generateBBCode(d, mimeType))} 445 ${generateInputRow("HTML", generateHTMLCode(d, mimeType))} 446 ${generateInputRow("Delete", d.url_delete)} 447 </div> 448 </div> 449 `; 450 resultDiv.appendChild(card); 451 452 const inputs = card.querySelectorAll('.el-input__inner'); 453 inputs.forEach(input => { 454 input.addEventListener('click', () => copyText(input.value.trim())); 455 }); 456} 457 458function createUploadCardElement(data, file) { 459 const d = data.data; 460 const mimeType = file.type; 461 462 const card = document.createElement('div'); 463 card.classList.add('upload-list-item'); 464 card.setAttribute('span', '8'); 465 card.setAttribute('data-v-616bcf87', ''); 466 card.setAttribute('data-fileid', d.id); 467 card.innerHTML = ` 468 <a href="${d.url_preview}" target="_blank" class="upload-list-item-preview">${generatePreviewHTML(d, mimeType)}</a> 469 <div class="upload-list-item-content">
470 <span class="el-text is-truncated upload-list-item-name">${file.name}</span> 471 <div class="upload-list-item-url"> 472 ${generateInputRow("Preview", d.url_preview)} 473 ${generateInputRow("URL", d.url_direct)} 474 ${generateInputRow("BBCode", generateBBCode(d, mimeType))} 475 ${generateInputRow("HTML", generateHTMLCode(d, mimeType))} 476 ${generateInputRow("Delete", d.url_delete)} 477 </div> 478 </div> 479 `; 480 481 card.querySelectorAll('.el-input__inner').forEach(input => { 482 input.addEventListener('click', () => copyText(input.value.trim())); 483 }); 484 485 return card; 486} 487 488 489function copied() { 490 const toast = document.getElementById('success'); 491 document.getElementById('success_message').innerHTML = langData['copied']; 492 toast.style.removeProperty('display'); 493 setTimeout(() => { 494 toast.style.display = 'none'; 495 }, 5000); 496} 497 498function copy_failed() { 499 const toast = document.getElementById('error'); 500 document.getElementById('error_message').innerHTML = langData['copy_failed']; 501 toast.style.removeProperty('display'); 502 setTimeout(() => { 503 toast.style.display = 'none'; 504 }, 5000); 505} 506 507const copyText = async (text) => { 508 if (navigator.clipboard && window.isSecureContext) { 509 try { 510 await navigator.clipboard.writeText(text); 511 copied(); 512 } catch (e) { 513 fallbackCopy(text); 514 } 515 } else { 516 fallbackCopy(text); 517 } 518}; 519 520const fallbackCopy = (text) => { 521 const textArea = document.createElement('textarea'); 522 textArea.value = text; 523 textArea.style.position = 'fixed'; 524 textArea.style.left = '-9999px'; 525 document.body.appendChild(textArea); 526 textArea.focus(); 527 textArea.select(); 528 529 try { 530 const successful = document.execCommand('copy'); 531 if (successful) { 532 copied(); 533 } else { 534 copy_failed(); 535 } 536 } catch (err) { 537 copy_failed(); 538 console.error(err); 539 } 540 541 document.body.removeChild(textArea); 542}; 543 544async function uploadSingleFile(file) { 545 uploading_count++; 546 updateCounts(); 547 548 document.querySelector('.upload-list-card')?.style.removeProperty('display'); 549 550 const placeholderCard = document.createElement('div'); 551 placeholderCard.classList.add('upload-list-item'); 552 placeholderCard.innerHTML = ` 553 <strong>${file.name}</strong> 554 <div class="progress-bar-container" style="background:#eee; border-radius:4px; overflow:hidden; height:16px; margin:0.5em 0;"> 555 <div class="progress-bar" style="background:#546e7a; height:100%; width:0%; transition:width 0.2s ease; font-size: 12px;"></div> 556 </div> 557 <div class="upload-status"></div> 558 `; 559 document.getElementById('result').appendChild(placeholderCard); 560 561 const progressBar = placeholderCard.querySelector('.progress-bar'); 562 const statusDiv = placeholderCard.querySelector('.upload-status'); 563 564 //const r18Checked = document.getElementById('r18Switch').checked ? '1' : '0'; 565 let r18Checked = getR18Value(); 566 const r18El = document.getElementById('r18Switch'); 567 const isAuto = r18El?.tagName === 'SELECT' && r18El.value === 'auto'; 568 if (isAuto) { 569 progressBar.style.width = '100%'; 570 571 let analyzeDotCount = 0; 572 progressBar.textContent = 'Analyzing content'; 573 const analyzeInterval = setInterval(() => { 574 analyzeDotCount = (analyzeDotCount % 3) + 1; 575 progressBar.textContent = 'Analyzing content' + '.'.repeat(analyzeDotCount); 576 }, 400); 577 578 await new Promise(r => requestAnimationFrame(() => setTimeout(r, 0))); 579 const isNsfw = await detectNsfwSilently(file); 580 r18Checked = isNsfw ? '1' : '0'; 581 582 clearInterval(analyzeInterval); 583 } 584 585 progressBar.style.background = '#4caf50'; 586 const formData = new FormData(); 587 let fileToUpload = file; 588 const needCompress = document.getElementById('compressSwitch').checked ? '1' : '0'; 589 const needCleanExif = document.getElementById('exifSwitch').checked ? '1' : '0'; 590 await new Promise(r => requestAnimationFrame(() => setTimeout(r, 0))); 591 progressBar.style.width = '5%'; 592 progressBar.textContent = "Preparing..."; 593 594 if (needCompress === '1' || needCleanExif === '1') { 595 const type = file.type.toLowerCase(); 596 const isJpeg = type.includes('jpeg') || type.includes('jpg'); 597 const isPngWebp = type.includes('png') || type.includes('webp'); 598 const isHeic = type === 'image/heic' || file.name.toLowerCase().endsWith('.heic'); 599 600 if (isJpeg || isPngWebp || isHeic) { 601 try { 602 let isAnimated = false; 603 if (file.type === 'image/webp') { 604 isAnimated = await isAnimatedWebP(file); 605 } 606 607 if (isAnimated) { 608 console.log("WebP: isAnimated"); 609 } else { 610 const canvasBlocked = await isCanvasBlocked(); 611 612 if (needCompress === '1' && !canvasBlocked) { 613 progressBar.textContent = "Smart Compressing..."; 614 progressBar.style.background = '#005AB5'; 615 616 let sourceFile = file; 617 if (isHeic) {
618 progressBar.textContent = "Converting HEIC for compression..."; 619 await loadScript('/js/heic2any.min.js'); 620 sourceFile = await processHeicToJpeg(file); 621 } 622 623 const compressed = await smartCompress(sourceFile, 800, (percent) => { 624 progressBar.style.width = percent + '%'; 625 progressBar.textContent = `Compressing... ${percent}%`; 626 }); 627 628 if (compressed && (compressed.size < file.size || file.size > 1024 * 1024)) { 629 fileToUpload = compressed; 630 } 631 } 632 633 if (needCleanExif === '1' && fileToUpload === file) { 634 progressBar.textContent = "Scanning for privacy data..."; 635 progressBar.style.background = '#4CAE4F'; 636 const tags = await ExifReader.load(file); 637 638 const hasSensitiveData = !!( 639 tags['GPSLatitude'] || tags['Make'] || tags['Model'] || 640 tags['DateTime'] || tags['Software'] || tags['Artist'] 641 ); 642 643 if (hasSensitiveData) { 644 if (isJpeg) { 645 progressBar.textContent = "Cleaning JPEG EXIF..."; 646 progressBar.style.background = '#F3B420'; 647 fileToUpload = await processJpegSafe(file); 648 } 649 else { 650 if (canvasBlocked) { 651 console.warn("Canvas blocked. Skipping clean for:", type); 652 } else { 653 if (isHeic) { 654 progressBar.textContent = "Loading HEIC decoder..."; 655 await loadScript('/js/heic2any.min.js'); 656 progressBar.textContent = "Converting HEIC..."; 657 progressBar.style.background = '#F3B420'; 658 fileToUpload = await processHeicToJpeg(file); 659 } else if (isPngWebp) { 660 progressBar.textContent = "Cleaning Metadata..."; 661 progressBar.style.background = '#F3B420'; 662 fileToUpload = await processViaCanvas(file); 663 } 664 } 665 } 666 } 667 } 668 } 669 } catch (err) { 670 console.warn("Processing error, skipping optimization:", err); 671 fileToUpload = file; 672 } 673 } 674 } 675 676 const sha256 = await getFileSHA256(fileToUpload); 677 formData.append('file', fileToUpload); 678 formData.append('sha256', sha256); 679 formData.append('r18', r18Checked); 680 formData.append('r18_auto', isAuto ? '1' : '0'); 681 formData.append('exif', needCleanExif); 682 formData.append('compress', needCompress); 683 formData.append('token', ''); 684 685 686 let dotCount = 0; 687 let dotInterval = null; 688 uploadFileWithProgress('https://api-v1-t2-upload.urusai.cc', formData, percent => { 689 let p = percent; 690 691 if (p >= 99) { 692 p = 99; 693 progressBar.style.width = p + '%'; 694 progressBar.textContent = langData.processing_file || "Processing file... Please wait"; 695 696 if (!dotInterval) { 697 dotInterval = setInterval(() => { 698 dotCount = (dotCount % 5) + 1; 699 const dots = '.'.repeat(dotCount); 700 progressBar.textContent = (langData.processing_file || "Processing file... Please wait") + dots; 701 }, 500); 702 } 703 } else { 704 progressBar.style.width = p.toFixed(2) + '%'; 705 progressBar.textContent = Math.floor(p) + '%'; 706 } 707 }) 708 .then(data => { 709 uploading_count--; 710 if (dotInterval) clearInterval(dotInterval); 711 if (data.status === 'success') { 712 uploaded_count++; 713 714 const newCard = createUploadCardElement(data, file); 715 if (placeholderCard.isConnected) { 716 placeholderCard.replaceWith(newCard); 717 } else { 718 document.getElementById('result').appendChild(newCard); 719 } 720 } else { 721 uploadFailed_count++; 722 placeholderCard.classList.add('error'); 723 statusDiv.textContent = `\u274C ${langData['upload_failed']}`; 724 const resultDiv = document.getElementById('result'); 725 resultDiv.prepend(placeholderCard); 726 } 727 updateCounts(); 728 }) 729 .catch(err => { 730 uploading_count--; 731 uploadFailed_count++; 732 progressBar.style.background = '#FF0000'; 733 progressBar.style.color = '#ffffff'; 734 progressBar.style.width = '100%'; 735 progressBar.textContent = langData['error'] || 'Error'; 736 if (dotInterval) clearInterval(dotInterval); 737 placeholderCard.classList.add('error'); 738 statusDiv.textContent = `\u274C ${langData['upload_error']}: ${err.message}`; 739 const resultDiv = document.getElementById('result'); 740 resultDiv.prepend(placeholderCard); 741 updateCounts(); 742 });
743} 744 745async function uploadFiles(files) { 746 for (let file of Array.from(files)) { 747 document.querySelector('.upload-list-card')?.style.removeProperty('display'); 748 file = await fixFileExtension(file); 749 750 if (!isValidFileType(file)) { 751 const errorCard = document.createElement('div'); 752 errorCard.classList.add('upload-list-item', 'error'); 753 errorCard.innerHTML = `<div class="upload-status">\u274C ${langData['file_notallowd']}: ${file.name}</div>`; 754 755 const resultDiv = document.getElementById('result'); 756 resultDiv.appendChild(errorCard); 757 updateCounts(); 758 continue; 759 } 760 if (file.size > maxFileSize) { 761 const errorCard = document.createElement('div'); 762 errorCard.classList.add('upload-list-item', 'error'); 763 errorCard.innerHTML = `<div class="upload-status">\u274C ${langData['size_exceeds']} ${maxFileSize / (1024 * 1024)} MB: ${file.name}</div>`; 764 document.getElementById('result').appendChild(errorCard); 765 updateCounts(); 766 continue; 767 } 768 if (file.size == 0) { 769 const errorCard = document.createElement('div'); 770 errorCard.classList.add('upload-list-item', 'error'); 771 errorCard.innerHTML = `<div class="upload-status">\u274C ${langData['size_zero']}: ${file.name}</div>`; 772 document.getElementById('result').appendChild(errorCard); 773 updateCounts(); 774 continue; 775 } 776 uploadSingleFile(file); 777 } 778} 779 780const uploadCard = document.getElementById('uploadCard'); 781const fileInput = uploadCard.querySelector('input[type=file]'); 782 783uploadCard.addEventListener('click', () => { 784 fileInput.click(); 785}); 786 787fileInput.addEventListener('change', e => { 788 if (e.target.files.length > 0) { 789 uploadFiles(e.target.files); 790 e.target.value = ''; 791 } 792}); 793 794uploadCard.addEventListener('dragover', e => { 795 e.preventDefault(); 796 e.dataTransfer.dropEffect = 'copy'; 797}); 798 799uploadCard.addEventListener('drop', e => { 800 e.preventDefault(); 801 if (e.dataTransfer.files.length > 0) { 802 uploadFiles(e.dataTransfer.files); 803 } 804}); 805 806window.addEventListener('paste', e => { 807 const clipboardItems = e.clipboardData.items; 808 const files = []; 809 for (let i = 0; i < clipboardItems.length; i++) { 810 const item = clipboardItems[i]; 811 if (item.kind === 'file') { 812 const file = item.getAsFile(); 813 if (file) { 814 files.push(file); 815 } 816 } 817 } 818 if (files.length > 0) { 819 uploadFiles(files); 820 } 821}); 822 823document.getElementById('clear_list').addEventListener('click', () => { 824 const resultDiv = document.getElementById('result'); 825 resultDiv.innerHTML = ''; 826 uploading_count = 0; 827 uploaded_count = 0; 828 uploadFailed_count = 0; 829 updateCounts(); 830}); 831 832document.getElementById('copy_all').addEventListener('click', () => { 833 const urls = []; 834 const inputs = document.querySelectorAll('.upload-list-item-url-row input'); 835 836 inputs.forEach(input => { 837 const label = input.closest('.upload-list-item-url-row')?.querySelector('.el-input-group__prepend')?.textContent.trim(); 838 if (label && label.includes('URL')) { 839 urls.push(input.value.trim()); 840 } 841 }); 842 843 if (urls.length > 0) { 844 copyText(urls.join('\n')); 845 } else { 846 copy_failed(); 847 } 848}); 849 850 851document.getElementById('create_ab').addEventListener('click', async () => { 852 const fileIds = []; 853 854 document.querySelectorAll('.upload-list-item').forEach(item => { 855 const id = item.getAttribute('data-fileid'); 856 if (id) fileIds.push(id); 857 }); 858 859 if (fileIds.length === 0) { 860 const toast = document.getElementById('error'); 861 document.getElementById('error_message').innerHTML = langData['album_nofile']; 862 toast.style.removeProperty('display'); 863 setTimeout(() => { 864 toast.style.display = 'none'; 865 }, 5000); 866 return; 867 } 868 if (fileIds.length > 50) { 869 const toast = document.getElementById('error'); 870 document.getElementById('error_message').innerHTML = langData['album_fail']; 871 toast.style.removeProperty('display'); 872 setTimeout(() => { 873 toast.style.display = 'none'; 874 }, 5000); 875 return; 876 } 877 const formData = new FormData(); 878 fileIds.forEach(id => formData.append('fileid[]', id)); 879 formData.append('token', ''); 880 881 try { 882 const res = await fetch('https://api.urusai.cc/v1/album', { 883 method: 'POST', 884 body: formData, 885 credentials: 'include' 886 });
887 888 const data = await res.json(); 889 if (data.status === 'success') { 890 document.getElementById('albumDialog').style.display = 'block'; 891 document.getElementById('album_url').value = data.url; 892 document.getElementById('album_delete').value = data.delete; 893 } else { 894 const toast = document.getElementById('error'); 895 document.getElementById('error_message').innerHTML = langData['error'] + ':' + (data.message || 'Unknown error'); 896 toast.style.removeProperty('display'); 897 setTimeout(() => { 898 toast.style.display = 'none'; 899 }, 5000); 900 } 901 } catch (err) { 902 const toast = document.getElementById('error'); 903 document.getElementById('error_message').innerHTML = langData['error'] + ':' + err.message; 904 toast.style.removeProperty('display'); 905 setTimeout(() => { 906 toast.style.display = 'none'; 907 }, 5000); 908 } 909}); 910 911// EXIF - JPEG 912function processJpegSafe(file) { 913 return new Promise((resolve) => { 914 const reader = new FileReader(); 915 reader.onload = (e) => { 916 try { 917 const originalBase64 = e.target.result; 918 const exifObj = piexif.load(originalBase64); 919 const newExif = { "0th": {}, "Exif": {}, "GPS": {} }; 920 if (exifObj["0th"][piexif.ImageIFD.Orientation]) { 921 newExif["0th"][piexif.ImageIFD.Orientation] = exifObj["0th"][piexif.ImageIFD.Orientation]; 922 } 923 const cleanedBase64 = piexif.insert(piexif.dump(newExif), originalBase64); 924 resolve(base64ToFile(cleanedBase64, file.name, 'image/jpeg')); 925 } catch { 926 resolve(file); 927 } 928 }; 929 reader.readAsDataURL(file); 930 }); 931} 932 933// EXIF - HEIC 934async function processHeicToJpeg(file) { 935 try { 936 const blob = await heic2any({ 937 blob: file, 938 toType: "image/jpeg", 939 quality: 0.95 940 }); 941 942 if (blob.size > file.size * 3 || blob.size < 1000) { 943 return file; 944 } 945 946 return new File([blob], file.name.replace(/\.[^/.]+$/, ".jpg"), { type: "image/jpeg" }); 947 } catch (err) { 948 return file; 949 } 950} 951 952// EXIF - PNG/WebP (Canvas) 953function processViaCanvas(file) { 954 return new Promise((resolve) => { 955 const img = new Image(); 956 const url = URL.createObjectURL(file); 957 img.src = url; 958 959 img.onload = () => { 960 URL.revokeObjectURL(url); 961 const canvas = document.createElement('canvas'); 962 canvas.width = img.width; 963 canvas.height = img.height; 964 const ctx = canvas.getContext('2d'); 965 ctx.drawImage(img, 0, 0); 966 967 const type = file.type || 'image/png'; 968 canvas.toBlob((blob) => { 969 if (!blob || blob.size < 100) { 970 console.warn("Canvas output blocked or corrupted, using original file."); 971 resolve(file); 972 } else { 973 resolve(new File([blob], file.name, { type: type })); 974 } 975 }, type, 0.95); 976 }; 977 978 img.onerror = () => { 979 URL.revokeObjectURL(url); 980 resolve(file); 981 }; 982 }); 983} 984 985// EXIF - Base64 To File 986function base64ToFile(base64, filename, type) { 987 const arr = base64.split(','), bstr = atob(arr[1]); 988 let n = bstr.length, u8arr = new Uint8Array(n); 989 while (n--) u8arr[n] = bstr.charCodeAt(n); 990 return new File([u8arr], filename, { type }); 991} 992 993function loadScript(src) { 994 return new Promise((resolve, reject) => { 995 if (window.heic2any) return resolve(); 996 const script = document.createElement('script'); 997 script.src = src; 998 script.onload = resolve; 999 script.onerror = reject; 1000 document.head.appendChild(script); 1001 }); 1002} 1003 1004// isCanvasBlocked? 1005async function isCanvasBlocked() { 1006 const canvas = document.createElement('canvas'); 1007 canvas.width = 20; 1008 canvas.height = 20; 1009 const ctx = canvas.getContext('2d'); 1010 if (!ctx) return true; 1011 1012 ctx.fillStyle = '#FF5733'; 1013 ctx.fillRect(0, 0, 20, 20); 1014 1015 ctx.textBaseline = "top"; 1016 ctx.font = "12px Arial"; 1017 ctx.fillStyle = "#000"; 1018 ctx.fillText("PrivacyTest", 2, 2); 1019 1020 try { 1021 const imageData = ctx.getImageData(0, 0, 20, 20).data; 1022 1023 const isBlank = imageData.every(pixel => pixel === 0); 1024 if (isBlank) return true; 1025 1026 const r = imageData[0]; 1027 const g = imageData[1]; 1028 const b = imageData[2]; 1029 1030 if (Math.abs(r - 255) > 2 || Math.abs(g - 87) > 2 || Math.abs(b - 51) > 2) { 1031 console.warn("Canvas fingerprint noise detected."); 1032 return true; 1033 } 1034 1035 return false; 1036 } catch (e) { 1037 return true; 1038 } 1039} 1040 1041async function isAnimatedWebP(file) { 1042 const buffer = await file.slice(0, 1024).arrayBuffer(); 1043 const uint8 = new Uint8Array(buffer); 1044 const view = new DataView(buffer); 1045 const content = new TextDecoder().decode(buffer); 1046 return content.includes('ANIM'); 1047} 1048 1049// compress exif - Switch 1050const compressSwitch = document.getElementById('compressSwitch'); 1051const exifSwitch = document.getElementById('exifSwitch'); 1052 1053if (compressSwitch && exifSwitch) { 1054 compressSwitch.addEventListener('change', function() { 1055 if (this.checked) exifSwitch.checked = true; 1056 });
1057 1058 exifSwitch.addEventListener('change', function() { 1059 if (!this.checked) compressSwitch.checked = false; 1060 }); 1061} 1062 1063// smartCompress 1064async function smartCompress(file, targetKB = 800, onProgress) { 1065 const img = await new Promise((res, rej) => { 1066 const i = new Image(); 1067 i.onload = () => res(i); i.onerror = rej; 1068 i.src = URL.createObjectURL(file); 1069 }); 1070 1071 const canvas = document.createElement('canvas'); 1072 const ctx = canvas.getContext('2d'); 1073 let { width, height } = img; 1074 1075 const MAX_PIXELS = 32000000; 1076 if (width * height > MAX_PIXELS) { 1077 /*const ratio = Math.sqrt(MAX_PIXELS / (width * height)); 1078 width = Math.floor(width * ratio); height = Math.floor(height * ratio);*/ 1079 console.warn("Image too large, skipping compression."); 1080 URL.revokeObjectURL(img.src); 1081 return file; 1082 } 1083 canvas.width = width; canvas.height = height; 1084 ctx.drawImage(img, 0, 0, width, height); 1085 URL.revokeObjectURL(img.src); 1086 1087 const type = file.type.toLowerCase(); 1088 let hasAlpha = false; 1089 if (type.includes('png') || type.includes('webp')) { 1090 const imageData = ctx.getImageData(0, 0, width, height).data; 1091 const step = width * height > 5000000 ? 120 : 40; 1092 for (let i = 3; i < imageData.length; i += step) { 1093 if (imageData[i] < 255) { hasAlpha = true; break; } 1094 } 1095 } 1096 let outputType = hasAlpha ? (type.includes('webp') ? 'image/webp' : 'image/png') : 'image/jpeg'; 1097 1098 const finalCanvas = document.createElement('canvas'); 1099 finalCanvas.width = width; finalCanvas.height = height; 1100 const finalCtx = finalCanvas.getContext('2d'); 1101 if (outputType === 'image/jpeg') { 1102 finalCtx.fillStyle = '#FFFFFF'; 1103 finalCtx.fillRect(0, 0, width, height); 1104 } 1105 finalCtx.drawImage(canvas, 0, 0); 1106 1107 const megaPixels = (width * height) / 1000000; 1108 let dynamicTargetKB = Math.max(800, Math.min(Math.floor(megaPixels * 150), 5000)); 1109 const targetBytes = dynamicTargetKB * 1024; 1110 1111 let bestBlob = null; 1112 const canLossy = (outputType === 'image/jpeg' || outputType === 'image/webp'); 1113 1114 if (canLossy) { 1115 let min = 0.1, max = 0.9; 1116 const totalSteps = 8; 1117 for (let i = 0; i < totalSteps; i++) { 1118 const q = (min + max) / 2; 1119 const blob = await new Promise(r => finalCanvas.toBlob(r, outputType, q)); 1120 1121 if (blob && blob.size <= targetBytes) { 1122 bestBlob = blob; 1123 min = q; 1124 } else { 1125 max = q; 1126 } 1127 1128 if (onProgress) { 1129 const percent = Math.floor(((i + 1) / totalSteps) * 100); 1130 onProgress(percent); 1131 } 1132 } 1133 } else { 1134 bestBlob = await new Promise(r => finalCanvas.toBlob(r, 'image/png')); 1135 if (onProgress) onProgress(100); 1136 } 1137 1138 //return bestBlob || await new Promise(r => finalCanvas.toBlob(r, outputType, 0.7)); 1139 /*const finalBlob = bestBlob || await new Promise(r => finalCanvas.toBlob(r, outputType, 0.7)); 1140 return new File([finalBlob], file.name, { type: outputType });*/ 1141 const finalBlob = bestBlob || await new Promise(r => finalCanvas.toBlob(r, outputType, 0.7)); 1142 const extMap = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp' }; 1143 const baseName = file.name.replace(/\.[^/.]+$/, ''); 1144 return new File([finalBlob], `${baseName}.${extMap[outputType]}`, { type: outputType }); 1145} 1146 1147 1148// URL Upload 1149async function uploadSingleUrl(urlValue) { 1150 uploading_count++; 1151 updateCounts(); 1152 1153 document.querySelector('.upload-list-card')?.style.removeProperty('display'); 1154 1155 const guessedName = urlValue.split('/').pop().split('?')[0] || 'remote_file'; 1156 1157 const placeholderCard = document.createElement('div'); 1158 placeholderCard.classList.add('upload-list-item'); 1159 placeholderCard.innerHTML = ` 1160 <strong>${guessedName}</strong> 1161 <div class="progress-bar-container" style="background:#eee; border-radius:4px; overflow:hidden; height:16px; margin:0.5em 0;"> 1162 <div class="progress-bar" style="background:#f57e0d; height:100%; width:0%; transition:width 0.2s ease; font-size: 12px;"></div> 1163 </div> 1164 <div class="upload-status"></div> 1165 `; 1166 document.getElementById('result').appendChild(placeholderCard); 1167 1168 const progressBar = placeholderCard.querySelector('.progress-bar'); 1169 const statusDiv = placeholderCard.querySelector('.upload-status'); 1170 1171 //const r18Checked = document.getElementById('r18Switch').checked ? '1' : '0'; 1172 const r18Checked = getR18Value(); 1173 const needCompress = document.getElementById('compressSwitch').checked ? '1' : '0'; 1174 const needCleanExif = document.getElementById('exifSwitch').checked ? '1' : '0'; 1175 1176 const formData = new FormData(); 1177 formData.append('url', urlValue); 1178 formData.append('r18', r18Checked); 1179 formData.append('exif', needCleanExif); 1180 formData.append('compress', needCompress); 1181 1182 1183 await new Promise(r => requestAnimationFrame(() => setTimeout(r, 0))); 1184 progressBar.style.width = '10%'; 1185 progressBar.style.background = '#00A3A6'; 1186 progressBar.textContent = "Connecting to remote server..."; 1187 1188 let dotCount = 0; 1189 let dotInterval = null; 1190 1191 uploadFileWithProgress('https://api-v1-t2-upload.urusai.cc', formData, percent => { 1192 let p = percent; 1193 if (p >= 99) { 1194 p = 99; 1195 progressBar.style.width = p + '%'; 1196 progressBar.textContent = langData.processing_file || "Processing file... Please wait"; 1197 1198 if (!dotInterval) { 1199 dotInterval = setInterval(() => { 1200 dotCount = (dotCount % 5) + 1; 1201 const dots = '.'.repeat(dotCount); 1202 progressBar.textContent = (langData.processing_file || "Processing file... Please wait") + dots; 1203 }, 500); 1204 } 1205 } else { 1206 progressBar.style.width = p.toFixed(2) + '%'; 1207 progressBar.textContent = Math.floor(p) + '%'; 1208 } 1209 }) 1210 .then(data => { 1211 uploading_count--; 1212 if (dotInterval) clearInterval(dotInterval); 1213 1214 if (data.status === 'success') { 1215 uploaded_count++; 1216 1217 const mockFile = { 1218 name: guessedName, 1219 type: data.data.mime || 'image/jpeg' 1220 }; 1221 1222 const newCard = createUploadCardElement(data, mockFile); 1223 if (placeholderCard.isConnected) { 1224 placeholderCard.replaceWith(newCard); 1225 } else { 1226 document.getElementById('result').appendChild(newCard); 1227 } 1228 } else { 1229 uploadFailed_count++; 1230 placeholderCard.classList.add('error'); 1231 statusDiv.textContent = `\u274C ${langData['upload_failed'] || 'Upload failed'}`; 1232 const resultDiv = document.getElementById('result'); 1233 resultDiv.prepend(placeholderCard); 1234 } 1235 updateCounts(); 1236 }) 1237 .catch(err => { 1238 uploading_count--; 1239 uploadFailed_count++; 1240 progressBar.style.background = '#FF0000'; 1241 progressBar.style.color = '#ffffff'; 1242 progressBar.style.width = '100%'; 1243 progressBar.textContent = langData['error'] || 'Error'; 1244 if (dotInterval) clearInterval(dotInterval); 1245 1246 placeholderCard.classList.add('error'); 1247 1248 let displayErrorMessage = err.message; 1249 const errStr = (err.message || '').toLowerCase(); 1250 1251 if (err.code === 'FILE_SIZE_EXCEEDS' || errStr.includes('file size exceeds the limit')) { 1252 displayErrorMessage = langData['size_exceeds'] || 'The file size exceeds the limit.'; 1253 } 1254 else if (err.code === 'FILE_TYPE_NOT_ALLOWED' || errStr.includes('file type not allowed')) { 1255 displayErrorMessage = langData['file_notallowd'] || 'File type not allowed.'; 1256 } 1257 else if ( 1258 err.code === 'URL_FETCH_FAILED' || 1259 errStr.includes('unable to fetch') || 1260 errStr.includes('failed to fetch') || 1261 errStr.includes('timed out') 1262 ) { 1263 displayErrorMessage = langData['err_url_fetch'] || 'Failed to fetch or download the file from the provided URL.'; 1264 } 1265 1266 else { 1267 displayErrorMessage = `${langData['upload_error'] || 'Upload error'}: ${err.message}`; 1268 } 1269 1270 statusDiv.textContent = `\u274C ${displayErrorMessage}`; 1271 1272 const resultDiv = document.getElementById('result'); 1273 resultDiv.prepend(placeholderCard); 1274 updateCounts(); 1275 }); 1276} 1277 1278// Restore cookie state 1279const r18El = document.getElementById('r18Switch'); 1280if (r18El) { 1281 const saved = getR18Cookie(); 1282 if (r18El.tagName === 'SELECT') { 1283 r18El.value = saved || 'auto'; 1284 } else { 1285 r18El.checked = saved === 'on'; 1286 } 1287 r18El.addEventListener('change', function () { 1288 const val = this.tagName === 'SELECT' ? this.value : (this.checked ? 'on' : 'off'); 1289 setR18Cookie(val); 1290 if (val === 'auto') { 1291 try { getNsfwWorker(); } catch (e) {} 1292 } 1293 }); 1294 1295 // auto pre-load - model 1296 if ((r18El.tagName === 'SELECT' && r18El.value === 'auto') || 1297 (r18El.tagName !== 'SELECT' && !r18El.checked)) { 1298 const preload = () => { 1299 try { 1300 getNsfwWorker(); 1301 } catch (e) { 1302 console.warn('NSFW worker preload failed:', e); 1303 } 1304 }; 1305 if ('requestIdleCallback' in window) { 1306 requestIdleCallback(preload, { timeout: 5000 }); 1307 } else { 1308 setTimeout(preload, 2000); 1309 } 1310 } 1311} 1312// dropdownBtn 1313document.addEventListener('DOMContentLoaded', () => { 1314 const dropdownBtn = document.getElementById('dropdownBtn'); 1315 const dropdownMenu = document.getElementById('dropdownMenu'); 1316 const urlUploadOpt = document.getElementById('urlUploadOpt'); 1317 const urlInputContainer = document.getElementById('urlInputContainer'); 1318 const uploadUrlInput = document.getElementById('uploadUrlInput'); 1319 const urlSubmitBtn = document.getElementById('urlSubmitBtn'); 1320 const urlCancelBtn = document.getElementById('urlCancelBtn'); 1321 1322 dropdownBtn.addEventListener('click', (e) => { 1323 e.stopPropagation(); 1324 const isVisible = dropdownMenu.style.display === 'block'; 1325 dropdownMenu.style.display = isVisible ? 'none' : 'block'; 1326 });
1327 1328 document.addEventListener('click', () => { 1329 dropdownMenu.style.display = 'none'; 1330 }); 1331 1332 urlUploadOpt.addEventListener('click', (e) => { 1333 e.stopPropagation(); 1334 dropdownMenu.style.display = 'none'; 1335 urlInputContainer.style.display = 'flex'; 1336 uploadUrlInput.focus(); 1337 }); 1338 1339 urlCancelBtn.addEventListener('click', () => { 1340 urlInputContainer.style.display = 'none'; 1341 uploadUrlInput.value = ''; 1342 }); 1343 1344 urlSubmitBtn.addEventListener('click', () => { 1345 const urlValue = uploadUrlInput.value.trim(); 1346 1347 const showInputError = (errorMsg) => { 1348 uploadUrlInput.value = ''; 1349 uploadUrlInput.placeholder = errorMsg; 1350 uploadUrlInput.style.borderColor = '#f56c6c'; 1351 uploadUrlInput.classList.add('has-error'); 1352 }; 1353 1354 let isValidUrl = false; 1355 try { 1356 if (urlValue && /^https?:\/\/.+\..+/i.test(urlValue)) { 1357 new URL(urlValue); 1358 isValidUrl = true; 1359 } 1360 } catch (e) { 1361 isValidUrl = false; 1362 } 1363 1364 if (!isValidUrl) { 1365 //if (!urlValue || !/^https?:\/\/.+/i.test(urlValue)) { 1366 const err_msg = (langData && langData['invalidurl_format']) ? langData['invalidurl_format'] : 'Invalid URL format. Must start with http:// or https://'; 1367 showInputError(`\u26A0 ${err_msg}`); 1368 return; 1369 } 1370 1371 1372 uploadSingleUrl(urlValue); 1373 1374 urlInputContainer.style.display = 'none'; 1375 uploadUrlInput.value = ''; 1376 uploadUrlInput.placeholder = (langData && langData['url_upload']) ? langData['url_upload'] : 'Enter image/file URL (Max 10MB)'; 1377 uploadUrlInput.style.borderColor = ''; 1378 }); 1379 1380 1381 uploadUrlInput.addEventListener('focus', () => { 1382 if (uploadUrlInput.classList.contains('has-error')) { 1383 uploadUrlInput.classList.remove('has-error'); 1384 uploadUrlInput.placeholder = (langData && langData['url_upload']) ? langData['url_upload'] : 'Enter image/file URL (Max 10MB)'; 1385 uploadUrlInput.style.borderColor = '#f57e0d'; 1386 } 1387 }); 1388}); 1389 1390 1391async function sniffFileType(file) { 1392 if (file.size < 16) return null; 1393 const buf = new Uint8Array(await file.slice(0, 64).arrayBuffer()); 1394 const ascii = (s, e) => String.fromCharCode(...buf.slice(s, e)); 1395 const eq = (...bytes) => bytes.every((b, i) => buf[i] === b); 1396 const u32le = o => (buf[o] | buf[o+1] << 8 | buf[o+2] << 16 | buf[o+3] << 24) >>> 0; 1397 1398 if (eq(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A) && ascii(12, 16) === 'IHDR') 1399 return { mime: 'image/png', ext: 'png' }; 1400 1401 if (eq(0xFF, 0xD8, 0xFF) && (buf[3] >= 0xE0 && buf[3] <= 0xEF || buf[3] === 0xDB || buf[3] === 0xFE)) 1402 return { mime: 'image/jpeg', ext: 'jpg' }; 1403 1404 if (ascii(0, 6) === 'GIF87a' || ascii(0, 6) === 'GIF89a') 1405 return { mime: 'image/gif', ext: 'gif' }; 1406 1407 if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP' && u32le(4) + 8 <= file.size + 1) 1408 return { mime: 'image/webp', ext: 'webp' }; 1409 1410 if (ascii(0, 2) === 'BM' && 1411 u32le(6) === 0 && 1412 [12, 40, 52, 56, 64, 108, 124].includes(u32le(14)) && 1413 u32le(2) === file.size) 1414 return { mime: 'image/bmp', ext: 'bmp' }; 1415 1416 if (ascii(0, 7) === '%PDF-1.' || ascii(0, 7) === '%PDF-2.') 1417 return { mime: 'application/pdf', ext: 'pdf' }; 1418 1419 if (ascii(4, 8) === 'ftyp') { 1420 const brand = ascii(8, 12); 1421 if (['heic', 'heix', 'hevc', 'hevx'].includes(brand)) return { mime: 'image/heic', ext: 'heic' }; 1422 if (['avif', 'avis'].includes(brand)) return { mime: 'image/avif', ext: 'avif' }; 1423 } 1424 1425 return null; 1426} 1427 1428const EXT_ALIASES = { 1429 jpg: ['jpg', 'jpeg', 'jpe', 'jfif'], 1430 heic: ['heic', 'heif'], 1431}; 1432 1433async function fixFileExtension(file) { 1434 try { 1435 const real = await sniffFileType(file); 1436 if (!real) return file; 1437 1438 const curExt = (file.name.split('.').pop() || '').toLowerCase(); 1439 const okExts = EXT_ALIASES[real.ext] || [real.ext]; 1440 1441 if (okExts.includes(curExt)) { 1442 return file.type === real.mime ? file : new File([file], file.name, { type: real.mime }); 1443 } 1444 1445 const hasExt = /\.[^/.]+$/.test(file.name); 1446 const base = hasExt ? file.name.replace(/\.[^/.]+$/, '') : file.name; 1447 console.log(`Extension corrected: ${file.name} -> ${base}.${real.ext}`); 1448 return new File([file], `${base}.${real.ext}`, { type: real.mime }); 1449 } catch (e) { 1450 return file; 1451 } 1452}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.