// Asset library — v2.1.0 // Handles: asset grid render, card selection, details panel, tabs, growing poll. (function () { const Library = {}; Library.state = { assets: [], growing: [], selectedId: null, currentTab: 'library', // 'library' | 'growing' projects: [], selectedProject: 'all', searchQuery: '', _growingTimer: null, _livePolls: {}, // assetId → intervalId (status poll for live → ready) _importedAssets: JSON.parse(localStorage.getItem('df.uxp.imported') || '{}'), }; // ── Project filter ────────────────────────────────────────────── Library.loadProjects = async function () { try { const projects = await API.listProjects(); Library.state.projects = projects; const sel = document.getElementById('project-filter'); sel.innerHTML = ''; projects.forEach(p => { const o = document.createElement('option'); o.value = p.id; o.textContent = p.name; sel.appendChild(o); }); // Also populate export project selects ['export-proj-select'].forEach(id => { const el = document.getElementById(id); if (!el) return; el.innerHTML = ''; projects.forEach(p => { const o = document.createElement('option'); o.value = p.id; o.textContent = p.name; el.appendChild(o); }); }); } catch (e) { console.warn('loadProjects:', e.message); } }; // ── Tab switching ─────────────────────────────────────────────── Library.switchTab = function (tab) { Library.state.currentTab = tab; document.getElementById('tab-library').classList.toggle('active', tab === 'library'); document.getElementById('tab-growing').classList.toggle('active', tab === 'growing'); document.getElementById('library-container').classList.toggle('hidden', tab !== 'library'); document.getElementById('growing-container').classList.toggle('hidden', tab !== 'growing'); Library.hideDetails(); if (tab === 'library') Library.refresh(Library.state.searchQuery); else Library._pollGrowing(); }; // ── Growing poll ──────────────────────────────────────────────── Library.startGrowingPoll = function () { Library.stopGrowingPoll(); Library._pollGrowing(); Library.state._growingTimer = setInterval(Library._pollGrowing, 5000); }; Library.stopGrowingPoll = function () { if (Library.state._growingTimer) { clearInterval(Library.state._growingTimer); Library.state._growingTimer = null; } }; Library._pollGrowing = async function () { if (!API.state.connected) return; try { const data = await API.listAssets(Library.state.searchQuery, Library.state.selectedProject); const all = (data && (data.assets || data.rows)) || []; Library.state.growing = all.filter(a => a.status === 'live' || a.status === 'ingesting' || a.status === 'processing' || (a.status === 'ready' && Library.state._importedAssets['live:' + a.id]) ); const active = Library.state.growing.filter(a => a.status === 'live' || a.status === 'ingesting' || a.status === 'processing' ).length; const badge = document.getElementById('growing-count'); badge.textContent = active; badge.style.display = active > 0 ? 'inline-flex' : 'none'; if (Library.state.currentTab === 'growing') { Library._renderGrid('growing-grid', Library.state.growing); } } catch (_) {} }; // ── Asset refresh ─────────────────────────────────────────────── Library.refresh = async function (query) { Library.state.searchQuery = query || ''; const grid = document.getElementById('asset-grid'); grid.innerHTML = '
Loading…
'; try { const data = await API.listAssets(Library.state.searchQuery, Library.state.selectedProject); Library.state.assets = (data && (data.assets || data.rows)) || []; Library._renderGrid('asset-grid', Library.state.assets); UI.toast('Loaded ' + Library.state.assets.length + ' assets', 'ok'); } catch (e) { grid.innerHTML = '
Error: ' + e.message + '
'; UI.toast('Asset load failed: ' + e.message, 'error'); } }; // ── Grid render ───────────────────────────────────────────────── Library._renderGrid = function (gridId, assets) { const grid = document.getElementById(gridId); grid.innerHTML = ''; if (!assets.length) { const e = document.createElement('div'); e.className = 'empty muted'; e.textContent = gridId === 'growing-grid' ? 'No growing files' : 'No assets'; grid.appendChild(e); return; } assets.forEach(a => grid.appendChild(Library._makeCard(a))); Library._syncActions(); }; // Fetch thumbnail with Bearer auth, assign as blob URL. // img.src direct assignment never sends Authorization headers. Library._loadThumb = function (assetId, img) { API.request('/api/v1/assets/' + assetId + '/thumbnail?redirect=1') .then(function (r) { return r.ok ? r.blob() : Promise.reject(new Error('HTTP ' + r.status)); }) .then(function (blob) { img.src = URL.createObjectURL(blob); }) .catch(function () { const p = document.createElement('div'); p.className = 'asset-thumb-placeholder'; p.textContent = 'no preview'; if (img.parentNode) img.replaceWith(p); }); }; Library._makeCard = function (asset) { const card = document.createElement('div'); card.className = 'asset-card'; if (asset.id === Library.state.selectedId) card.classList.add('selected'); card.dataset.assetId = asset.id; // Thumbnail — loaded async with auth header const img = document.createElement('img'); img.className = 'asset-thumb'; img.alt = asset.display_name || asset.filename || asset.id; card.appendChild(img); Library._loadThumb(asset.id, img); // Status badge const status = (asset.status || 'ready').toLowerCase(); const badge = document.createElement('div'); badge.className = 'asset-status status-' + status; badge.textContent = status.toUpperCase(); card.appendChild(badge); // Info row const info = document.createElement('div'); info.className = 'asset-info'; const name = document.createElement('div'); name.className = 'asset-name'; name.title = asset.display_name || asset.filename || asset.id; name.textContent = asset.display_name || asset.filename || asset.id; info.appendChild(name); const meta = document.createElement('div'); meta.className = 'asset-meta'; const dur = asset.duration_ms ? UI.formatDuration(asset.duration_ms / 1000) : (status === 'live' ? 'LIVE' : '—'); const codec = asset.codec || asset.media_type || ''; meta.textContent = dur + (codec ? ' · ' + codec.toUpperCase() : ''); info.appendChild(meta); card.appendChild(info); card.addEventListener('click', () => Library.select(asset.id)); return card; }; // ── Selection ─────────────────────────────────────────────────── Library.select = function (id) { Library.state.selectedId = id; // Re-render selection highlight without full refresh document.querySelectorAll('.asset-card').forEach(c => { c.classList.toggle('selected', c.dataset.assetId === id); }); const asset = Library.selectedAsset(); if (asset) Library._showDetails(asset); Library._syncActions(); }; Library.selectedAsset = function () { const all = Library.state.currentTab === 'growing' ? Library.state.growing : Library.state.assets; return all.find(a => a.id === Library.state.selectedId) || null; }; Library.hideDetails = function () { // v2.2.0: details panel dropped — clearing selection is enough. Library.state.selectedId = null; Library._syncActions(); }; Library._showDetails = function (_asset) { // v2.2.0: card already shows display_name / duration / codec, and the // accent-bordered selected card is the affordance. No-op kept so call // sites in main.js / library.js don't need branching. }; Library._syncActions = function () { const sel = Library.selectedAsset(); const live = sel && sel.status === 'live'; const ready = sel && sel.status === 'ready'; const hasLiveImport = sel && !!Library.state._importedAssets['live:' + sel.id]; _btn('import-proxy-btn').disabled = !sel || live; _btn('import-hires-btn').disabled = !sel || live || !sel.original_s3_key; _btn('mount-live-btn').disabled = !sel || !live; _btn('relink-btn').disabled = !(ready && hasLiveImport); // export-timeline-btn (Export menu) and upload-mam-btn are always available. }; function _btn(id) { return document.getElementById(id) || { disabled: false }; } // ── Import mapping persistence ────────────────────────────────── Library.recordImport = function (key, entry) { Library.state._importedAssets[key] = entry; try { localStorage.setItem('df.uxp.imported', JSON.stringify(Library.state._importedAssets)); } catch (_) {} }; Library.getImport = function (key) { return Library.state._importedAssets[key] || null; }; Library.resolveClipsToAssets = function (clips) { return clips.map(clip => { const byPath = Library.getImport(clip.filePath); const byName = Library.getImport('name:' + clip.fileName); const entry = byPath || byName; return Object.assign({}, clip, { asset_id: entry ? entry.assetId : null }); }); }; // ── Live status poll (single asset: live → ready) ─────────────── Library.startLiveStatusPoll = function (assetId) { if (Library.state._livePolls[assetId]) return; Library.state._livePolls[assetId] = setInterval(async () => { try { const a = await API.getAsset(assetId); if (a.status === 'ready') { clearInterval(Library.state._livePolls[assetId]); delete Library.state._livePolls[assetId]; UI.toast('"' + (a.display_name || assetId) + '" finalized — Relink Hi-Res available', 'ok'); if (Library.state.selectedId === assetId) { Library._showDetails(a); Library._syncActions(); } } } catch (_) {} }, 5000); }; window.Library = Library; })();