dragonflight/services/premiere-plugin-uxp/src/main.js

358 lines
17 KiB
JavaScript
Raw Normal View History

// 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);
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'));
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(); }
});
$('import-all-btn').addEventListener('click', async () => {
const assets = Library.state.assets;
if (!assets.length) { UI.toast('No assets', 'error'); return; }
_disableImportBtns(true);
let ok = 0, fail = 0;
for (const a of assets) {
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 });
ok++;
} catch (_) { fail++; }
}
_disableImportBtns(false);
UI.hideProgress();
UI.toast('Import all: ' + ok + ' ok' + (fail ? ', ' + fail + ' failed' : ''), fail ? 'error' : 'ok');
Library._syncActions();
});
$('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(); }
});
$('export-timeline-btn').addEventListener('click', openExportPanel);
$('export-conform-btn').addEventListener('click', openConformPanel);
$('fetch-relink-btn').addEventListener('click', openRelinkPanel);
}
function _disableImportBtns(dis) {
['import-proxy-btn','import-hires-btn'].forEach(id => { $(id).disabled = dis; });
}
let _seqCache = null;
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 matched = resolved.filter(c => c.asset_id).length;
$('conform-clip-info').textContent = matched + ' of ' + _seqCache.clips.length + ' clip(s) matched';
$('conform-start-btn').disabled = matched === 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; }
});
$('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');
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'); }
});
}
function init() {
wireConnectPane(); wireLibraryPane();
wireExportPanel(); wireConformPanel(); wireRelinkPanel();
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();
})();