// UI helpers — v2.1.0 (function () { const UI = {}; UI.$ = sel => document.querySelector(sel); UI.$$ = sel => document.querySelectorAll(sel); UI.setHidden = function (sel, hidden) { const el = typeof sel === 'string' ? UI.$(sel) : sel; if (!el) return; el.classList.toggle('hidden', !!hidden); }; UI.showPane = function (name) { UI.setHidden('#connect-pane', name !== 'connect'); UI.setHidden('#library-pane', name !== 'library'); }; UI.setStatus = function (sel, text, kind) { const el = typeof sel === 'string' ? UI.$(sel) : sel; if (!el) return; el.textContent = text || ''; el.classList.remove('error', 'muted'); if (kind) el.classList.add(kind); }; // Toast — auto-hides after 6s UI.toast = function (msg, kind) { const el = UI.$('#toast'); if (!el) return; el.textContent = msg; el.classList.remove('hidden', 'ok', 'error'); if (kind) el.classList.add(kind); clearTimeout(UI._toastTimer); UI._toastTimer = setTimeout(() => el.classList.add('hidden'), 6000); }; UI.showProgress = function (label, pct) { UI.setHidden('#progress-row', false); const lEl = UI.$('#progress-label'); if (lEl) lEl.textContent = label; const fEl = UI.$('#progress-fill'); if (fEl) fEl.style.width = Math.max(0, Math.min(100, pct || 0)) + '%'; }; UI.hideProgress = function () { UI.setHidden('#progress-row', true); }; // Slide panels UI.openSlide = function (overlayId, panelId) { UI.setHidden('#' + overlayId, false); UI.setHidden('#' + panelId, false); }; UI.closeSlide = function (overlayId, panelId) { UI.setHidden('#' + overlayId, true); UI.setHidden('#' + panelId, true); }; // Format helpers UI.formatBytes = function (n) { if (!Number.isFinite(n)) return ''; const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return n.toFixed(n < 10 && i > 0 ? 1 : 0) + ' ' + u[i]; }; UI.formatDuration = function (sec) { if (!Number.isFinite(sec)) return '—'; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = Math.floor(sec % 60); return h > 0 ? h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0') : m + ':' + String(s).padStart(2, '0'); }; UI.sanitizeFilename = function (s) { return String(s || 'asset').replace(/[\\/:*?"<>|]/g, '_').slice(0, 120); }; UI.escapeHtml = function (s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }; UI.escapeXml = function (s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,'''); }; // FCP timecode / frame rate helpers (used by timeline module) UI.timecodeFromFrames = function (frames, fps) { const r = Math.round(fps); const f = frames % r; const totalSec = Math.floor(frames / r); const s = totalSec % 60; const m = Math.floor(totalSec / 60) % 60; const h = Math.floor(totalSec / 3600); return h + ':' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0') + ':' + String(f).padStart(2,'0'); }; UI.formatFrameRate = function (fps) { // FCP XML uses rational notation: 1001/30000 for 29.97, 1/25 for 25, etc. const known = { 23.976: '1001/24000', 24: '1/24', 25: '1/25', 29.97: '1001/30000', 30: '1/30', 50: '1/50', 59.94: '1001/60000', 60: '1/60', }; // Find closest key let best = '1001/30000'; let bestDist = Infinity; for (const k in known) { const d = Math.abs(Number(k) - fps); if (d < bestDist) { bestDist = d; best = known[k]; } } return best; }; window.UI = UI; })();