UXP v2.1.0: library.js — project filter, status badges, details panel, growing tab, growing poll
This commit is contained in:
parent
cd18988d6d
commit
2608d7a465
1 changed files with 239 additions and 68 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue