| |
|
|
| |
| |
| |
|
|
| import uiModule from './ui.js'; |
| import spinnerModule from './spinner.js'; |
|
|
| let pendingFiles = []; |
| let uploaded = []; |
| |
| |
| |
| let _lastUploadedMeta = []; |
| let API_BASE = ''; |
| let _uploadSpinners = []; |
| const _previewUrls = new WeakMap(); |
|
|
| function _getPreviewUrl(f) { |
| if (!f) return ''; |
| let url = _previewUrls.get(f); |
| if (!url) { |
| url = URL.createObjectURL(f); |
| _previewUrls.set(f, url); |
| } |
| return url; |
| } |
|
|
| function _revokePreviewUrl(f) { |
| const url = _previewUrls.get(f); |
| if (url) { |
| try { URL.revokeObjectURL(url); } catch (_) {} |
| _previewUrls.delete(f); |
| } |
| } |
|
|
| |
| |
| |
| export function init(apiBase) { |
| API_BASE = apiBase; |
| } |
|
|
| |
| |
| |
| export function openPicker() { |
| document.getElementById('file-input').click(); |
| } |
|
|
| const MAX_VISIBLE = 3; |
| const MAX_EXPAND = 6; |
| let _expanded = false; |
|
|
| |
| |
| |
| |
| |
| export function renderAttachStrip() { |
| const strip = document.getElementById('attach-strip'); |
|
|
| while (strip.firstChild) strip.removeChild(strip.firstChild); |
| if (pendingFiles.length === 0) { |
| _expanded = false; |
| if (window._updateSendBtnIcon) window._updateSendBtnIcon(); |
| return; |
| } |
|
|
| const total = pendingFiles.length; |
| const collapsed = total > MAX_VISIBLE && !_expanded; |
|
|
| if (collapsed) { |
| |
| const badge = document.createElement('div'); |
| badge.className = 'thumb thumb-collapsed'; |
| const label = document.createElement('span'); |
| label.textContent = total + ' file' + (total > 1 ? 's' : ''); |
| label.className = 'thumb-collapsed-label'; |
| badge.appendChild(label); |
| badge.title = pendingFiles.map(f => f.name || 'pasted-image').join('\n'); |
| const canExpand = total <= MAX_EXPAND; |
| badge.style.cursor = canExpand ? 'pointer' : 'default'; |
| badge.addEventListener('click', (e) => { |
| if (e.target.closest('.thumb-collapsed-x')) return; |
| if (!canExpand) return; |
| _expanded = true; |
| renderAttachStrip(); |
| }); |
| const x = document.createElement('button'); |
| x.className = 'thumb-collapsed-x'; |
| x.textContent = '\u00d7'; |
| x.title = 'Remove all'; |
| x.addEventListener('click', (e) => { e.stopPropagation(); clearPending(); }); |
| badge.appendChild(x); |
| strip.appendChild(badge); |
| } else { |
| |
| for (let idx = 0; idx < total; idx++) { |
| strip.appendChild(_createChip(pendingFiles[idx], idx)); |
| } |
| } |
| if (window._updateSendBtnIcon) window._updateSendBtnIcon(); |
| } |
|
|
| function _createChip(f, idx) { |
| const chip = document.createElement('div'); |
| chip.className = 'thumb'; |
| const isImage = f.type?.startsWith('image/') || /\.(png|jpg|jpeg|gif|webp|svg|bmp)$/i.test(f.name || ''); |
| if (isImage) { |
| chip.classList.add('thumb-image'); |
| const img = document.createElement('img'); |
| img.className = 'thumb-img'; |
| img.src = _getPreviewUrl(f); |
| img.alt = f.name || 'image'; |
| chip.appendChild(img); |
| } else { |
| const span = document.createElement('span'); |
| span.textContent = f.name || 'pasted-image'; |
| chip.appendChild(span); |
| } |
| const x = document.createElement('button'); |
| x.textContent = '\u00d7'; |
| x.setAttribute('aria-label', 'Remove attachment'); |
| x.addEventListener('click', (e) => { e.stopPropagation(); removePending(idx); }); |
| chip.appendChild(x); |
| return chip; |
| } |
|
|
| |
| |
| |
| export function removePending(idx) { |
| _revokePreviewUrl(pendingFiles[idx]); |
| pendingFiles.splice(idx, 1); |
| renderAttachStrip(); |
| } |
|
|
| |
| |
| |
| export async function uploadPending() { |
| if (pendingFiles.length === 0) return []; |
|
|
| |
| |
| |
| const strip = document.getElementById('attach-strip'); |
| if (strip) { |
| strip.classList.add('attach-uploading'); |
| |
| |
| strip.querySelectorAll('.thumb').forEach(chip => { |
| try { |
| const sp = spinnerModule.create('', 'clean', 'whirlpool'); |
| const ov = document.createElement('span'); |
| ov.className = 'thumb-upload-spinner'; |
| ov.appendChild(sp.createElement()); |
| chip.appendChild(ov); |
| sp.start(); |
| _uploadSpinners.push(sp); |
| } catch (_) { } |
| }); |
| } |
|
|
| const fd = new FormData(); |
| pendingFiles.forEach(f => fd.append('files', f, f.name || 'paste.png')); |
|
|
| try { |
| const res = await fetch(`${API_BASE}/api/upload`, { |
| method: 'POST', |
| body: fd |
| }); |
| if (!res.ok) { |
| |
| |
| |
| |
| |
| let detail = ''; |
| try { const e = await res.json(); detail = e.detail || e.error || ''; } catch (_) {} |
| _showToast('Upload failed' + (detail ? ': ' + detail : ` (HTTP ${res.status})`)); |
| return []; |
| } |
| const data = await res.json(); |
| uploaded = (data.files || []); |
| pendingFiles = []; |
| |
| |
| |
| _lastUploadedMeta = uploaded; |
| return uploaded.map(x => x.id); |
| } finally { |
| _uploadSpinners.forEach(sp => { try { sp.stop && sp.stop(); } catch (_) {} }); |
| _uploadSpinners = []; |
| if (strip) strip.classList.remove('attach-uploading'); |
| |
| |
| renderAttachStrip(); |
| } |
| } |
|
|
| const MAX_FILES = 10; |
|
|
| |
| |
| |
| export function addFiles(files) { |
| for (const f of files) { |
| if (pendingFiles.length >= MAX_FILES) { |
| _showToast(`Max ${MAX_FILES} files allowed`); |
| break; |
| } |
| pendingFiles.push(f); |
| } |
| renderAttachStrip(); |
| } |
|
|
| function _showToast(msg) { |
| if (window.showToast) { window.showToast(msg); return; } |
| |
| let t = document.getElementById('_attach-toast'); |
| if (!t) { |
| t = document.createElement('div'); |
| t.id = '_attach-toast'; |
| t.style.cssText = 'position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:var(--panel);border:1px solid var(--red);color:var(--red);padding:6px 14px;border-radius:6px;font-size:13px;z-index:9999;opacity:0;transition:opacity .3s'; |
| document.body.appendChild(t); |
| } |
| t.textContent = msg; |
| t.style.opacity = '1'; |
| clearTimeout(t._timer); |
| t._timer = setTimeout(() => { t.style.opacity = '0'; }, 2500); |
| } |
|
|
| |
| |
| |
| export function getPendingCount() { |
| return pendingFiles.length; |
| } |
|
|
| |
| |
| |
| export function getPendingRaw() { |
| return [...pendingFiles]; |
| } |
|
|
| |
| |
| |
| export function getPendingInfo() { |
| return pendingFiles.map(f => { |
| const isImage = f.type?.startsWith('image/') || /\.(png|jpg|jpeg|gif|webp|svg|bmp)$/i.test(f.name || ''); |
| return { |
| name: f.name || 'pasted-image', |
| size: f.size || 0, |
| mime: f.type || '', |
| previewUrl: isImage ? _getPreviewUrl(f) : '', |
| }; |
| }); |
| } |
|
|
| |
| |
| |
| export function clearPending() { |
| pendingFiles.forEach(_revokePreviewUrl); |
| pendingFiles = []; |
| renderAttachStrip(); |
| } |
|
|
| |
| export function getLastUploadedMeta() { |
| return _lastUploadedMeta; |
| } |
|
|
| var escapeHtml = uiModule.esc; |
|
|
| const fileHandlerModule = { |
| init, |
| openPicker, |
| renderAttachStrip, |
| removePending, |
| uploadPending, |
| addFiles, |
| getPendingCount, |
| getPendingInfo, |
| getPendingRaw, |
| clearPending, |
| getLastUploadedMeta, |
| }; |
|
|
| export default fileHandlerModule; |
|
|