"use strict"; // Dragon Wind Chrome Extension — Popup Script let config = { serverUrl: 'http://localhost:3000', username: 'Admin', password: '' }; let authToken = null; let uploadMode = 'http'; let selectedFiles = []; let folderList = []; let connected = false; // ==================== INIT ==================== (async function init() { const stored = await chrome.storage.local.get(['config', 'token', 'mode']); if (stored.config) config = { ...config, ...stored.config }; if (stored.token) authToken = stored.token; if (stored.mode) uploadMode = stored.mode; document.getElementById('cfg-server').value = config.serverUrl; document.getElementById('cfg-user').value = config.username; document.getElementById('conn-server').textContent = config.serverUrl.replace(/https?:\/\//, ''); setMode(uploadMode, false); await tryConnect(); })(); document.getElementById('settings-toggle').addEventListener('click', () => { const panel = document.getElementById('settings-panel'); panel.classList.toggle('open'); }); // ==================== CONNECTION ==================== async function tryConnect() { setConnStatus('grey', 'Connecting…'); if (!authToken) { if (!config.username || !config.password) { setConnStatus('red', 'Set credentials in settings'); return; } await login(); return; } try { const r = await apiFetch('GET', '/api/health'); if (r.status === 'ok') { setConnStatus('green', `Connected — ${r.s3Configured ? 'S3 ✓' : 'S3 not configured'}`); connected = true; await loadFolders(); } else { authToken = null; await chrome.storage.local.remove('token'); await login(); } } catch (e) { setConnStatus('red', `Cannot reach server`); } } async function login() { try { const r = await apiFetch('POST', '/api/login', { username: config.username, password: config.password }); if (r.success) { authToken = r.token; await chrome.storage.local.set({ token: authToken }); setConnStatus('green', `Connected as ${r.user}`); connected = true; await loadFolders(); } else { setConnStatus('red', r.error || 'Auth failed'); } } catch (e) { setConnStatus('red', 'Connection failed'); } } function setConnStatus(color, text) { const dot = document.getElementById('conn-dot'); dot.className = `conn-dot ${color}`; document.getElementById('conn-label').textContent = text; } // ==================== SETTINGS ==================== async function saveSettings() { const serverUrl = document.getElementById('cfg-server').value.trim().replace(/\/$/, ''); const username = document.getElementById('cfg-user').value.trim(); const password = document.getElementById('cfg-pass').value; if (!serverUrl) { showSettingsStatus('Server URL required', 'error'); return; } config = { serverUrl, username, password: password || config.password }; authToken = null; await chrome.storage.local.set({ config, token: null }); document.getElementById('conn-server').textContent = serverUrl.replace(/https?:\/\//, ''); showSettingsStatus('Saved — connecting…', 'loading'); connected = false; await tryConnect(); showSettingsStatus('Settings saved', 'success'); } function showSettingsStatus(msg, type) { const el = document.getElementById('settings-status'); el.className = `status-bar ${type}`; el.textContent = msg; } // ==================== FOLDERS ==================== async function loadFolders() { try { const d = await apiFetch('GET', '/api/folders'); folderList = []; flattenTree(d.tree || [], '', folderList); const sel = document.getElementById('folder-select'); const current = sel.value; sel.innerHTML = ''; folderList.forEach(f => { const opt = document.createElement('option'); opt.value = f.path; opt.textContent = f.display; sel.appendChild(opt); }); if (current) sel.value = current; } catch (_) {} } function flattenTree(nodes, prefix, out) { nodes.forEach(n => { const path = prefix ? `${prefix}/${n.name}` : n.name; const display = '\u00a0'.repeat(prefix.split('/').filter(Boolean).length * 2) + (prefix ? '↳ ' : '') + n.name; out.push({ path, display }); if (n.children.length) flattenTree(n.children, path, out); }); } // ==================== MODE ==================== function setMode(mode, save = true) { uploadMode = mode; document.getElementById('btn-http').className = `mode-btn${mode === 'http' ? ' active-http' : ''}`; document.getElementById('btn-udp').className = `mode-btn${mode === 'udp' ? ' active-udp' : ''}`; const btn = document.getElementById('upload-btn'); if (mode === 'http') { btn.className = 'upload-btn http'; btn.textContent = 'Upload Files'; } else { btn.className = 'upload-btn'; btn.textContent = '⚡ UDP Upload'; } if (save) chrome.storage.local.set({ mode }); } // ==================== FILES ==================== function onDrop(e) { e.preventDefault(); document.getElementById('drop-zone').classList.remove('over'); addFiles(Array.from(e.dataTransfer.files)); } function onFileInputChange(e) { addFiles(Array.from(e.target.files)); e.target.value = ''; } function addFiles(files) { files.forEach(f => { if (!selectedFiles.find(x => x.name === f.name && x.size === f.size)) { selectedFiles.push({ file: f, name: f.name, size: f.size, status: 'pending' }); } }); renderFileList(); updateBtn(); } function renderFileList() { const list = document.getElementById('file-list'); list.innerHTML = ''; selectedFiles.forEach((item, i) => { const el = document.createElement('div'); el.className = 'file-item'; el.innerHTML = ` ${getIcon(item.name)}
${esc(item.name)}
${fmtSize(item.size)}
${item.status} `; list.appendChild(el); }); } function removeFile(i) { selectedFiles.splice(i, 1); renderFileList(); updateBtn(); } function updateBtn() { const n = selectedFiles.filter(f => f.status === 'pending').length; const btn = document.getElementById('upload-btn'); btn.disabled = n === 0 || !connected; if (uploadMode === 'udp') btn.textContent = n > 0 ? `⚡ UDP Upload (${n})` : '⚡ UDP Upload'; else btn.textContent = n > 0 ? `Upload ${n} File${n>1?'s':''}` : 'Upload Files'; } // ==================== UPLOAD ==================== async function startUpload() { if (!connected) { showStatus('Not connected to server', 'error'); return; } const pending = selectedFiles.filter(f => f.status === 'pending'); document.getElementById('upload-btn').disabled = true; const prefix = document.getElementById('folder-select').value; let done = 0, failed = 0; for (const item of pending) { const idx = selectedFiles.indexOf(item); setFileStatus(idx, 'uploading', 'Uploading…'); document.getElementById(`fp-${idx}`).style.display = 'block'; try { if (uploadMode === 'http') await uploadHTTP(item, idx, prefix); else await uploadUDP(item, idx, prefix); setFileStatus(idx, 'done', '✓'); item.status = 'done'; done++; } catch (e) { setFileStatus(idx, 'error', '✗'); item.status = 'error'; failed++; console.error(`Upload failed for ${item.name}:`, e); } } const total = done + failed; if (failed === 0) showStatus(`✅ ${done} file${done>1?'s':''} uploaded`, 'success'); else showStatus(`${done}/${total} uploaded, ${failed} failed`, failed === total ? 'error' : 'loading'); updateBtn(); } async function uploadHTTP(item, idx, prefix) { const presigned = await apiFetch('POST', '/api/presigned', { filename: item.name, prefix, contentType: item.file.type || 'application/octet-stream' }); if (!presigned.success) throw new Error(presigned.error); await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', presigned.url); xhr.setRequestHeader('Content-Type', item.file.type || 'application/octet-stream'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); document.getElementById(`fpb-${idx}`).style.width = `${pct}%`; setFileStatus(idx, 'uploading', `${pct}%`); } }; xhr.onload = () => xhr.status < 300 ? resolve() : reject(new Error(`S3 ${xhr.status}`)); xhr.onerror = () => reject(new Error('Network error')); xhr.send(item.file); }); document.getElementById(`fpb-${idx}`).style.width = '100%'; } async function uploadUDP(item, idx, prefix) { // Create UDP session on server const sessResp = await apiFetch('POST', '/api/udp/session', { filename: item.name, size: item.size, prefix }); if (!sessResp.success) throw new Error(sessResp.error); const { sessionId, relayUrl } = sessResp; const CHUNK = 64 * 1024; const totalChunks = Math.ceil(item.size / CHUNK); // Send via chunked HTTP fallback (true UDP requires chrome.sockets which needs host app) for (let i = 0; i < totalChunks; i++) { const chunk = item.file.slice(i * CHUNK, (i + 1) * CHUNK); const resp = await fetch(`${relayUrl}/session/${sessionId}/chunk/${i}`, { method: 'POST', body: chunk, headers: { 'Content-Type': 'application/octet-stream' } }); if (!resp.ok) throw new Error(`Chunk ${i} failed`); const pct = Math.round(((i + 1) / totalChunks) * 100); document.getElementById(`fpb-${idx}`).style.width = `${pct}%`; setFileStatus(idx, 'uploading', `⚡ ${pct}%`); } await apiFetch('POST', `/api/udp/session/${sessionId}/complete`, { success: true }); document.getElementById(`fpb-${idx}`).style.width = '100%'; } // ==================== HELPERS ==================== async function apiFetch(method, path, body) { const url = config.serverUrl.replace(/\/$/, '') + path; const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (authToken) opts.headers['x-auth-token'] = authToken; if (body) opts.body = JSON.stringify(body); const r = await fetch(url, opts); if (r.status === 401) { authToken = null; await chrome.storage.local.remove('token'); throw new Error('Session expired'); } return r.json(); } function showStatus(msg, type) { const el = document.getElementById('status-bar'); el.className = `status-bar ${type}`; el.textContent = msg; } function setFileStatus(i, cls, text) { const el = document.getElementById(`fs-${i}`); if (el) { el.className = `fi-status ${cls}`; el.textContent = text; } } function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } function fmtSize(b) { if (b < 1024) return `${b}B`; if (b < 1048576) return `${(b/1024).toFixed(0)}KB`; if (b < 1073741824) return `${(b/1048576).toFixed(1)}MB`; return `${(b/1073741824).toFixed(2)}GB`; } function getIcon(name) { const ext = name.split('.').pop().toLowerCase(); if (['mp4','mov','mxf','mkv','avi','r3d','braw','mts','m2ts'].includes(ext)) return '🎬'; if (['mp3','wav','aac','flac','aiff','m4a'].includes(ext)) return '🎵'; if (['jpg','jpeg','png','tiff','exr','dpx','arw','cr2','dng'].includes(ext)) return '🖼️'; return '📄'; }