feat(uxp): ship the icon-rail panel redesign as v2.2.2 (recover from redesign branch)
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>
This commit is contained in:
parent
8f26f1bd9a
commit
39ef551489
10 changed files with 1156 additions and 624 deletions
BIN
services/premiere-plugin-uxp/dragonflight-mam-2.2.2.ccx
Normal file
BIN
services/premiere-plugin-uxp/dragonflight-mam-2.2.2.ccx
Normal file
Binary file not shown.
|
|
@ -9,12 +9,10 @@
|
|||
<div id="root">
|
||||
|
||||
<!-- ── Connect Pane ─────────────────────────────────────────────── -->
|
||||
<section id="connect-pane" class="pane">
|
||||
<section id="connect-pane" class="pane pane-connect">
|
||||
<div class="brand">
|
||||
<div class="brand-icon">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2"/>
|
||||
</svg>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24"><path fill="currentColor" d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
</div>
|
||||
<div class="brand-title">Dragonflight</div>
|
||||
<div class="brand-tag">Wild Dragon Broadcast</div>
|
||||
|
|
@ -28,113 +26,151 @@
|
|||
<div id="connect-status" class="status-msg muted"></div>
|
||||
</section>
|
||||
|
||||
<!-- ── Library Pane ─────────────────────────────────────────────── -->
|
||||
<!-- ── Library Pane (app-shell: statusbar · rail · main · dock) ──── -->
|
||||
<section id="library-pane" class="pane hidden">
|
||||
<div class="app">
|
||||
|
||||
<!-- Connection status strip (v2.1.8): dot + identity + ⋯ menu.
|
||||
Disconnect lives inside the menu so it's not always visible. -->
|
||||
<header class="status-strip">
|
||||
<span class="signal-dot"></span>
|
||||
<span id="connected-host" class="connected-host"></span>
|
||||
<span id="panel-version" class="panel-version" title="Plugin version"></span>
|
||||
<button id="menu-btn" class="btn-ghost" title="More" aria-label="More">⋯</button>
|
||||
<div id="status-menu" class="menu hidden" role="menu">
|
||||
<button id="disconnect-btn" class="menu-item" role="menuitem">Disconnect</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tab-nav">
|
||||
<button id="tab-library" class="tab-btn active">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
</svg>
|
||||
Library
|
||||
</button>
|
||||
<button id="tab-growing" class="tab-btn">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>
|
||||
Growing
|
||||
<span id="growing-count" class="badge" style="display:none">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search + Project filter -->
|
||||
<div class="search-row">
|
||||
<input id="search-input" type="search" placeholder="Search assets…" />
|
||||
<select id="project-filter" title="Filter by project">
|
||||
<option value="all">All Projects</option>
|
||||
</select>
|
||||
<button id="refresh-btn" class="btn btn-icon" title="Refresh">↻</button>
|
||||
</div>
|
||||
|
||||
<!-- Active sequence info bar -->
|
||||
<div id="seq-info-bar" class="seq-info-bar hidden">
|
||||
<span class="chip chip-ok">SEQ</span>
|
||||
<span id="seq-info-name" class="seq-info-name"></span>
|
||||
</div>
|
||||
|
||||
<!-- Asset grid (library tab) -->
|
||||
<div id="library-container" class="grid-container">
|
||||
<div id="asset-grid" class="asset-grid">
|
||||
<div class="empty muted">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Growing grid -->
|
||||
<div id="growing-container" class="grid-container hidden">
|
||||
<div id="growing-grid" class="asset-grid">
|
||||
<div class="empty muted">No growing files</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- (v2.2.0) Asset details panel dropped — card meta already carries
|
||||
name / codec / duration. If we need richer detail later, surface
|
||||
it on the card hover state rather than reserving permanent space. -->
|
||||
<div id="details-panel" class="hidden"></div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
<footer class="actions">
|
||||
<div class="action-row">
|
||||
<button id="import-proxy-btn" class="btn btn-primary" disabled>Import Proxy</button>
|
||||
<button id="import-hires-btn" class="btn btn-secondary" disabled>Hi-Res</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="mount-live-btn" class="btn btn-secondary" disabled title="Open live file from SMB share">Mount Live</button>
|
||||
<button id="relink-btn" class="btn btn-secondary" disabled title="Relink proxy → hi-res original">Relink Hi-Res</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="import-all-btn" class="btn btn-secondary" disabled>Import All</button>
|
||||
<button id="export-timeline-btn" class="btn btn-secondary" disabled>Export Timeline ↑</button>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div id="progress-row" class="progress-row hidden">
|
||||
<div class="progress-bar"><div id="progress-fill"></div></div>
|
||||
<div id="progress-label" class="progress-label">…</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
</footer>
|
||||
|
||||
<!-- Advanced section — collapsed by default; click the row to expand. -->
|
||||
<div class="advanced-section">
|
||||
<button id="advanced-toggle" class="advanced-toggle" type="button" aria-expanded="false">
|
||||
<span class="advanced-caret">▸</span>
|
||||
<span class="advanced-title">Advanced</span>
|
||||
</button>
|
||||
<div id="advanced-body" class="advanced-body hidden">
|
||||
<div class="action-row">
|
||||
<button id="export-conform-btn" class="btn btn-secondary" disabled>Export & Conform</button>
|
||||
<button id="fetch-relink-btn" class="btn btn-secondary" disabled>Fetch & Relink All</button>
|
||||
<!-- Connection collapsed to a status line: dot + host + version + ⋯ -->
|
||||
<header class="statusbar">
|
||||
<span class="signal-dot"></span>
|
||||
<span id="connected-host" class="connected-host"></span>
|
||||
<span id="panel-version" class="panel-version" title="Plugin version"></span>
|
||||
<div id="menu-btn" role="button" tabindex="0" class="iconbtn iconbtn--sm" data-tip="More" data-tip-pos="down-left" aria-label="More">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24"><path fill="currentColor" d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="status-menu" class="menu hidden" role="menu">
|
||||
<button id="disconnect-btn" class="menu-item" role="menuitem">Disconnect</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace">
|
||||
|
||||
<!-- Vertical icon rail: views on top, global actions below -->
|
||||
<nav class="rail">
|
||||
<div id="tab-library" role="button" tabindex="0" class="rail-btn active" data-tip="Library" data-tip-pos="right">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24"><rect x="3" y="3" width="7.5" height="7.5" rx="1.5" fill="currentColor"/><rect x="13.5" y="3" width="7.5" height="7.5" rx="1.5" fill="currentColor"/><rect x="3" y="13.5" width="7.5" height="7.5" rx="1.5" fill="currentColor"/><rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1.5" fill="currentColor"/></svg>
|
||||
</div>
|
||||
<div id="tab-growing" role="button" tabindex="0" class="rail-btn" data-tip="Growing" data-tip-pos="right">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>
|
||||
<span id="growing-count" class="rail-count" style="display:none">0</span>
|
||||
</div>
|
||||
|
||||
<span class="rail-spacer"></span>
|
||||
|
||||
<div id="export-timeline-btn" role="button" tabindex="0" class="rail-btn rail-btn--accent" data-tip="Export" data-tip-pos="right">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>
|
||||
</div>
|
||||
<div id="refresh-btn" role="button" tabindex="0" class="rail-btn" data-tip="Refresh" data-tip-pos="right">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M17.65 6.35A7.96 7.96 0 0 0 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0 1 12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main column -->
|
||||
<div class="main">
|
||||
|
||||
<!-- Search + project filter -->
|
||||
<div class="toolbar">
|
||||
<label class="search">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.5 6.5 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<input id="search-input" type="search" placeholder="Search assets" />
|
||||
</label>
|
||||
<select id="project-filter" class="filter-select" title="Filter by project">
|
||||
<option value="all">All Projects</option>
|
||||
</select>
|
||||
<div id="view-toggle-btn" role="button" tabindex="0" class="iconbtn iconbtn--sm" data-tip="Grid view" data-tip-pos="down-left" aria-label="Toggle layout">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active sequence info bar -->
|
||||
<div id="seq-info-bar" class="seq-info-bar hidden">
|
||||
<span class="chip chip-ok">SEQ</span>
|
||||
<span id="seq-info-name" class="seq-info-name"></span>
|
||||
</div>
|
||||
|
||||
<!-- Asset grid (library tab) -->
|
||||
<div id="library-container" class="grid-container">
|
||||
<div id="asset-grid" class="asset-grid">
|
||||
<div class="empty muted">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Growing grid -->
|
||||
<div id="growing-container" class="grid-container hidden">
|
||||
<div id="growing-grid" class="asset-grid">
|
||||
<div class="empty muted">No growing files</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details panel retired (v2.2.0) — card meta carries name/codec/duration. -->
|
||||
<div id="details-panel" class="hidden"></div>
|
||||
|
||||
<!-- Progress + toast sit just above the action dock -->
|
||||
<div id="progress-row" class="progress-row hidden">
|
||||
<div class="progress-bar"><div id="progress-fill"></div></div>
|
||||
<div id="progress-label" class="progress-label">…</div>
|
||||
</div>
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<!-- Contextual action dock: text buttons replaced by icon buttons
|
||||
with hover labels. Per-asset actions left, batch actions right. -->
|
||||
<footer class="dock">
|
||||
<div id="import-proxy-btn" role="button" tabindex="0" class="iconbtn iconbtn--primary" data-tip="Import Proxy" data-tip-pos="up" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
||||
</div>
|
||||
<div id="import-hires-btn" role="button" tabindex="0" class="iconbtn" data-tip="Import Hi-Res" data-tip-pos="up" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M11.99 18.54l-7.37-5.73L3 14.07l9 7 9-7-1.63-1.27-7.38 5.74zM12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27L12 16z"/></svg>
|
||||
</div>
|
||||
<div id="mount-live-btn" role="button" tabindex="0" class="iconbtn" data-tip="Mount Live" data-tip-pos="up" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M14 12c0 1.11-.89 2-2 2s-2-.89-2-2 .89-2 2-2 2 .89 2 2zm-2-6c-3.31 0-6 2.69-6 6 0 2.22 1.21 4.15 3 5.19l1-1.74A3.98 3.98 0 0 1 8 12c0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.48-.81 2.75-2 3.45l1 1.74c1.79-1.04 3-2.97 3-5.19 0-3.31-2.69-6-6-6zm0-4C7.58 2 4 5.58 4 10c0 2.96 1.61 5.53 4 6.92l1-1.73C7.21 14.07 6 12.18 6 10c0-3.31 2.69-6 6-6s6 2.69 6 6c0 2.18-1.21 4.07-3 5.19l1 1.73c2.39-1.39 4-3.96 4-6.92 0-4.42-3.58-8-8-8z"/></svg>
|
||||
</div>
|
||||
<div id="relink-btn" role="button" tabindex="0" class="iconbtn" data-tip="Relink Hi-Res" data-tip-pos="up" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
|
||||
</div>
|
||||
|
||||
<span class="dock-sep"></span>
|
||||
|
||||
<div id="upload-mam-btn" role="button" tabindex="0" class="iconbtn" data-tip="Upload to MAM" data-tip-pos="up-left">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/></svg>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div><!-- /main -->
|
||||
</div><!-- /workspace -->
|
||||
</div><!-- /app -->
|
||||
</section>
|
||||
|
||||
<!-- ── Export Timeline Slide Panel ──────────────────────────────── -->
|
||||
<!-- Full-panel Export screen -->
|
||||
<div id="export-screen" class="screen hidden">
|
||||
<div class="screen-header">
|
||||
<span class="screen-title">Export</span>
|
||||
<button id="export-screen-close" class="btn btn-icon">✕</button>
|
||||
</div>
|
||||
<div class="screen-body">
|
||||
<div id="opt-conform" role="button" tabindex="0" class="export-option">
|
||||
<div class="eo-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24"><path fill="currentColor" d="M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z"/></svg>
|
||||
</div>
|
||||
<div class="eo-text">
|
||||
<div class="eo-title">Conform Timeline → MAM</div>
|
||||
<div class="eo-desc">Render the sequence to a chosen codec and push it to the MAM</div>
|
||||
</div>
|
||||
<span class="eo-arrow">›</span>
|
||||
</div>
|
||||
<div id="opt-local-export" role="button" tabindex="0" class="export-option">
|
||||
<div class="eo-icon">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24"><path fill="currentColor" d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
||||
</div>
|
||||
<div class="eo-text">
|
||||
<div class="eo-title">Local Export</div>
|
||||
<div class="eo-desc">Trim the hi-res sources on the MAM, download and relink them in Premiere</div>
|
||||
</div>
|
||||
<span class="eo-arrow">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── (retired) Push Timeline Slide Panel — kept hidden/unused ──── -->
|
||||
<div id="export-overlay" class="slide-overlay hidden"></div>
|
||||
<div id="export-panel" class="slide-panel hidden">
|
||||
<div class="slide-header">
|
||||
|
|
@ -164,40 +200,43 @@
|
|||
<div class="slide-body">
|
||||
<label class="field-label">Target project</label>
|
||||
<select id="conform-proj-select"><option value="">— Select project —</option></select>
|
||||
<label class="field-label">Preset</label>
|
||||
<div id="preset-cards" class="preset-grid">
|
||||
<div class="preset-card selected" data-preset="broadcast"><div class="pc-title">Broadcast</div><div class="pc-desc">ProRes 422 HQ · 1080p · 48kHz</div></div>
|
||||
<div class="preset-card" data-preset="web"><div class="pc-title">Web</div><div class="pc-desc">H.264 · 1080p · AAC 320k</div></div>
|
||||
<div class="preset-card" data-preset="archive"><div class="pc-title">Archive</div><div class="pc-desc">ProRes 4444 · UHD · 48kHz</div></div>
|
||||
<div class="preset-card" data-preset="custom"><div class="pc-title">Custom</div><div class="pc-desc">Manual settings</div></div>
|
||||
<label class="field-label">Format</label>
|
||||
<div id="preset-cards" class="preset-list">
|
||||
<div class="preset-card selected" data-preset="broadcast"><span class="pc-title">Broadcast</span><span class="pc-desc">ProRes 422 HQ · 1080p</span></div>
|
||||
<div class="preset-card" data-preset="web"><span class="pc-title">Web</span><span class="pc-desc">H.264 · 1080p · AAC</span></div>
|
||||
<div class="preset-card" data-preset="archive"><span class="pc-title">Archive</span><span class="pc-desc">ProRes 4444 · UHD</span></div>
|
||||
<div class="preset-card" data-preset="custom"><span class="pc-title">Custom</span><span class="pc-desc">Manual settings</span></div>
|
||||
</div>
|
||||
|
||||
<div id="conform-custom" class="custom-fields hidden">
|
||||
<label class="field-label">Codec</label>
|
||||
<select id="conform-codec">
|
||||
<option value="prores_hq">ProRes 422 HQ</option>
|
||||
<option value="prores_4444">ProRes 4444</option>
|
||||
<option value="h264">H.264</option>
|
||||
<option value="h265">H.265 / HEVC</option>
|
||||
<option value="dnxhr_hq">DNxHR HQ</option>
|
||||
</select>
|
||||
<label class="field-label">Quality</label>
|
||||
<select id="conform-quality">
|
||||
<option value="high">High</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<label class="field-label">Resolution</label>
|
||||
<select id="conform-resolution">
|
||||
<option value="uhd">UHD (3840×2160)</option>
|
||||
<option value="1080p" selected>Full HD (1920×1080)</option>
|
||||
<option value="720p">HD (1280×720)</option>
|
||||
<option value="source">Source</option>
|
||||
</select>
|
||||
<label class="field-label">Audio</label>
|
||||
<select id="conform-audio">
|
||||
<option value="broadcast">Broadcast (48kHz PCM)</option>
|
||||
<option value="web">Web (AAC 320k)</option>
|
||||
<option value="archive">Archive (96kHz PCM)</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="field-label">Codec</label>
|
||||
<select id="conform-codec">
|
||||
<option value="prores_hq">ProRes 422 HQ</option>
|
||||
<option value="prores_4444">ProRes 4444</option>
|
||||
<option value="h264">H.264</option>
|
||||
<option value="h265">H.265 / HEVC</option>
|
||||
<option value="dnxhr_hq">DNxHR HQ</option>
|
||||
</select>
|
||||
<label class="field-label">Quality</label>
|
||||
<select id="conform-quality">
|
||||
<option value="high">High</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
<label class="field-label">Resolution</label>
|
||||
<select id="conform-resolution">
|
||||
<option value="uhd">UHD (3840×2160)</option>
|
||||
<option value="1080p" selected>Full HD (1920×1080)</option>
|
||||
<option value="720p">HD (1280×720)</option>
|
||||
<option value="source">Source</option>
|
||||
</select>
|
||||
<label class="field-label">Audio</label>
|
||||
<select id="conform-audio">
|
||||
<option value="broadcast">Broadcast (48kHz PCM)</option>
|
||||
<option value="web">Web (AAC 320k)</option>
|
||||
<option value="archive">Archive (96kHz PCM)</option>
|
||||
</select>
|
||||
<div id="conform-clip-info" class="clip-info"></div>
|
||||
</div>
|
||||
<div class="slide-footer">
|
||||
|
|
@ -231,6 +270,7 @@
|
|||
<script src="src/library.js"></script>
|
||||
<script src="src/import-flow.js"></script>
|
||||
<script src="src/timeline.js"></script>
|
||||
<script src="src/tooltip.js"></script>
|
||||
<script src="src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -167,5 +167,57 @@
|
|||
});
|
||||
};
|
||||
|
||||
// Local Export trim job polling + segment retrieval.
|
||||
API.getTrimStatus = function (jobId) {
|
||||
return API.json('/api/v1/assets/trim-status/' + jobId);
|
||||
};
|
||||
API.getTempSegmentUrl = function (clipInstanceId) {
|
||||
return API.json('/api/v1/assets/temp-segment-url/' + clipInstanceId);
|
||||
};
|
||||
|
||||
// ── Upload (ingest editor media into the MAM) ────────────────────
|
||||
// Single-shot multipart form upload (server caps simple at <50 MB).
|
||||
API.uploadSimple = async function (blob, meta) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', blob, meta.filename);
|
||||
fd.append('filename', meta.filename);
|
||||
fd.append('projectId', meta.projectId);
|
||||
if (meta.binId) fd.append('binId', meta.binId);
|
||||
if (meta.contentType) fd.append('contentType', meta.contentType);
|
||||
const r = await API.request('/api/v1/upload/simple', { method: 'POST', body: fd });
|
||||
if (!r.ok) throw new Error('Upload HTTP ' + r.status + ' — ' + (await r.text().catch(() => '')).slice(0, 160));
|
||||
return r.json();
|
||||
};
|
||||
|
||||
// Chunked multipart for large originals.
|
||||
API.uploadInit = function (meta) {
|
||||
return API.json('/api/v1/upload/init', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(meta),
|
||||
});
|
||||
};
|
||||
API.uploadPart = async function (blob, meta) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', blob, 'part-' + meta.partNumber);
|
||||
fd.append('uploadId', meta.uploadId);
|
||||
fd.append('key', meta.key);
|
||||
fd.append('partNumber', String(meta.partNumber));
|
||||
const r = await API.request('/api/v1/upload/part', { method: 'POST', body: fd });
|
||||
if (!r.ok) throw new Error('Upload part HTTP ' + r.status);
|
||||
return r.json();
|
||||
};
|
||||
API.uploadComplete = function (meta) {
|
||||
return API.json('/api/v1/upload/complete', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(meta),
|
||||
});
|
||||
};
|
||||
API.uploadAbort = function (meta) {
|
||||
return API.json('/api/v1/upload/abort', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(meta),
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
window.API = API;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -154,5 +154,111 @@
|
|||
return destPath;
|
||||
};
|
||||
|
||||
// ── Upload (ingest editor media into the MAM) ────────────────────
|
||||
const SIMPLE_MAX = 45 * 1024 * 1024; // server caps /simple at <50 MB
|
||||
const PART_SIZE = 16 * 1024 * 1024; // chunk size for multipart
|
||||
|
||||
function _contentType(name) {
|
||||
const ext = String(name).split('.').pop().toLowerCase();
|
||||
const map = {
|
||||
mp4:'video/mp4', m4v:'video/mp4', mov:'video/quicktime', mxf:'application/mxf',
|
||||
mkv:'video/x-matroska', avi:'video/x-msvideo', mpg:'video/mpeg', mpeg:'video/mpeg',
|
||||
mts:'video/mp2t', m2ts:'video/mp2t', wav:'audio/wav', aif:'audio/aiff', aiff:'audio/aiff',
|
||||
mp3:'audio/mpeg', png:'image/png', jpg:'image/jpeg', jpeg:'image/jpeg',
|
||||
};
|
||||
return map[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
// Read a local file and push it to the MAM. Returns the created asset row.
|
||||
// NOTE: reads the whole file into memory once (fine for typical clips;
|
||||
// very large multi-GB originals may strain memory — revisit with a
|
||||
// positional-read stream if that becomes a problem).
|
||||
Import.uploadFile = async function (nativePath, meta) {
|
||||
meta = meta || {};
|
||||
if (!meta.projectId) throw new Error('No target project for upload');
|
||||
const filename = meta.filename || path.basename(nativePath);
|
||||
const contentType = _contentType(filename);
|
||||
|
||||
const buf = await fs.readFile(nativePath);
|
||||
const size = buf.byteLength != null ? buf.byteLength : buf.length;
|
||||
|
||||
if (size <= SIMPLE_MAX) {
|
||||
const blob = new Blob([buf], { type: contentType });
|
||||
return API.uploadSimple(blob, { filename, projectId: meta.projectId, binId: meta.binId, contentType });
|
||||
}
|
||||
|
||||
// Chunked multipart for large files.
|
||||
const init = await API.uploadInit({ filename, fileSize: size, contentType, projectId: meta.projectId, binId: meta.binId });
|
||||
const parts = [];
|
||||
try {
|
||||
let partNumber = 1;
|
||||
for (let off = 0; off < size; off += PART_SIZE, partNumber++) {
|
||||
const chunk = buf.slice(off, Math.min(off + PART_SIZE, size));
|
||||
const blob = new Blob([chunk], { type: contentType });
|
||||
if (meta.onProgress) meta.onProgress(off, size);
|
||||
const res = await API.uploadPart(blob, { uploadId: init.uploadId, key: init.key, partNumber });
|
||||
parts.push({ PartNumber: partNumber, ETag: res.etag });
|
||||
}
|
||||
return API.uploadComplete({ uploadId: init.uploadId, key: init.key, assetId: init.assetId, parts });
|
||||
} catch (e) {
|
||||
await API.uploadAbort({ uploadId: init.uploadId, key: init.key, assetId: init.assetId });
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Bin selection (best-effort) + file-picker fallback ───────────
|
||||
// Tries to read the highlighted project-panel item(s). The UXP premierepro
|
||||
// selection surface varies by version, so every access is guarded; on any
|
||||
// miss this returns [] and callers fall back to a native file picker.
|
||||
Import.getSelectedBinPaths = async function () {
|
||||
const paths = [];
|
||||
try {
|
||||
const P = _ppro();
|
||||
const project = await P.Project.getActiveProject();
|
||||
if (!project) return paths;
|
||||
let sel = null;
|
||||
try { if (project.getSelection) sel = await project.getSelection(); } catch (_) {}
|
||||
let items = [];
|
||||
if (sel) {
|
||||
if (typeof sel.getItems === 'function') items = await sel.getItems();
|
||||
else if (Array.isArray(sel)) items = sel;
|
||||
else if (Array.isArray(sel.items)) items = sel.items;
|
||||
}
|
||||
for (const it of (items || [])) {
|
||||
try {
|
||||
const ci = await P.ClipProjectItem.cast(it);
|
||||
const mp = await ci.getMediaFilePath();
|
||||
if (mp) paths.push(mp);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
return paths;
|
||||
};
|
||||
|
||||
// Native file picker — returns array of native paths (may be empty).
|
||||
Import.pickFiles = async function () {
|
||||
if (!uxpFs || !uxpFs.getFileForOpening) throw new Error('File picker unavailable in this host');
|
||||
const sel = await uxpFs.getFileForOpening({ allowMultiple: true });
|
||||
if (!sel) return [];
|
||||
const arr = Array.isArray(sel) ? sel : [sel];
|
||||
return arr.map(f => f && f.nativePath).filter(Boolean);
|
||||
};
|
||||
|
||||
// Upload any timeline clips not yet in the MAM, recording the path→asset
|
||||
// mapping so resolveClipsToAssets picks them up on the next pass.
|
||||
Import.ensureClipsInMam = async function (clips, projectId, onProgress) {
|
||||
const missing = clips.filter(c => !c.asset_id && c.filePath);
|
||||
for (let i = 0; i < missing.length; i++) {
|
||||
const c = missing[i];
|
||||
if (onProgress) onProgress(c.fileName || path.basename(c.filePath), i + 1, missing.length);
|
||||
const asset = await Import.uploadFile(c.filePath, { projectId, filename: path.basename(c.filePath) });
|
||||
if (asset && asset.id) {
|
||||
Library.recordImport(c.filePath, { assetId: asset.id });
|
||||
if (c.fileName) Library.recordImport('name:' + c.fileName, { assetId: asset.id });
|
||||
}
|
||||
}
|
||||
return { uploaded: missing.length };
|
||||
};
|
||||
|
||||
window.Import = Import;
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -220,10 +220,7 @@
|
|||
_btn('import-hires-btn').disabled = !sel || live || !sel.original_s3_key;
|
||||
_btn('mount-live-btn').disabled = !sel || !live;
|
||||
_btn('relink-btn').disabled = !(ready && hasLiveImport);
|
||||
_btn('import-all-btn').disabled = Library.state.currentTab !== 'library';
|
||||
_btn('export-timeline-btn').disabled = false; // available once connected
|
||||
_btn('export-conform-btn').disabled = false;
|
||||
_btn('fetch-relink-btn').disabled = false;
|
||||
// export-timeline-btn (Export menu) and upload-mam-btn are always available.
|
||||
};
|
||||
|
||||
function _btn(id) { return document.getElementById(id) || { disabled: false }; }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,50 @@
|
|||
|
||||
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();
|
||||
}
|
||||
|
|
@ -74,6 +118,11 @@
|
|||
$('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);
|
||||
|
|
@ -114,24 +163,8 @@
|
|||
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();
|
||||
});
|
||||
// ── 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;
|
||||
|
|
@ -181,12 +214,8 @@
|
|||
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);
|
||||
// Single Export entry → popup menu (Conform Timeline / Local Export).
|
||||
wireExportMenu();
|
||||
|
||||
// Advanced collapsible toggle (v2.2.0).
|
||||
const advToggle = $('advanced-toggle');
|
||||
|
|
@ -203,6 +232,83 @@
|
|||
['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 ────────────────────────────────────
|
||||
|
|
@ -331,9 +437,13 @@
|
|||
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;
|
||||
$('conform-clip-info').textContent = matched + ' of ' + _seqCache.clips.length + ' clip(s) matched';
|
||||
$('conform-start-btn').disabled = matched === 0;
|
||||
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>';
|
||||
|
|
@ -360,6 +470,8 @@
|
|||
};
|
||||
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;
|
||||
|
|
@ -367,6 +479,15 @@
|
|||
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, {
|
||||
|
|
@ -465,6 +586,8 @@
|
|||
}
|
||||
|
||||
function init() {
|
||||
enableDivDisabled();
|
||||
applyViewMode(getViewMode());
|
||||
wireConnectPane(); wireLibraryPane();
|
||||
wireExportPanel(); wireConformPanel(); wireRelinkPanel();
|
||||
showVersion();
|
||||
|
|
|
|||
|
|
@ -304,5 +304,76 @@
|
|||
return count;
|
||||
};
|
||||
|
||||
// ── Local Export ─────────────────────────────────────────────────
|
||||
// Server trims each timeline clip's hi-res via FFMPEG, then we download
|
||||
// the trimmed segments and relink the project items to them.
|
||||
// CAVEAT: relink keys on the source media path, so a source used by
|
||||
// multiple timeline clips with different in/out points will relink to a
|
||||
// single segment (last one wins). Common single-use case is exact.
|
||||
Timeline.localExport = async function (resolvedClips, onProgress) {
|
||||
const P = ppro();
|
||||
const project = await P.Project.getActiveProject();
|
||||
if (!project) throw new Error('No active Premiere project');
|
||||
const matched = (resolvedClips || []).filter(c => c.asset_id);
|
||||
if (!matched.length) throw new Error('No clips matched MAM assets to export');
|
||||
|
||||
const payload = matched.map(c => ({
|
||||
assetId: c.asset_id,
|
||||
filename: c.fileName || (c.filePath ? path.basename(c.filePath) : 'clip'),
|
||||
sourceInFrames: c.sourceInFrames, sourceOutFrames: c.sourceOutFrames,
|
||||
timelineInFrames: c.timelineInFrames, timelineOutFrames: c.timelineOutFrames,
|
||||
trackIndex: c.trackIndex,
|
||||
}));
|
||||
|
||||
onProgress && onProgress('Requesting trim of ' + matched.length + ' clip(s)…', 10);
|
||||
const job = await API.batchTrim(payload);
|
||||
const jobId = job.jobId;
|
||||
const clipByInstance = {};
|
||||
(job.clips || []).forEach((cr, i) => { if (cr.clipInstanceId) clipByInstance[cr.clipInstanceId] = matched[i]; });
|
||||
|
||||
// Poll until every segment is ready (s3Key set) or the job fails.
|
||||
const ready = {};
|
||||
await new Promise((resolve, reject) => {
|
||||
const t = setInterval(async () => {
|
||||
try {
|
||||
const st = await API.getTrimStatus(jobId);
|
||||
const clips = st.clips || [];
|
||||
const completed = clips.filter(c => c.status === 'completed' && c.s3Key);
|
||||
onProgress && onProgress('Trimming on server… (' + completed.length + '/' + clips.length + ')',
|
||||
15 + (completed.length / Math.max(1, clips.length)) * 45);
|
||||
if (st.status === 'failed') { clearInterval(t); reject(new Error('Server trim job failed')); return; }
|
||||
if (clips.length && completed.length === clips.length) {
|
||||
clearInterval(t); completed.forEach(c => { ready[c.clipInstanceId] = c; }); resolve();
|
||||
}
|
||||
} catch (_) { /* transient — keep polling */ }
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Download each segment and relink the source media path to it.
|
||||
const results = { succeeded: 0, failed: 0, errors: [] };
|
||||
const ids = Object.keys(ready);
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const cid = ids[i];
|
||||
const clip = clipByInstance[cid];
|
||||
if (!clip) continue;
|
||||
try {
|
||||
onProgress && onProgress('Downloading segment ' + (i + 1) + '/' + ids.length + '…', 60 + (i / ids.length) * 35);
|
||||
const seg = await API.getTempSegmentUrl(cid);
|
||||
const ext = (seg.s3Key && seg.s3Key.split('.').pop()) || 'mov';
|
||||
const base = UI.sanitizeFilename((clip.fileName || 'clip') + '-trim-' + cid.slice(0, 8) + '.' + ext);
|
||||
const dest = await Import._tempPath(base);
|
||||
const r = await API.requestExternal(seg.url);
|
||||
if (!r.ok) throw new Error('Segment download HTTP ' + r.status);
|
||||
await Import._writeBuffer(dest, await r.arrayBuffer());
|
||||
if (clip.filePath) await Timeline._relinkInProject(project, clip.filePath, dest);
|
||||
results.succeeded++;
|
||||
} catch (e) {
|
||||
results.failed++;
|
||||
results.errors.push((clip && clip.fileName || 'clip') + ': ' + e.message);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
window.Timeline = Timeline;
|
||||
})();
|
||||
|
|
|
|||
78
services/premiere-plugin-uxp/src/tooltip.js
Normal file
78
services/premiere-plugin-uxp/src/tooltip.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Hover tooltips — v1
|
||||
// Icon-first UI: every actionable control carries a [data-tip] label that
|
||||
// surfaces on hover. UXP's CSS engine can't be trusted with
|
||||
// `content: attr(data-tip)` on ::after, so we position a single floating
|
||||
// bubble with plain DOM + getBoundingClientRect (both well supported).
|
||||
|
||||
(function () {
|
||||
let bubble = null;
|
||||
let timer = null;
|
||||
|
||||
function ensure() {
|
||||
if (bubble) return bubble;
|
||||
bubble = document.createElement('div');
|
||||
bubble.className = 'tip-bubble';
|
||||
document.body.appendChild(bubble);
|
||||
return bubble;
|
||||
}
|
||||
|
||||
function show(el) {
|
||||
const text = el.getAttribute('data-tip');
|
||||
if (!text) return;
|
||||
const tip = ensure();
|
||||
tip.textContent = text;
|
||||
tip.style.display = 'block';
|
||||
tip.style.opacity = '0';
|
||||
|
||||
const r = el.getBoundingClientRect();
|
||||
const t = tip.getBoundingClientRect();
|
||||
// position:absolute on a body-level node is offset from the document
|
||||
// origin; add scroll offset (0 in practice, body doesn't scroll) for safety.
|
||||
const sx = window.pageXOffset || document.documentElement.scrollLeft || 0;
|
||||
const sy = window.pageYOffset || document.documentElement.scrollTop || 0;
|
||||
const gap = 7;
|
||||
const pos = el.getAttribute('data-tip-pos') || 'down';
|
||||
let x, y;
|
||||
|
||||
if (pos === 'right') {
|
||||
x = r.right + gap; y = r.top + (r.height - t.height) / 2;
|
||||
} else if (pos === 'up') {
|
||||
x = r.left + (r.width - t.width) / 2; y = r.top - t.height - gap;
|
||||
} else if (pos === 'up-left') {
|
||||
x = r.right - t.width; y = r.top - t.height - gap;
|
||||
} else if (pos === 'down-left') {
|
||||
x = r.right - t.width; y = r.bottom + gap;
|
||||
} else {
|
||||
x = r.left + (r.width - t.width) / 2; y = r.bottom + gap;
|
||||
}
|
||||
|
||||
const vw = window.innerWidth || document.documentElement.clientWidth || 99999;
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight || 99999;
|
||||
x = Math.max(4, Math.min(x, vw - t.width - 4));
|
||||
y = Math.max(4, Math.min(y, vh - t.height - 4));
|
||||
tip.style.left = (x + sx) + 'px';
|
||||
tip.style.top = (y + sy) + 'px';
|
||||
tip.style.opacity = '1';
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clearTimeout(timer);
|
||||
if (bubble) { bubble.style.opacity = '0'; bubble.style.display = 'none'; }
|
||||
}
|
||||
|
||||
function bind(el) {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => show(el), 240);
|
||||
});
|
||||
el.addEventListener('mouseleave', hide);
|
||||
el.addEventListener('click', hide);
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.querySelectorAll('[data-tip]').forEach(bind);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})();
|
||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
Loading…
Reference in a new issue