The redesigned UXP panel (left icon rail, compact list-view toggle, hover tooltips, single Export menu) was committed only to redesign/panel-icon-rail and never merged, so main + the website kept serving the old blocky-button build under the same version number (2.2.2). That branch had diverged off an old main and is missing recent worker/HLS/NVENC/import work, so it can't be merged wholesale — cherry-pick just the plugin instead. - services/premiere-plugin-uxp: replace source with the redesigned panel (adds src/tooltip.js; reworks index.html + styles.css + src/*). Verified byte-identical to the build installed on BMG-PC-Edit. - web-ui/public/downloads/dragonflight-mam-2.2.2.ccx: swap the served artifact to the redesigned 34708-byte build (download link unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
600 lines
28 KiB
JavaScript
600 lines
28 KiB
JavaScript
// Dragonflight UXP Panel — main.js v2.1.5
|
|
|
|
(function () {
|
|
if (window.__df_uxp_started) return;
|
|
window.__df_uxp_started = true;
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
// UXP renders native <button> chrome that ignores CSS `background` and does
|
|
// not draw <svg>-only button content, so the rail/dock icon controls are
|
|
// <div role="button"> (divs render custom backgrounds + SVG children fine).
|
|
// Divs have no native `disabled`, so reflect the `.disabled` property the
|
|
// rest of the code sets onto a [disabled] attribute the stylesheet keys off.
|
|
const ICON_CONTROLS = [
|
|
'menu-btn', 'tab-library', 'tab-growing', 'export-timeline-btn', 'refresh-btn',
|
|
'import-proxy-btn', 'import-hires-btn', 'mount-live-btn', 'relink-btn', 'upload-mam-btn'
|
|
];
|
|
function enableDivDisabled() {
|
|
ICON_CONTROLS.forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (!el || Object.getOwnPropertyDescriptor(el, 'disabled')) return;
|
|
Object.defineProperty(el, 'disabled', {
|
|
configurable: true,
|
|
get() { return this.hasAttribute('disabled'); },
|
|
set(v) { if (v) this.setAttribute('disabled', ''); else this.removeAttribute('disabled'); }
|
|
});
|
|
});
|
|
}
|
|
|
|
// Asset layout toggle: compact list (default) vs thumbnail grid. Persisted
|
|
// in localStorage when available (UXP host permitting), else session-only.
|
|
const GRID_ICON = '<svg width="15" height="15" viewBox="0 0 24 24"><path fill="currentColor" d="M4 11h5V5H4v6zm0 7h5v-6H4v6zm6 0h5v-6h-5v6zm6 0h5v-6h-5v6zm-6-7h5V5h-5v6zm6-6v6h5V5h-5z"/></svg>';
|
|
const LIST_ICON = '<svg width="15" height="15" viewBox="0 0 24 24"><path fill="currentColor" d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>';
|
|
let _viewMode = null;
|
|
function getViewMode() {
|
|
if (_viewMode) return _viewMode;
|
|
try { _viewMode = localStorage.getItem('df_view_mode'); } catch (e) {}
|
|
return _viewMode || 'list';
|
|
}
|
|
function applyViewMode(mode) {
|
|
_viewMode = mode === 'grid' ? 'grid' : 'list';
|
|
try { localStorage.setItem('df_view_mode', _viewMode); } catch (e) {}
|
|
const isList = _viewMode === 'list';
|
|
document.querySelectorAll('.asset-grid').forEach(g => g.classList.toggle('list-view', isList));
|
|
const btn = $('view-toggle-btn');
|
|
if (btn) {
|
|
// Show the icon for the layout a click switches TO.
|
|
btn.innerHTML = isList ? GRID_ICON : LIST_ICON;
|
|
btn.setAttribute('data-tip', isList ? 'Grid view' : 'List view');
|
|
}
|
|
}
|
|
|
|
function syncConnectBtn() {
|
|
$('connect-btn').disabled = !$('server-url').value.trim() || !$('api-token').value.trim();
|
|
}
|
|
|
|
async function tryConnect(serverUrl, apiToken) {
|
|
UI.setStatus('#connect-status', 'Connecting…', 'muted');
|
|
try {
|
|
const me = await API.connect(serverUrl, apiToken);
|
|
const who = (me && (me.display_name || me.username)) || serverUrl;
|
|
$('connected-host').textContent = who === serverUrl ? who : who + ' @ ' + serverUrl;
|
|
UI.setStatus('#connect-status', '', 'muted');
|
|
UI.showPane('library');
|
|
await Library.loadProjects();
|
|
await Library.refresh('');
|
|
Library.startGrowingPoll();
|
|
Timeline.refreshSeqBar().catch(() => {});
|
|
} catch (e) {
|
|
API.state.connected = false;
|
|
UI.setStatus('#connect-status', 'Connect failed: ' + e.message, 'error');
|
|
UI.showPane('connect');
|
|
}
|
|
}
|
|
|
|
function wireConnectPane() {
|
|
$('server-url').value = API.state.serverUrl;
|
|
$('api-token').value = API.state.apiToken;
|
|
syncConnectBtn();
|
|
['input','change'].forEach(ev => {
|
|
$('server-url').addEventListener(ev, syncConnectBtn);
|
|
$('api-token').addEventListener(ev, syncConnectBtn);
|
|
});
|
|
$('connect-btn').addEventListener('click', async () => {
|
|
await tryConnect($('server-url').value.trim(), $('api-token').value.trim());
|
|
});
|
|
}
|
|
|
|
function wireLibraryPane() {
|
|
// Overflow menu toggle (v2.1.8). Click ⋯ to open; click elsewhere closes.
|
|
const menuBtn = $('menu-btn');
|
|
const menu = $('status-menu');
|
|
if (menuBtn && menu) {
|
|
menuBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
menu.classList.toggle('hidden');
|
|
});
|
|
document.addEventListener('click', (e) => {
|
|
if (!menu.classList.contains('hidden')
|
|
&& !menu.contains(e.target)
|
|
&& e.target !== menuBtn) {
|
|
menu.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
$('disconnect-btn').addEventListener('click', () => {
|
|
if (menu) menu.classList.add('hidden');
|
|
API.disconnect();
|
|
Library.stopGrowingPoll();
|
|
$('server-url').value = '';
|
|
$('api-token').value = '';
|
|
syncConnectBtn();
|
|
UI.showPane('connect');
|
|
UI.setStatus('#connect-status', 'Disconnected.', 'muted');
|
|
});
|
|
|
|
$('tab-library').addEventListener('click', () => Library.switchTab('library'));
|
|
$('tab-growing').addEventListener('click', () => Library.switchTab('growing'));
|
|
|
|
const vt = $('view-toggle-btn');
|
|
if (vt) vt.addEventListener('click', () => {
|
|
applyViewMode(getViewMode() === 'list' ? 'grid' : 'list');
|
|
});
|
|
|
|
let searchTimer;
|
|
$('search-input').addEventListener('input', e => {
|
|
clearTimeout(searchTimer);
|
|
const q = e.target.value;
|
|
searchTimer = setTimeout(() => {
|
|
if (Library.state.currentTab === 'library') Library.refresh(q);
|
|
else { Library.state.searchQuery = q; Library._pollGrowing(); }
|
|
}, 280);
|
|
});
|
|
|
|
$('project-filter').addEventListener('change', e => {
|
|
Library.state.selectedProject = e.target.value;
|
|
if (Library.state.currentTab === 'library') Library.refresh(Library.state.searchQuery);
|
|
else Library._pollGrowing();
|
|
});
|
|
|
|
$('refresh-btn').addEventListener('click', () => Library.refresh($('search-input').value));
|
|
|
|
$('import-proxy-btn').addEventListener('click', async () => {
|
|
const a = Library.selectedAsset(); if (!a) return;
|
|
_disableImportBtns(true);
|
|
try {
|
|
const { localPath, safeName } = await Import.proxy(a);
|
|
Library.recordImport(localPath, { assetId: a.id, displayName: a.display_name || a.filename });
|
|
Library.recordImport('name:' + safeName, { assetId: a.id, displayName: a.display_name || a.filename });
|
|
} catch (e) { UI.hideProgress(); UI.toast('Proxy import failed: ' + e.message, 'error'); }
|
|
finally { _disableImportBtns(false); Library._syncActions(); }
|
|
});
|
|
|
|
$('import-hires-btn').addEventListener('click', async () => {
|
|
const a = Library.selectedAsset(); if (!a) return;
|
|
_disableImportBtns(true);
|
|
try {
|
|
const { localPath, safeName } = await Import.hires(a);
|
|
Library.recordImport(localPath, { assetId: a.id, displayName: a.display_name || a.filename });
|
|
Library.recordImport('name:' + safeName, { assetId: a.id, displayName: a.display_name || a.filename });
|
|
} catch (e) { UI.hideProgress(); UI.toast('Hi-res import failed: ' + e.message, 'error'); }
|
|
finally { _disableImportBtns(false); Library._syncActions(); }
|
|
});
|
|
|
|
// ── Upload highlighted bin file(s) to the MAM ──
|
|
$('upload-mam-btn').addEventListener('click', uploadToMam);
|
|
|
|
$('mount-live-btn').addEventListener('click', async () => {
|
|
const a = Library.selectedAsset(); if (!a) return;
|
|
$('mount-live-btn').disabled = true;
|
|
UI.showProgress('Resolving live path…', 10);
|
|
try {
|
|
const info = await API.getLivePath(a.id);
|
|
const hostPath = info.win_path || info.posix_path;
|
|
UI.showProgress('Importing live file…', 50);
|
|
await Import.importIntoProject(hostPath);
|
|
Library.recordImport('live:' + a.id, { assetId: a.id, displayName: info.display_name, livePath: hostPath });
|
|
Library.startLiveStatusPoll(a.id);
|
|
UI.hideProgress();
|
|
UI.toast('Mounted live: ' + (info.display_name || a.id), 'ok');
|
|
} catch (e) { UI.hideProgress(); UI.toast('Mount live failed: ' + e.message, 'error'); }
|
|
finally { Library._syncActions(); }
|
|
});
|
|
|
|
// ── Relink single asset (proxy → hi-res) ──
|
|
// ALL premierepro calls must be awaited — runtime returns Promises
|
|
$('relink-btn').addEventListener('click', async () => {
|
|
const a = Library.selectedAsset(); if (!a) return;
|
|
const entry = Library.getImport('live:' + a.id);
|
|
if (!entry) { UI.toast('No live mount recorded', 'error'); return; }
|
|
$('relink-btn').disabled = true;
|
|
UI.showProgress('Fetching hi-res…', 10);
|
|
try {
|
|
const info = await API.getHiresInfo(a.id);
|
|
const safeName = UI.sanitizeFilename(info.filename || (a.display_name || a.id) + '.' + (info.ext || 'mxf'));
|
|
const dest = await Import._tempPath(safeName);
|
|
UI.showProgress('Downloading ' + safeName + '…', 20);
|
|
// S3 presigned URL — no auth, auto-follow redirects (UXP handles)
|
|
const r = await API.requestExternal(info.url);
|
|
if (!r.ok) throw new Error('Download HTTP ' + r.status);
|
|
UI.showProgress('Writing to disk…', 65);
|
|
const buf = await r.arrayBuffer();
|
|
await Import._writeBuffer(dest, buf);
|
|
UI.showProgress('Relinking…', 85);
|
|
const P = require('premierepro');
|
|
const proj = await P.Project.getActiveProject(); // ← must await
|
|
if (!proj) throw new Error('No active project');
|
|
await Timeline._relinkInProject(proj, entry.livePath, dest);
|
|
Library.recordImport(dest, { assetId: a.id, displayName: a.display_name || a.filename });
|
|
UI.hideProgress();
|
|
UI.toast('Relinked: ' + safeName, 'ok');
|
|
} catch (e) { UI.hideProgress(); UI.toast('Relink failed: ' + e.message, 'error'); }
|
|
finally { Library._syncActions(); }
|
|
});
|
|
|
|
// Single Export entry → popup menu (Conform Timeline / Local Export).
|
|
wireExportMenu();
|
|
|
|
// Advanced collapsible toggle (v2.2.0).
|
|
const advToggle = $('advanced-toggle');
|
|
const advBody = $('advanced-body');
|
|
if (advToggle && advBody) {
|
|
advToggle.addEventListener('click', () => {
|
|
const open = advBody.classList.toggle('hidden') === false;
|
|
advToggle.setAttribute('aria-expanded', String(open));
|
|
});
|
|
}
|
|
}
|
|
|
|
function _disableImportBtns(dis) {
|
|
['import-proxy-btn','import-hires-btn'].forEach(id => { $(id).disabled = dis; });
|
|
}
|
|
|
|
function _basename(p) { return String(p).split(/[\\/]/).pop(); }
|
|
|
|
// Target project for uploads/auto-upload: the project filter when specific,
|
|
// else the only project if there's exactly one, else null (caller prompts).
|
|
function getTargetProjectId() {
|
|
const sel = Library.state.selectedProject;
|
|
if (sel && sel !== 'all') return sel;
|
|
const projs = Library.state.projects || [];
|
|
return projs.length === 1 ? projs[0].id : null;
|
|
}
|
|
|
|
// ── Export screen (full-panel chooser → Conform / Local Export) ──
|
|
function wireExportMenu() {
|
|
const btn = $('export-timeline-btn');
|
|
if (!btn) return;
|
|
const close = () => UI.setHidden('#export-screen', true);
|
|
btn.addEventListener('click', () => UI.setHidden('#export-screen', false));
|
|
const closeBtn = $('export-screen-close');
|
|
if (closeBtn) closeBtn.addEventListener('click', close);
|
|
const optConform = $('opt-conform');
|
|
if (optConform) optConform.addEventListener('click', () => { close(); openConformPanel(); });
|
|
const optLocal = $('opt-local-export');
|
|
if (optLocal) optLocal.addEventListener('click', () => { close(); runLocalExport(); });
|
|
}
|
|
|
|
// ── Upload highlighted bin file(s) (or file-picker fallback) ─────
|
|
async function uploadToMam() {
|
|
const projectId = getTargetProjectId();
|
|
if (!projectId) { UI.toast('Pick a target project (project filter) before uploading', 'error'); return; }
|
|
let paths = [];
|
|
try { paths = await Import.getSelectedBinPaths(); } catch (_) {}
|
|
if (!paths.length) {
|
|
UI.toast('No bin selection — choose file(s) to upload', 'muted');
|
|
try { paths = await Import.pickFiles(); }
|
|
catch (e) { UI.toast('File picker unavailable: ' + e.message, 'error'); return; }
|
|
}
|
|
if (!paths.length) return;
|
|
let ok = 0, fail = 0;
|
|
for (let i = 0; i < paths.length; i++) {
|
|
const name = _basename(paths[i]);
|
|
UI.showProgress('Uploading ' + name + ' (' + (i + 1) + '/' + paths.length + ')…', 10 + (i / paths.length) * 80);
|
|
try { await Import.uploadFile(paths[i], { projectId }); ok++; }
|
|
catch (e) { fail++; console.warn('[df] upload failed', paths[i], e.message); }
|
|
}
|
|
UI.hideProgress();
|
|
UI.toast('Uploaded ' + ok + (fail ? ', ' + fail + ' failed' : '') + ' to MAM', fail ? 'error' : 'ok');
|
|
if (ok) Library.refresh(Library.state.searchQuery);
|
|
}
|
|
|
|
// ── Local Export (server FFMPEG-trims hi-res → download → relink) ─
|
|
async function runLocalExport() {
|
|
const projectId = getTargetProjectId();
|
|
UI.showProgress('Reading Premiere sequence…', 8);
|
|
let td;
|
|
try { td = await Timeline.readActiveSequence(); }
|
|
catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
|
|
if (!td.clips.length) { UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; }
|
|
|
|
let resolved = Library.resolveClipsToAssets(td.clips);
|
|
const missing = resolved.filter(c => !c.asset_id && c.filePath);
|
|
if (missing.length) {
|
|
if (!projectId) { UI.hideProgress(); UI.toast(missing.length + ' clip(s) not in MAM — pick a target project so they can be uploaded', 'error'); return; }
|
|
try {
|
|
await Import.ensureClipsInMam(resolved, projectId, (name, n, total) =>
|
|
UI.showProgress('Uploading missing source ' + name + ' (' + n + '/' + total + ')…', 8 + (n / total) * 20));
|
|
resolved = Library.resolveClipsToAssets(td.clips);
|
|
} catch (e) { UI.hideProgress(); UI.toast('Auto-upload failed: ' + e.message, 'error'); return; }
|
|
}
|
|
|
|
try {
|
|
const res = await Timeline.localExport(resolved, (label, pct) => UI.showProgress(label, pct));
|
|
UI.hideProgress();
|
|
if (res.failed) UI.toast('Local Export: ' + res.succeeded + ' ok, ' + res.failed + ' failed', 'error');
|
|
else UI.toast('Local Export complete — ' + res.succeeded + ' clip(s) relinked', 'ok');
|
|
} catch (e) { UI.hideProgress(); UI.toast('Local Export failed: ' + e.message, 'error'); }
|
|
}
|
|
|
|
let _seqCache = null;
|
|
|
|
// ── One-click Export Timeline ────────────────────────────────────
|
|
//
|
|
// Contract: clicking Export Timeline reads the active Premiere
|
|
// sequence, pushes it to MAM, kicks off a hi-res conform render, and
|
|
// lets the result land as an asset in the Library. No prompts.
|
|
//
|
|
// Defaults are baked in (ProRes 422 HQ / source res / broadcast
|
|
// audio). Advanced → Export & Conform is still wired for fine-grained
|
|
// control.
|
|
const LS_EXPORT_PROJECT = 'df.uxp.exportProjectId';
|
|
const DEFAULT_EXPORT_OPTS = {
|
|
codec: 'prores_hq',
|
|
quality: 'high',
|
|
resolution: 'source',
|
|
audio: 'broadcast',
|
|
};
|
|
|
|
// Resolve a target MAM project for the export. Cached in localStorage
|
|
// after first run so subsequent exports require zero clicks. Falls
|
|
// back to the first project on the server. If the cache references a
|
|
// project that no longer exists, we transparently re-pick.
|
|
async function resolveExportProject() {
|
|
const cached = localStorage.getItem(LS_EXPORT_PROJECT) || '';
|
|
let projects;
|
|
try { projects = await API.listProjects(); }
|
|
catch (e) { throw new Error('Project lookup failed: ' + e.message); }
|
|
if (!Array.isArray(projects) || !projects.length) {
|
|
throw new Error('No projects in MAM — create one in the web UI first');
|
|
}
|
|
if (cached && projects.some(p => p.id === cached)) return cached;
|
|
const id = projects[0].id;
|
|
localStorage.setItem(LS_EXPORT_PROJECT, id);
|
|
return id;
|
|
}
|
|
|
|
async function oneClickExport() {
|
|
UI.$('#export-timeline-btn').disabled = true;
|
|
try {
|
|
UI.showProgress('Reading Premiere sequence…', 5);
|
|
let td;
|
|
try { td = await Timeline.readActiveSequence(); }
|
|
catch (e) { throw new Error('Timeline read failed: ' + e.message); }
|
|
if (!td || !td.clips || !td.clips.length) {
|
|
throw new Error('No clips in active sequence');
|
|
}
|
|
|
|
UI.showProgress('Resolving target project…', 10);
|
|
const projectId = await resolveExportProject();
|
|
const seqName = td.sequenceName || 'Premiere Export';
|
|
|
|
UI.showProgress('Pushing timeline + queueing render…', 15);
|
|
let jobId;
|
|
try {
|
|
jobId = await Timeline.startConform(projectId, seqName, td, DEFAULT_EXPORT_OPTS);
|
|
} catch (e) { throw new Error('Export failed: ' + e.message); }
|
|
if (!jobId) throw new Error('Export started but no job id was returned');
|
|
|
|
// Poll the conform job to completion. Map progress 20→95% so the
|
|
// user sees the bar move during the long render step.
|
|
UI.showProgress('Rendering hi-res…', 20);
|
|
await new Promise((resolve) => {
|
|
Timeline.pollConform(jobId,
|
|
(progress, status) => {
|
|
UI.showProgress('Rendering hi-res (' + (status || 'queued') + ')…', 20 + 0.75 * (Number(progress) || 0));
|
|
},
|
|
(job) => {
|
|
UI.hideProgress();
|
|
if (job && job.status === 'completed') {
|
|
UI.toast('Rendered: ' + seqName + ' — now in Library', 'ok');
|
|
const q = ($('search-input') && $('search-input').value) || '';
|
|
Library.refresh(q).catch(() => {});
|
|
} else {
|
|
const why = (job && (job.error || job.message)) || 'unknown reason';
|
|
UI.toast('Render failed: ' + why, 'error');
|
|
}
|
|
resolve();
|
|
}
|
|
);
|
|
});
|
|
} catch (e) {
|
|
UI.hideProgress();
|
|
UI.toast(e.message || 'Export failed', 'error');
|
|
} finally {
|
|
UI.$('#export-timeline-btn').disabled = false;
|
|
}
|
|
}
|
|
|
|
async function openExportPanel() {
|
|
UI.showProgress('Reading Premiere sequence…', 20);
|
|
try { _seqCache = await Timeline.readActiveSequence(); }
|
|
catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
|
|
if (!_seqCache.clips.length) { UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; }
|
|
UI.hideProgress();
|
|
const resolved = Library.resolveClipsToAssets(_seqCache.clips);
|
|
const matched = resolved.filter(c => c.asset_id).length;
|
|
$('export-seq-name').value = _seqCache.sequenceName || 'Sequence 1';
|
|
$('export-clip-info').textContent = matched + ' of ' + _seqCache.clips.length + ' clip(s) matched to MAM assets';
|
|
UI.openSlide('export-overlay', 'export-panel');
|
|
}
|
|
|
|
function wireExportPanel() {
|
|
$('export-close-btn').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
|
|
$('export-cancel-btn').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
|
|
$('export-overlay').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
|
|
$('export-confirm-btn').addEventListener('click', async () => {
|
|
if (!_seqCache) return;
|
|
const seqName = ($('export-seq-name').value || '').trim() || 'Sequence 1';
|
|
const projectId = $('export-proj-select').value;
|
|
if (!projectId) { UI.toast('Select a target project', 'error'); return; }
|
|
UI.closeSlide('export-overlay', 'export-panel');
|
|
UI.showProgress('Pushing timeline…', 20);
|
|
try {
|
|
const { matched, skipped } = await Timeline.pushToMAM(seqName, projectId, _seqCache);
|
|
UI.hideProgress();
|
|
UI.toast('Pushed ' + matched + ' clip(s)' + (skipped ? ' (' + skipped + ' skipped)' : ''), 'ok');
|
|
} catch (e) { UI.hideProgress(); UI.toast('Export failed: ' + e.message, 'error'); }
|
|
});
|
|
}
|
|
|
|
async function openConformPanel() {
|
|
UI.showProgress('Reading Premiere sequence…', 20);
|
|
try { _seqCache = await Timeline.readActiveSequence(); }
|
|
catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
|
|
if (!_seqCache.clips.length) { UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; }
|
|
UI.hideProgress();
|
|
const resolved = Library.resolveClipsToAssets(_seqCache.clips);
|
|
const total = _seqCache.clips.length;
|
|
const matched = resolved.filter(c => c.asset_id).length;
|
|
const missing = total - matched;
|
|
$('conform-clip-info').textContent = missing
|
|
? matched + ' of ' + total + ' clip(s) in MAM — ' + missing + ' will be uploaded first'
|
|
: matched + ' of ' + total + ' clip(s) in MAM';
|
|
$('conform-start-btn').disabled = total === 0;
|
|
const conformProj = $('conform-proj-select');
|
|
if (conformProj) {
|
|
conformProj.innerHTML = '<option value="">— Select project —</option>';
|
|
Library.state.projects.forEach(p => {
|
|
const o = document.createElement('option'); o.value = p.id; o.textContent = p.name;
|
|
conformProj.appendChild(o);
|
|
});
|
|
}
|
|
UI.openSlide('conform-overlay', 'conform-panel');
|
|
}
|
|
|
|
function wireConformPanel() {
|
|
$('conform-close-btn').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel'));
|
|
$('conform-cancel-btn').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel'));
|
|
$('conform-overlay').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel'));
|
|
$('preset-cards').addEventListener('click', e => {
|
|
const card = e.target.closest('.preset-card'); if (!card) return;
|
|
document.querySelectorAll('.preset-card').forEach(c => c.classList.remove('selected'));
|
|
card.classList.add('selected');
|
|
const presets = {
|
|
broadcast: { codec:'prores_hq', quality:'high', resolution:'1080p', audio:'broadcast' },
|
|
web: { codec:'h264', quality:'medium', resolution:'1080p', audio:'web' },
|
|
archive: { codec:'prores_4444', quality:'high', resolution:'uhd', audio:'archive' },
|
|
};
|
|
const p = presets[card.dataset.preset];
|
|
if (p) { $('conform-codec').value=p.codec; $('conform-quality').value=p.quality; $('conform-resolution').value=p.resolution; $('conform-audio').value=p.audio; }
|
|
// Manual codec/quality/res/audio fields appear only for Custom.
|
|
UI.setHidden('#conform-custom', card.dataset.preset !== 'custom');
|
|
});
|
|
$('conform-start-btn').addEventListener('click', async () => {
|
|
if (!_seqCache) return;
|
|
const conformProj = $('conform-proj-select');
|
|
const projectId = conformProj ? conformProj.value : '';
|
|
if (!projectId) { UI.toast('Select a target project', 'error'); return; }
|
|
UI.closeSlide('conform-overlay', 'conform-panel');
|
|
// Auto-upload any timeline sources not yet in the MAM, then conform.
|
|
try {
|
|
const resolved = Library.resolveClipsToAssets(_seqCache.clips);
|
|
const missing = resolved.filter(c => !c.asset_id && c.filePath);
|
|
if (missing.length) {
|
|
await Import.ensureClipsInMam(resolved, projectId, (name, n, tot) =>
|
|
UI.showProgress('Uploading missing source ' + name + ' (' + n + '/' + tot + ')…', 5 + (n / tot) * 10));
|
|
}
|
|
} catch (e) { UI.hideProgress(); UI.toast('Auto-upload failed: ' + e.message, 'error'); return; }
|
|
UI.showProgress('Starting conform job…', 15);
|
|
try {
|
|
const jobId = await Timeline.startConform(projectId, _seqCache.sequenceName, _seqCache, {
|
|
codec:$('conform-codec').value, quality:$('conform-quality').value,
|
|
resolution:$('conform-resolution').value, audio:$('conform-audio').value,
|
|
});
|
|
Timeline.pollConform(jobId,
|
|
(pct, status) => UI.showProgress('Conform: ' + status + ' (' + pct + '%)…', 15 + pct * 0.8),
|
|
job => {
|
|
UI.hideProgress();
|
|
if (job.status==='completed') { UI.toast('Conform complete','ok'); Library.refresh(Library.state.searchQuery); }
|
|
else { UI.toast('Conform failed: '+(job.error||'unknown'),'error'); }
|
|
}
|
|
);
|
|
} catch (e) { UI.hideProgress(); UI.toast('Conform failed: ' + e.message, 'error'); }
|
|
});
|
|
}
|
|
|
|
let _relinkClips = [];
|
|
|
|
async function openRelinkPanel() {
|
|
UI.showProgress('Reading Premiere sequence…', 20);
|
|
try { _seqCache = await Timeline.readActiveSequence(); }
|
|
catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
|
|
UI.hideProgress();
|
|
const resolved = Library.resolveClipsToAssets(_seqCache.clips);
|
|
_relinkClips = resolved;
|
|
const list = $('clip-list');
|
|
list.innerHTML = '';
|
|
$('relink-summary').classList.add('hidden');
|
|
$('relink-start-btn').disabled = true;
|
|
if (!resolved.length) {
|
|
list.innerHTML = '<div class="empty muted">No clips in active sequence</div>';
|
|
} else {
|
|
resolved.forEach((clip, i) => {
|
|
const matched = !!clip.asset_id;
|
|
const row = document.createElement('div');
|
|
row.className = 'clip-item' + (matched ? ' matched' : ' unmatched');
|
|
const cb = document.createElement('input');
|
|
cb.type='checkbox'; cb.id='rclip-'+i; cb.disabled=!matched; cb.checked=matched;
|
|
cb.addEventListener('change', _syncRelinkStart);
|
|
const lbl = document.createElement('label');
|
|
lbl.htmlFor='rclip-'+i; lbl.className='clip-item-name'; lbl.textContent=clip.fileName||'clip';
|
|
const st = document.createElement('span');
|
|
st.className='clip-item-status'; st.textContent=matched?'matched':'not in MAM';
|
|
row.append(cb, lbl, st); list.appendChild(row);
|
|
});
|
|
}
|
|
UI.openSlide('relink-overlay', 'relink-panel');
|
|
_syncRelinkStart();
|
|
}
|
|
|
|
function _syncRelinkStart() {
|
|
$('relink-start-btn').disabled = !document.querySelector('#clip-list input[type="checkbox"]:checked');
|
|
}
|
|
|
|
function wireRelinkPanel() {
|
|
$('relink-close-btn').addEventListener('click', () => UI.closeSlide('relink-overlay', 'relink-panel'));
|
|
$('relink-cancel-btn').addEventListener('click', () => UI.closeSlide('relink-overlay', 'relink-panel'));
|
|
$('relink-overlay').addEventListener('click', () => UI.closeSlide('relink-overlay', 'relink-panel'));
|
|
$('relink-start-btn').addEventListener('click', async () => {
|
|
const checked = document.querySelectorAll('#clip-list input[type="checkbox"]:checked');
|
|
const selected = [];
|
|
checked.forEach(cb => {
|
|
const i = parseInt(cb.id.replace('rclip-',''), 10);
|
|
if (_relinkClips[i] && _relinkClips[i].asset_id) selected.push(_relinkClips[i]);
|
|
});
|
|
if (!selected.length) return;
|
|
UI.closeSlide('relink-overlay', 'relink-panel');
|
|
UI.showProgress('Starting batch relink…', 5);
|
|
try {
|
|
const res = await Timeline.batchRelink(selected);
|
|
UI.hideProgress();
|
|
UI.toast(res.succeeded + ' relinked' + (res.failed ? ', ' + res.failed + ' failed' : ''), res.failed ? 'error' : 'ok');
|
|
} catch (e) { UI.hideProgress(); UI.toast('Batch relink failed: ' + e.message, 'error'); }
|
|
});
|
|
}
|
|
|
|
// Read the plugin version from manifest.json at runtime — the canonical
|
|
// source of truth. Display it on both the connect-pane brand and the
|
|
// library-pane status strip so we can verify which build is actually
|
|
// running (UPIA stacks every install in its own _<version> dir, so the
|
|
// "highest in folder" picker has historically loaded stale copies).
|
|
async function showVersion() {
|
|
let v = '';
|
|
try {
|
|
const lfs = require('uxp').storage.localFileSystem;
|
|
const folder = await lfs.getPluginFolder();
|
|
const entry = await folder.getEntry('manifest.json');
|
|
const text = await entry.read();
|
|
const m = JSON.parse(text);
|
|
v = m && m.version ? ('v' + m.version) : '';
|
|
} catch (_) { /* leave blank — the absence itself signals a bad load */ }
|
|
const a = $('panel-version'); if (a) a.textContent = v;
|
|
const b = $('brand-version'); if (b) b.textContent = v;
|
|
}
|
|
|
|
function init() {
|
|
enableDivDisabled();
|
|
applyViewMode(getViewMode());
|
|
wireConnectPane(); wireLibraryPane();
|
|
wireExportPanel(); wireConformPanel(); wireRelinkPanel();
|
|
showVersion();
|
|
if (API.state.serverUrl && API.state.apiToken) tryConnect(API.state.serverUrl, API.state.apiToken);
|
|
else UI.showPane('connect');
|
|
}
|
|
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
else init();
|
|
})();
|