UXP v2.1.0: library.js — project filter, status badges, details panel, growing tab, growing poll

This commit is contained in:
Zac Gaetano 2026-05-28 00:58:57 -04:00
parent cd18988d6d
commit 2608d7a465

View file

@ -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 = '<option value="all">All Projects</option>';
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 = '<option value="">— Select project —</option>';
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 = '<div class="empty muted">Loading…</div>';
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 = '<div class="empty muted">Error: ' + e.message + '</div>';
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 = '<span class="muted">—</span>';
}
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 = '<div class="empty muted">Loading…</div>';
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;