From 16366267c4ef2f100d25fda45d4d52ff032f1e37 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 01:01:29 -0400 Subject: [PATCH] =?UTF-8?q?UXP=20v2.1.0:=20main.js=20=E2=80=94=20full=20re?= =?UTF-8?q?write,=20wire=20all=20panels,=20tabs,=20export,=20conform,=20re?= =?UTF-8?q?link,=20mount=20live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/premiere-plugin-uxp/src/main.js | 409 ++++++++++++++++++++--- 1 file changed, 364 insertions(+), 45 deletions(-) diff --git a/services/premiere-plugin-uxp/src/main.js b/services/premiere-plugin-uxp/src/main.js index 45b0717..df5ed89 100644 --- a/services/premiere-plugin-uxp/src/main.js +++ b/services/premiere-plugin-uxp/src/main.js @@ -1,27 +1,29 @@ -// Panel bootstrap. Wires DOM events to API / Library / Import handlers and -// restores the connection from localStorage on mount. +// Dragonflight UXP Panel — main.js v2.1.0 +// Bootstrap + event wiring for all UI panels. (function () { - // Avoid running twice if UXP reloads the panel. if (window.__df_uxp_started) return; window.__df_uxp_started = true; + // ── Helpers ────────────────────────────────────────────────────── + const $ = id => document.getElementById(id); function syncConnectBtn() { - const u = UI.$('#server-url').value.trim(); - const t = UI.$('#api-token').value.trim(); - UI.$('#connect-btn').disabled = !u || !t; + $('connect-btn').disabled = !$('server-url').value.trim() || !$('api-token').value.trim(); } + // ── Connect ────────────────────────────────────────────────────── async function tryConnect(serverUrl, apiToken) { UI.setStatus('#connect-status', 'Connecting…', 'muted'); try { const me = await API.connect(serverUrl, apiToken); - // /auth/me returns the user fields directly (no `user:` wrapper). const who = (me && (me.display_name || me.username)) || serverUrl; - UI.$('#connected-host').textContent = who + (who === serverUrl ? '' : ' @ ' + 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'); @@ -29,65 +31,384 @@ } } + // ── Connect pane wiring ────────────────────────────────────────── function wireConnectPane() { - UI.$('#server-url').value = API.state.serverUrl; - UI.$('#api-token').value = API.state.apiToken; + $('server-url').value = API.state.serverUrl; + $('api-token').value = API.state.apiToken; syncConnectBtn(); - ['input', 'change'].forEach(ev => { - UI.$('#server-url').addEventListener(ev, syncConnectBtn); - UI.$('#api-token').addEventListener(ev, syncConnectBtn); + ['input','change'].forEach(ev => { + $('server-url').addEventListener(ev, syncConnectBtn); + $('api-token').addEventListener(ev, syncConnectBtn); }); - UI.$('#connect-btn').addEventListener('click', async () => { - const u = UI.$('#server-url').value.trim(); - const t = UI.$('#api-token').value.trim(); - await tryConnect(u, t); + $('connect-btn').addEventListener('click', async () => { + await tryConnect($('server-url').value.trim(), $('api-token').value.trim()); }); } + // ── Library pane wiring ────────────────────────────────────────── function wireLibraryPane() { - UI.$('#disconnect-btn').addEventListener('click', () => { + // Disconnect + $('disconnect-btn').addEventListener('click', () => { API.disconnect(); - UI.$('#server-url').value = ''; - UI.$('#api-token').value = ''; + Library.stopGrowingPoll(); + $('server-url').value = ''; + $('api-token').value = ''; syncConnectBtn(); UI.showPane('connect'); UI.setStatus('#connect-status', 'Disconnected.', 'muted'); }); - let searchTimer = null; - UI.$('#search-input').addEventListener('input', (e) => { + // Tabs + $('tab-library').addEventListener('click', () => Library.switchTab('library')); + $('tab-growing').addEventListener('click', () => Library.switchTab('growing')); + + // Search (debounced) + let searchTimer; + $('search-input').addEventListener('input', e => { clearTimeout(searchTimer); const q = e.target.value; - searchTimer = setTimeout(() => Library.refresh(q), 250); - }); - UI.$('#refresh-btn').addEventListener('click', () => Library.refresh(UI.$('#search-input').value)); - - UI.$('#import-proxy-btn').addEventListener('click', async () => { - const a = Library.selectedAsset(); - if (!a) return; - UI.$('#import-proxy-btn').disabled = true; - UI.$('#import-hires-btn').disabled = true; - try { await Import.proxy(a); } - catch (e) { UI.hideProgress(); UI.toast('Proxy import failed: ' + e.message, 'error'); } - finally { Library.syncActions(); } + searchTimer = setTimeout(() => { + if (Library.state.currentTab === 'library') Library.refresh(q); + else { Library.state.searchQuery = q; Library._pollGrowing(); } + }, 280); }); - UI.$('#import-hires-btn').addEventListener('click', async () => { + // Project filter + $('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 + $('refresh-btn').addEventListener('click', () => + Library.refresh($('search-input').value) + ); + + // Import proxy + $('import-proxy-btn').addEventListener('click', async () => { const a = Library.selectedAsset(); if (!a) return; - UI.$('#import-proxy-btn').disabled = true; - UI.$('#import-hires-btn').disabled = true; + _disableImportBtns(true); + try { + await Import.proxy(a); + Library.recordImport(a._localPath, { assetId: a.id, displayName: a.display_name || a.filename }); + Library.recordImport('name:' + a._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 hi-res + $('import-hires-btn').addEventListener('click', async () => { + const a = Library.selectedAsset(); + if (!a) return; + _disableImportBtns(true); try { await Import.hires(a); } catch (e) { UI.hideProgress(); UI.toast('Hi-res import failed: ' + e.message, 'error'); } - finally { Library.syncActions(); } + finally { _disableImportBtns(false); Library._syncActions(); } + }); + + // Import all + $('import-all-btn').addEventListener('click', async () => { + const assets = Library.state.assets; + if (!assets.length) { UI.toast('No assets to import', 'error'); return; } + _disableImportBtns(true); + let ok = 0, fail = 0; + for (const a of assets) { + try { await Import.proxy(a); ok++; } + catch (_) { fail++; } + } + _disableImportBtns(false); + UI.hideProgress(); + UI.toast('Import all: ' + ok + ' ok' + (fail ? ', ' + fail + ' failed' : ''), fail ? 'error' : 'ok'); + Library._syncActions(); + }); + + // Mount live + $('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); + // UXP runs on Windows PPro → use win_path + 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, 'ok'); + } catch (e) { + UI.hideProgress(); + UI.toast('Mount live failed: ' + e.message, 'error'); + } finally { + Library._syncActions(); + } + }); + + // Relink single asset + $('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 for this asset', '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); + const r = await API.requestFollow(info.url, {}); + if (!r.ok) throw new Error('Download HTTP ' + r.status); + await Import._streamToFile(r, dest, ({ received, total }) => { + const pct = total ? 20 + (received / total) * 60 : 20; + UI.showProgress('Downloading ' + UI.formatBytes(received) + '…', pct); + }); + UI.showProgress('Relinking…', 85); + const P = require('premierepro'); + const project = await P.Project.getActiveProject(); + if (!project) throw new Error('No active project'); + await Timeline._relinkInProject(project, entry.livePath, dest); + Library.recordImport(dest, { assetId: a.id, displayName: a.display_name || a.filename }); + UI.hideProgress(); + UI.toast('Relinked to hi-res: ' + safeName, 'ok'); + } catch (e) { + UI.hideProgress(); + UI.toast('Relink failed: ' + e.message, 'error'); + } finally { + Library._syncActions(); + } + }); + + // Export timeline + $('export-timeline-btn').addEventListener('click', openExportPanel); + + // Advanced + $('export-conform-btn').addEventListener('click', openConformPanel); + $('fetch-relink-btn').addEventListener('click', openRelinkPanel); + } + + function _disableImportBtns(dis) { + $('import-proxy-btn').disabled = dis; + $('import-hires-btn').disabled = dis; + } + + // ── Export timeline panel ───────────────────────────────────────── + let _timelineCache = null; + + async function openExportPanel() { + UI.showProgress('Reading Premiere sequence…', 20); + try { + _timelineCache = await Timeline.readActiveSequence(); + } catch (e) { + UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; + } + if (!_timelineCache.clips.length) { + UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; + } + UI.hideProgress(); + + const resolved = Library.resolveClipsToAssets(_timelineCache.clips); + const matched = resolved.filter(c => c.asset_id).length; + $('export-seq-name').value = _timelineCache.sequenceName || 'Sequence 1'; + $('export-clip-info').textContent = matched + ' of ' + _timelineCache.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-confirm-btn').addEventListener('click', async () => { + 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, _timelineCache); + UI.hideProgress(); + UI.toast('Pushed ' + matched + ' clip(s)' + (skipped ? ' (' + skipped + ' skipped)' : ''), 'ok'); + } catch (e) { + UI.hideProgress(); + UI.toast('Export failed: ' + e.message, 'error'); + } + }); + $('export-overlay').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel')); + } + + // ── Conform panel ───────────────────────────────────────────────── + async function openConformPanel() { + UI.showProgress('Reading Premiere sequence…', 20); + try { + _timelineCache = await Timeline.readActiveSequence(); + } catch (e) { + UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; + } + if (!_timelineCache.clips.length) { + UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; + } + UI.hideProgress(); + const resolved = Library.resolveClipsToAssets(_timelineCache.clips); + const matched = resolved.filter(c => c.asset_id).length; + $('conform-clip-info').textContent = matched + ' of ' + _timelineCache.clips.length + ' clip(s) matched'; + $('conform-start-btn').disabled = matched === 0; + 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 + $('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 p = card.dataset.preset; + 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' }, + }; + if (presets[p]) { + $('conform-codec').value = presets[p].codec; + $('conform-quality').value = presets[p].quality; + $('conform-resolution').value = presets[p].resolution; + $('conform-audio').value = presets[p].audio; + } + }); + + $('conform-start-btn').addEventListener('click', async () => { + // Need a target project — use first available or prompt user + const projectId = $('export-proj-select').value; + if (!projectId) { + UI.toast('Select a project via Export Timeline first', 'error'); return; + } + UI.closeSlide('conform-overlay', 'conform-panel'); + UI.showProgress('Starting conform job…', 20); + try { + const jobId = await Timeline.startConform(projectId, _timelineCache.sequenceName, _timelineCache, { + 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 + '%)…', 20 + pct * 0.78), + 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'); + } }); } + // ── Batch Relink panel ──────────────────────────────────────────── + let _relinkClips = []; + + async function openRelinkPanel() { + UI.showProgress('Reading Premiere sequence…', 20); + try { + _timelineCache = await Timeline.readActiveSequence(); + } catch (e) { + UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; + } + UI.hideProgress(); + + const resolved = Library.resolveClipsToAssets(_timelineCache.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() { + const anyChecked = !!document.querySelector('#clip-list input[type="checkbox"]:checked'); + $('relink-start-btn').disabled = !anyChecked; + } + + 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 checkboxes = document.querySelectorAll('#clip-list input[type="checkbox"]:checked'); + const indices = []; + checkboxes.forEach(cb => indices.push(parseInt(cb.id.replace('rclip-', ''), 10))); + const selected = indices.map(i => _relinkClips[i]).filter(c => c && c.asset_id); + if (!selected.length) return; + + UI.closeSlide('relink-overlay', 'relink-panel'); + UI.showProgress('Starting batch relink…', 5); + try { + const results = await Timeline.batchRelink(selected); + UI.hideProgress(); + const msg = results.succeeded + ' clip(s) relinked' + + (results.failed ? ', ' + results.failed + ' failed' : ''); + UI.toast(msg, results.failed ? 'error' : 'ok'); + + // Show summary in panel + $('relink-summary').textContent = msg; + $('relink-summary').classList.remove('hidden'); + } catch (e) { + UI.hideProgress(); + UI.toast('Batch relink failed: ' + e.message, 'error'); + } + }); + } + + // ── Init ────────────────────────────────────────────────────────── function init() { wireConnectPane(); wireLibraryPane(); - // If we have stored creds, try to reconnect silently. On failure fall - // back to the connect pane so the user can retype. + wireExportPanel(); + wireConformPanel(); + wireRelinkPanel(); + if (API.state.serverUrl && API.state.apiToken) { tryConnect(API.state.serverUrl, API.state.apiToken); } else { @@ -95,9 +416,7 @@ } } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); + else init(); + })();