// 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(); } }); // v2.2.1: Export Timeline is now a single-click pipeline — // push to MAM → start conform → poll → new asset lands in Library. // The Conform slide panel is still wired for Advanced → Export & Conform. $('export-timeline-btn').addEventListener('click', oneClickExport); $('export-conform-btn').addEventListener('click', openConformPanel); $('fetch-relink-btn').addEventListener('click', openRelinkPanel); // 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; }); } 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 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 = ''; 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 = '
No clips in active sequence
'; } 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 _ 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() { 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(); })();