From 2608d7a465fad0c9b09ac64a80809f3059b00718 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 00:58:57 -0400 Subject: [PATCH] =?UTF-8?q?UXP=20v2.1.0:=20library.js=20=E2=80=94=20projec?= =?UTF-8?q?t=20filter,=20status=20badges,=20details=20panel,=20growing=20t?= =?UTF-8?q?ab,=20growing=20poll?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/premiere-plugin-uxp/src/library.js | 307 +++++++++++++++----- 1 file changed, 239 insertions(+), 68 deletions(-) diff --git a/services/premiere-plugin-uxp/src/library.js b/services/premiere-plugin-uxp/src/library.js index f12ab77..469598a 100644 --- a/services/premiere-plugin-uxp/src/library.js +++ b/services/premiere-plugin-uxp/src/library.js @@ -1,108 +1,279 @@ -// Asset library rendering. Calls API.listAssets, renders a grid into -// #asset-grid, tracks the selected asset for the action buttons. +// Asset library — v2.1.0 +// Handles: asset grid render, card selection, details panel, tabs, growing poll. (function () { const Library = {}; - Library.state = { assets: [], selectedId: null }; - Library.render = function () { - const grid = UI.$('#asset-grid'); + 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 (!Library.state.assets.length) { + if (!assets.length) { const e = document.createElement('div'); e.className = 'empty muted'; - e.textContent = 'No assets'; + e.textContent = gridId === 'growing-grid' ? 'No growing files' : 'No assets'; grid.appendChild(e); return; } - for (const a of Library.state.assets) { - grid.appendChild(makeCard(a)); - } - Library.syncActions(); + assets.forEach(a => grid.appendChild(Library._makeCard(a))); + Library._syncActions(); }; - function makeCard(asset) { + 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; - const thumbKey = asset.thumbnail_s3_key || asset.thumbnail || null; - const thumbUrl = thumbKey - ? `${API.state.serverUrl}/api/v1/assets/${asset.id}/thumbnail?redirect=1` - : null; + // Thumbnail + const thumbUrl = `${API.state.serverUrl}/api/v1/assets/${asset.id}/thumbnail?redirect=1`; + const img = document.createElement('img'); + img.className = 'asset-thumb'; + img.alt = asset.display_name || asset.filename || asset.id; + img.src = thumbUrl; + img.onerror = () => { + const p = document.createElement('div'); + p.className = 'asset-thumb-placeholder'; + p.textContent = 'no preview'; + img.replaceWith(p); + }; + card.appendChild(img); - if (thumbUrl) { - const img = document.createElement('img'); - img.className = 'asset-thumb'; - img.alt = asset.display_name || asset.filename || asset.id; - // UXP fetch supports cross-origin images; the redirect=1 query on - // /thumbnail tells the server to 302 to the presigned S3 URL. - img.src = thumbUrl; - img.onerror = () => { img.replaceWith(placeholder()); }; - card.appendChild(img); - } else { - card.appendChild(placeholder()); - } + // 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; - card.appendChild(name); + 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; - } - - function placeholder() { - const p = document.createElement('div'); - p.className = 'asset-thumb-placeholder'; - p.textContent = 'no preview'; - return p; - } + }; + // ── Selection ─────────────────────────────────────────────────── Library.select = function (id) { Library.state.selectedId = id; - Library.render(); + // 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 () { - return Library.state.assets.find(a => a.id === Library.state.selectedId) || null; + const all = Library.state.currentTab === 'growing' + ? Library.state.growing : Library.state.assets; + return all.find(a => a.id === Library.state.selectedId) || null; }; - Library.syncActions = function () { - const sel = Library.selectedAsset(); - const info = UI.$('#selected-info'); - if (sel) { - info.textContent = (sel.display_name || sel.filename || sel.id) - + (sel.file_size ? ' · ' + UI.formatBytes(Number(sel.file_size)) : ''); - info.classList.remove('muted'); + Library.hideDetails = function () { + Library.state.selectedId = null; + document.getElementById('details-panel').classList.add('hidden'); + Library._syncActions(); + }; + + Library._showDetails = function (asset) { + const p = document.getElementById('details-panel'); + p.classList.remove('hidden'); + document.getElementById('d-filename').textContent = asset.display_name || asset.filename || '—'; + document.getElementById('d-codec').textContent = asset.codec || asset.media_type || '—'; + document.getElementById('d-res').textContent = asset.resolution || '—'; + document.getElementById('d-fps').textContent = asset.fps ? asset.fps + ' fps' : '—'; + document.getElementById('d-dur').textContent = asset.duration_ms ? UI.formatDuration(asset.duration_ms / 1000) : '—'; + document.getElementById('d-size').textContent = asset.file_size ? UI.formatBytes(Number(asset.file_size)) : '—'; + const tagsEl = document.getElementById('d-tags'); + tagsEl.innerHTML = ''; + const tags = asset.tags || []; + if (tags.length) { + tags.forEach(t => { + const s = document.createElement('span'); + s.className = 'tag'; s.textContent = t; + tagsEl.appendChild(s); + }); } else { - info.textContent = 'No asset selected'; - info.classList.add('muted'); + tagsEl.innerHTML = ''; } - UI.$('#import-proxy-btn').disabled = !sel; - UI.$('#import-hires-btn').disabled = !sel || !sel.original_s3_key; }; - Library.refresh = async function (query) { - const grid = UI.$('#asset-grid'); - grid.innerHTML = '
Loading…
'; - try { - const data = await API.listAssets(query); - Library.state.assets = (data && (data.assets || data.rows)) || []; - Library.render(); - // Surface count via toast so we can tell empty-grid (zero) from - // CSS-broken-grid (cards exist but invisible) at a glance. - if (UI.toast) UI.toast('Loaded ' + Library.state.assets.length + ' assets (total ' + (data.total || '?') + ')', 'ok'); - } catch (e) { - grid.innerHTML = ''; - const err = document.createElement('div'); - err.className = 'empty muted'; - err.textContent = 'Error loading assets: ' + e.message; - grid.appendChild(err); - if (UI.toast) UI.toast('Asset load failed: ' + e.message, 'error'); - } + 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); + _btn('import-all-btn').disabled = Library.state.currentTab !== 'library'; + _btn('export-timeline-btn').disabled = false; // available once connected + _btn('export-conform-btn').disabled = false; + _btn('fetch-relink-btn').disabled = false; + }; + + 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;