diff --git a/services/premiere-plugin-uxp/manifest.json b/services/premiere-plugin-uxp/manifest.json index db6c4f0..245fc6f 100644 --- a/services/premiere-plugin-uxp/manifest.json +++ b/services/premiere-plugin-uxp/manifest.json @@ -2,7 +2,7 @@ "manifestVersion": 5, "id": "net.wilddragon.dragonflight.uxp", "name": "Dragonflight MAM", - "version": "2.2.0", + "version": "2.2.1", "main": "index.html", "host": { "app": "premierepro", diff --git a/services/premiere-plugin-uxp/src/main.js b/services/premiere-plugin-uxp/src/main.js index cf9e2a0..c3a0ad9 100644 --- a/services/premiere-plugin-uxp/src/main.js +++ b/services/premiere-plugin-uxp/src/main.js @@ -181,7 +181,10 @@ finally { Library._syncActions(); } }); - $('export-timeline-btn').addEventListener('click', openExportPanel); + // 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); @@ -202,6 +205,93 @@ 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(); }