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;