|
|
|
|
| |
| |
|
|
|
|
| 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();
|
|
|
| const MAX_FILES = 10;
|
| const MAX_VISIBLE = 3;
|
| let _expanded = false;
|
|
|
| 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();
|
| }
|
|
|
| |
| |
| |
| |
|
|
| 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');
|
| badge.style.cursor = 'pointer';
|
| badge.addEventListener('click', (e) => {
|
| if (e.target.closest('.thumb-collapsed-x')) 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();
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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;
|
|
|