PageSourceSearch

https://urusai.cc/js/upload.js

js urusai.cc collected 2026-10-07 09:20:43 UTC 49,940 bytes, 1,452 lines download raw bytes

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, '&amp;')
290            .replace(/</g, '&lt;')
291            .replace(/>/g, '&gt;')
292            .replace(/"/g, '&quot;')
293            .replace(/'/g, '&#039;');
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.