dragonflight/services/premiere-plugin-uxp/src/library.js
ZGaetano c3e4306d9f fix(uxp): fetch thumbnails via API.request() to carry Bearer token
img.src direct assignment never sends Authorization headers, so all
thumbnail requests returned 401 once the global auth gate was enabled.
Now fetches via API.request(), converts response to a blob URL, and
assigns that to img.src. Falls back to the placeholder div on error.
2026-05-28 09:41:26 -04:00

287 lines
12 KiB
JavaScript

// 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 = '<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 (!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 () {
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 {
tagsEl.innerHTML = '<span class="muted">—</span>';
}
};
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;
})();