diff --git a/services/premiere-plugin/js/main.js b/services/premiere-plugin/js/main.js index 2b57f08..e198bae 100644 --- a/services/premiere-plugin/js/main.js +++ b/services/premiere-plugin/js/main.js @@ -3,25 +3,28 @@ * Main JavaScript file for the CEP panel */ +// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls +const csInterface = new CSInterface(); + // ============================================================================ // State Management // ============================================================================ const state = { - serverUrl: localStorage.getItem("mam_server_url") || "http://localhost:7434", - isConnected: false, - isConnecting: false, - selectedAsset: null, - assets: [], - filteredAssets: [], - projects: [], - selectedProject: "all", - searchQuery: "", - currentPage: 0, - pageSize: 50, - totalAssets: 0, + serverUrl: localStorage.getItem('mam_server_url') || 'http://localhost:7434', + isConnected: false, + isConnecting: false, + selectedAsset: null, + assets: [], + projects: [], + selectedProject: 'all', + searchQuery: '', + currentPage: 0, + pageSize: 50, + totalAssets: 0, downloadProgress: 0, - isDownloading: false + isDownloading: false, + thumbCache: {}, // assetId -> signed URL }; // ============================================================================ @@ -32,70 +35,89 @@ let elements = {}; function initDOMElements() { elements = { - // Connection bar - serverUrlInput: document.getElementById("server-url"), - connectBtn: document.getElementById("connect-btn"), - statusIndicator: document.getElementById("status-indicator"), - - // Search and filter - searchInput: document.getElementById("search-input"), - projectFilter: document.getElementById("project-filter"), - - // Asset grid - assetGrid: document.getElementById("asset-grid"), - emptyState: document.getElementById("empty-state"), - - // Details panel - detailsPanel: document.getElementById("details-panel"), - detailsFilename: document.getElementById("details-filename"), - detailsCodec: document.getElementById("details-codec"), - detailsResolution: document.getElementById("details-resolution"), - detailsFps: document.getElementById("details-fps"), - detailsDuration: document.getElementById("details-duration"), - detailsSize: document.getElementById("details-size"), - detailsTags: document.getElementById("details-tags"), - - // Action bar - importBtn: document.getElementById("import-btn"), - importAllBtn: document.getElementById("import-all-btn"), - - // Progress - progressContainer: document.getElementById("progress-container"), - progressLabel: document.getElementById("progress-label"), - progressFill: document.getElementById("progress-fill") + serverUrlInput: document.getElementById('server-url'), + connectBtn: document.getElementById('connect-btn'), + statusIndicator: document.getElementById('status-indicator'), + searchInput: document.getElementById('search-input'), + projectFilter: document.getElementById('project-filter'), + assetGrid: document.getElementById('asset-grid'), + emptyState: document.getElementById('empty-state'), + detailsPanel: document.getElementById('details-panel'), + detailsFilename: document.getElementById('details-filename'), + detailsCodec: document.getElementById('details-codec'), + detailsResolution: document.getElementById('details-resolution'), + detailsFps: document.getElementById('details-fps'), + detailsDuration: document.getElementById('details-duration'), + detailsSize: document.getElementById('details-size'), + detailsTags: document.getElementById('details-tags'), + importBtn: document.getElementById('import-btn'), + importAllBtn: document.getElementById('import-all-btn'), + progressContainer: document.getElementById('progress-container'), + progressLabel: document.getElementById('progress-label'), + progressFill: document.getElementById('progress-fill'), }; } +// ============================================================================ +// Thumbnail Lazy Loading (IntersectionObserver) +// ============================================================================ + +const thumbObserver = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const img = entry.target; + const assetId = img.dataset.assetId; + if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId); + thumbObserver.unobserve(img); + } + }); +}, { rootMargin: '100px' }); + +async function loadThumbnail(img, assetId) { + if (state.thumbCache[assetId]) { + img.src = state.thumbCache[assetId]; + img.dataset.loaded = '1'; + return; + } + try { + const r = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/thumbnail`, { + headers: { Accept: 'application/json' }, + }); + if (!r.ok) return; + const { url } = await r.json(); + if (!url) return; + state.thumbCache[assetId] = url; + img.src = url; + img.dataset.loaded = '1'; + } catch (_) { + // thumbnail unavailable — leave placeholder visible + } +} + // ============================================================================ // Initialization // ============================================================================ -document.addEventListener("DOMContentLoaded", () => { +document.addEventListener('DOMContentLoaded', () => { initDOMElements(); setupEventListeners(); restoreSettings(); - logMessage("Wild Dragon MAM panel initialized"); + logMessage('Wild Dragon MAM panel initialized'); }); function setupEventListeners() { - // Connection controls - elements.serverUrlInput.addEventListener("change", (e) => { - state.serverUrl = e.target.value; - localStorage.setItem("mam_server_url", state.serverUrl); + elements.serverUrlInput.addEventListener('change', (e) => { + state.serverUrl = e.target.value.trim().replace(/\/$/, ''); + localStorage.setItem('mam_server_url', state.serverUrl); + state.thumbCache = {}; // bust cache when server changes }); - elements.connectBtn.addEventListener("click", connectToServer); - - // Search and filter - elements.searchInput.addEventListener("input", debounce(handleSearch, 300)); - elements.projectFilter.addEventListener("change", handleProjectFilter); - - // Asset grid events are delegated - elements.assetGrid.addEventListener("click", handleAssetClick); - - // Import buttons - elements.importBtn.addEventListener("click", importSelectedAsset); - elements.importAllBtn.addEventListener("click", importAllAssets); + elements.connectBtn.addEventListener('click', connectToServer); + elements.searchInput.addEventListener('input', debounce(handleSearch, 300)); + elements.projectFilter.addEventListener('change', handleProjectFilter); + elements.assetGrid.addEventListener('click', handleAssetClick); + elements.importBtn.addEventListener('click', importSelectedAsset); + elements.importAllBtn.addEventListener('click', importAllAssets); } function restoreSettings() { @@ -110,34 +132,34 @@ async function connectToServer() { if (state.isConnecting) return; state.isConnecting = true; - updateConnectionStatus("connecting"); + updateConnectionStatus('connecting'); elements.connectBtn.disabled = true; try { - const response = await fetch(`${state.serverUrl}/api/health`, { - method: "GET", - headers: { - "Accept": "application/json" - } + // Use the projects endpoint as a health check (no separate /health route exists) + const response = await fetch(`${state.serverUrl}/api/v1/projects`, { + method: 'GET', + headers: { Accept: 'application/json' }, }); if (response.ok) { state.isConnected = true; - updateConnectionStatus("connected"); - elements.connectBtn.textContent = "Reconnect"; - logMessage("Connected to Wild Dragon MAM"); + updateConnectionStatus('connected'); + elements.connectBtn.textContent = 'Reconnect'; + logMessage('Connected to Wild Dragon MAM'); - // Fetch initial data - await fetchProjects(); + // Pass the already-fetched JSON to avoid a second round-trip + const projectData = await response.json(); + await fetchProjects(projectData); await fetchAssets(); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { - console.error("Connection error:", error); + console.error('Connection error:', error); state.isConnected = false; - updateConnectionStatus("disconnected"); - elements.connectBtn.textContent = "Connect"; + updateConnectionStatus('disconnected'); + elements.connectBtn.textContent = 'Connect'; showErrorMessage(`Failed to connect: ${error.message}`); } finally { state.isConnecting = false; @@ -147,137 +169,110 @@ async function connectToServer() { function updateConnectionStatus(status) { const indicator = elements.statusIndicator; - - indicator.classList.remove("connected", "connecting"); - - if (status === "connected") { - indicator.classList.add("connected"); - } else if (status === "connecting") { - indicator.classList.add("connecting"); - } + indicator.classList.remove('connected', 'connecting'); + if (status === 'connected') indicator.classList.add('connected'); + else if (status === 'connecting') indicator.classList.add('connecting'); } // ============================================================================ // API Calls // ============================================================================ -async function fetchProjects() { +/** + * Load projects into state and the filter dropdown. + * @param {Array} [preloadedData] - If provided, skips the fetch (reuse connection-check response). + */ +async function fetchProjects(preloadedData) { try { - const response = await fetch(`${state.serverUrl}/api/projects`, { - headers: { - "Accept": "application/json" - } - }); + let projects; + if (preloadedData) { + // GET /api/v1/projects returns a plain array (not { projects: [] }) + projects = Array.isArray(preloadedData) ? preloadedData : []; + } else { + const response = await fetch(`${state.serverUrl}/api/v1/projects`, { + headers: { Accept: 'application/json' }, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const data = await response.json(); + projects = Array.isArray(data) ? data : []; + } - if (!response.ok) throw new Error(`HTTP ${response.status}`); + state.projects = projects; - const data = await response.json(); - state.projects = data.projects || []; - - // Update project filter - const oldSelectedProject = state.selectedProject; + const savedProject = state.selectedProject; elements.projectFilter.innerHTML = ''; - - state.projects.forEach((project) => { - const option = document.createElement("option"); - option.value = project.id; - option.textContent = project.name; - elements.projectFilter.appendChild(option); + state.projects.forEach((p) => { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = p.name; + elements.projectFilter.appendChild(opt); }); + elements.projectFilter.value = savedProject; - elements.projectFilter.value = oldSelectedProject; logMessage(`Loaded ${state.projects.length} projects`); } catch (error) { - console.error("Error fetching projects:", error); - showErrorMessage("Failed to fetch projects"); + console.error('Error fetching projects:', error); } } async function fetchAssets(page = 0) { - if (!state.isConnected) { - showErrorMessage("Not connected to server"); - return; - } + if (!state.isConnected) return; try { const params = new URLSearchParams({ offset: page * state.pageSize, - limit: state.pageSize + limit: state.pageSize, }); - if (state.searchQuery) { - params.append("search", state.searchQuery); - } - - if (state.selectedProject !== "all") { - params.append("project_id", state.selectedProject); - } + if (state.searchQuery) params.append('search', state.searchQuery); + if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject); const response = await fetch( - `${state.serverUrl}/api/assets?${params.toString()}`, - { - headers: { - "Accept": "application/json" - } - } + `${state.serverUrl}/api/v1/assets?${params.toString()}`, + { headers: { Accept: 'application/json' } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); - state.assets = data.assets || []; - state.totalAssets = data.total || 0; + state.assets = data.assets || []; + state.totalAssets = data.total || 0; state.currentPage = page; renderAssets(); logMessage(`Loaded ${state.assets.length} assets`); } catch (error) { - console.error("Error fetching assets:", error); - showErrorMessage("Failed to fetch assets"); + console.error('Error fetching assets:', error); + showErrorMessage('Failed to fetch assets'); } } async function fetchAssetDetails(assetId) { if (!state.isConnected) return null; - try { - const response = await fetch(`${state.serverUrl}/api/assets/${assetId}`, { - headers: { - "Accept": "application/json" - } + const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, { + headers: { Accept: 'application/json' }, }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { - console.error("Error fetching asset details:", error); + console.error('Error fetching asset details:', error); return null; } } -async function getSignedUrl(assetId, proxyId = null) { - if (!state.isConnected) return null; - - try { - const endpoint = proxyId - ? `${state.serverUrl}/api/assets/${assetId}/proxies/${proxyId}/download` - : `${state.serverUrl}/api/assets/${assetId}/download`; - - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Accept": "application/json" - } - }); - - if (!response.ok) throw new Error(`HTTP ${response.status}`); - - const data = await response.json(); - return data.signed_url; - } catch (error) { - console.error("Error getting signed URL:", error); - return null; - } +/** + * Returns a short-lived presigned URL for the H.264 proxy of the given asset. + * GET /api/v1/assets/:id/stream -> { url: '...' } + */ +async function getSignedDownloadUrl(assetId) { + const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, { + headers: { Accept: 'application/json' }, + }); + if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`); + const { url } = await response.json(); + if (!url) throw new Error('Stream endpoint returned no URL'); + return url; } // ============================================================================ @@ -285,112 +280,108 @@ async function getSignedUrl(assetId, proxyId = null) { // ============================================================================ function renderAssets() { - elements.assetGrid.innerHTML = ""; + elements.assetGrid.innerHTML = ''; if (state.assets.length === 0) { - elements.emptyState.style.display = "flex"; + elements.emptyState.style.display = 'flex'; return; } - elements.emptyState.style.display = "none"; - + elements.emptyState.style.display = 'none'; state.assets.forEach((asset) => { - const card = createAssetCard(asset); - elements.assetGrid.appendChild(card); + elements.assetGrid.appendChild(createAssetCard(asset)); }); } function createAssetCard(asset) { - const card = document.createElement("div"); - card.className = "asset-card"; - if (state.selectedAsset?.id === asset.id) { - card.classList.add("selected"); + const card = document.createElement('div'); + card.className = 'asset-card'; + if (state.selectedAsset && state.selectedAsset.id === asset.id) { + card.classList.add('selected'); } card.dataset.assetId = asset.id; - // Thumbnail - const thumbnail = document.createElement("div"); - thumbnail.className = "asset-thumbnail"; + // Thumbnail — lazy loaded by IntersectionObserver + const thumbnail = document.createElement('div'); + thumbnail.className = 'asset-thumbnail'; - if (asset.thumbnail_url) { - const img = document.createElement("img"); - img.src = asset.thumbnail_url; - img.alt = asset.filename; - img.loading = "lazy"; - img.onerror = () => { - img.style.display = "none"; - thumbnail.textContent = "No preview"; - }; - thumbnail.appendChild(img); - } else { - thumbnail.textContent = "No preview"; - } + const img = document.createElement('img'); + img.dataset.assetId = asset.id; + img.alt = escapeHtml(asset.display_name || asset.filename || ''); + img.style.cssText = 'width:100%;height:100%;object-fit:cover;display:block;'; + img.onerror = () => { img.style.display = 'none'; }; + thumbnail.appendChild(img); + thumbObserver.observe(img); card.appendChild(thumbnail); - // Info - const info = document.createElement("div"); - info.className = "asset-info"; + // Info row + const info = document.createElement('div'); + info.className = 'asset-info'; - const filename = document.createElement("div"); - filename.className = "asset-filename"; - filename.title = asset.filename; - filename.textContent = asset.filename; - info.appendChild(filename); + const name = asset.display_name || asset.filename || 'Untitled'; + const filenameEl = document.createElement('div'); + filenameEl.className = 'asset-filename'; + filenameEl.title = name; + filenameEl.textContent = name; + info.appendChild(filenameEl); - const meta = document.createElement("div"); - meta.className = "asset-meta"; - meta.innerHTML = ` - ${asset.duration ? formatDuration(asset.duration) : "N/A"} - ${asset.codec || "N/A"} - `; + const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null; + const codec = (asset.metadata && asset.metadata.codec) || asset.media_type || 'video'; + const meta = document.createElement('div'); + meta.className = 'asset-meta'; + meta.innerHTML = [ + '' + (durationSec ? formatDuration(durationSec) : 'N/A') + '', + '' + escapeHtml(codec.toUpperCase()) + '', + ].join(''); info.appendChild(meta); - const status = document.createElement("div"); - status.className = `asset-status-badge status-badge ${asset.status || "ready"}`; - status.textContent = (asset.status || "ready").toUpperCase(); - info.appendChild(status); + const statusStr = asset.status || 'ready'; + const statusBadge = document.createElement('div'); + statusBadge.className = 'asset-status-badge status-badge ' + statusStr; + statusBadge.textContent = statusStr.toUpperCase(); + info.appendChild(statusBadge); card.appendChild(info); - return card; } function showAssetDetails(asset) { state.selectedAsset = asset; + elements.detailsPanel.classList.remove('hidden'); - elements.detailsPanel.classList.remove("hidden"); - elements.detailsFilename.textContent = asset.filename; - elements.detailsCodec.textContent = asset.codec || "Unknown"; - elements.detailsResolution.textContent = asset.resolution || "N/A"; - elements.detailsFps.textContent = asset.fps || "N/A"; - elements.detailsDuration.textContent = asset.duration - ? formatDuration(asset.duration) - : "N/A"; - elements.detailsSize.textContent = asset.file_size - ? formatFileSize(asset.file_size) - : "N/A"; + const meta = asset.metadata || {}; + elements.detailsFilename.textContent = asset.display_name || asset.filename; + elements.detailsCodec.textContent = meta.codec || asset.media_type || 'Unknown'; + elements.detailsResolution.textContent = meta.resolution || 'N/A'; + elements.detailsFps.textContent = meta.fps ? meta.fps + ' fps' : 'N/A'; + elements.detailsDuration.textContent = asset.duration_ms + ? formatDuration(asset.duration_ms / 1000) + : 'N/A'; + elements.detailsSize.textContent = meta.file_size + ? formatFileSize(meta.file_size) + : 'N/A'; - // Render tags - elements.detailsTags.innerHTML = ""; - if (asset.tags && asset.tags.length > 0) { - asset.tags.forEach((tag) => { - const tagEl = document.createElement("span"); - tagEl.className = "tag"; - tagEl.textContent = tag; - elements.detailsTags.appendChild(tagEl); + elements.detailsTags.innerHTML = ''; + const tags = asset.tags || []; + if (tags.length > 0) { + tags.forEach((tag) => { + const el = document.createElement('span'); + el.className = 'tag'; + el.textContent = tag; + elements.detailsTags.appendChild(el); }); } else { - elements.detailsTags.innerHTML = 'No tags'; + elements.detailsTags.innerHTML = + 'No tags'; } - // Update import button state elements.importBtn.disabled = false; } function hideAssetDetails() { state.selectedAsset = null; - elements.detailsPanel.classList.add("hidden"); + elements.detailsPanel.classList.add('hidden'); elements.importBtn.disabled = true; } @@ -416,158 +407,157 @@ function handleProjectFilter(e) { async function importSelectedAsset() { if (!state.selectedAsset) { - showErrorMessage("No asset selected"); + showErrorMessage('No asset selected'); return; } - await importAsset(state.selectedAsset); } async function importAllAssets() { if (state.assets.length === 0) { - showErrorMessage("No assets to import"); + showErrorMessage('No assets to import'); return; } - for (const asset of state.assets) { await importAsset(asset); } - - showSuccessMessage("All assets imported"); + showSuccessMessage('All assets imported'); } async function importAsset(asset) { try { elements.importBtn.disabled = true; - // Get the proxy file (or original if no proxy) - const proxyId = asset.proxy_file_id || asset.file_id; - if (!proxyId) { - showErrorMessage("No file available for import"); - return; - } + // Step 1: get a presigned proxy URL from the MAM API + showProgress('Getting download link...', 5); + const url = await getSignedDownloadUrl(asset.id); - // Get signed URL for download - showProgress("Getting download link...", 0); - const signedUrl = await getSignedUrl(asset.id, proxyId); + // Step 2: download the proxy (H.264) to the OS temp directory + const safeName = sanitizeFilename( + (asset.display_name || asset.filename || asset.id) + '.mp4' + ); + showProgress('Downloading ' + safeName + '...', 10); + const filePath = await downloadFile(url, safeName); - if (!signedUrl) { - showErrorMessage("Failed to get download link"); - return; - } - - // Download the file - showProgress(`Downloading ${asset.filename}...`, 30); - const filePath = await downloadFile(signedUrl, asset.filename); - - if (!filePath) { - showErrorMessage("Failed to download file"); - return; - } - - // Import into Premiere - showProgress("Importing into Premiere Pro...", 70); + // Step 3: hand the local path to Premiere Pro via ExtendScript + showProgress('Importing into Premiere Pro...', 85); await importFileToPremiereProject(filePath); hideProgress(); - showSuccessMessage(`Imported: ${asset.filename}`); + showSuccessMessage('Imported: ' + safeName); } catch (error) { - console.error("Import error:", error); - showErrorMessage(`Import failed: ${error.message}`); + console.error('Import error:', error); + hideProgress(); + showErrorMessage('Import failed: ' + error.message); } finally { - elements.importBtn.disabled = state.selectedAsset === null; + elements.importBtn.disabled = !state.selectedAsset; } } -async function downloadFile(url, filename) { - return new Promise((resolve, reject) => { - // In a real implementation, use Node.js fs module or fetch API - // For now, we'll use fetch with progress tracking +/** + * Downloads a remote URL to a local temp file using Node.js http/https. + * + * Node.js is available via require() in CEP when the manifest contains: + * --enable-nodejs + * + * @param {string} url - Presigned download URL (http or https) + * @param {string} filename - Desired local filename (sanitized) + * @returns {Promise} Resolved with the absolute path to the saved file + */ +function downloadFile(url, filename) { + return new Promise(function (resolve, reject) { + try { + var https = require('https'); + var http = require('http'); + var fs = require('fs'); + var path = require('path'); + var os = require('os'); - fetch(url) - .then((response) => { - if (!response.ok) throw new Error(`HTTP ${response.status}`); + var tempPath = path.join(os.tmpdir(), filename); + var file = fs.createWriteStream(tempPath); + var protocol = url.startsWith('https') ? https : http; - const contentLength = response.headers.get("content-length"); - const reader = response.body.getReader(); - let receivedLength = 0; - - const chunks = []; - - function read() { - reader.read().then(({ done, value }) => { - if (done) { - // Convert chunks to blob - const blob = new Blob(chunks); - - // Save file (would need Node.js in production) - // For CEP, you'd use special file I/O - const tempPath = `${getTempPath()}/${filename}`; - - // Update progress - state.downloadProgress = 100; - updateProgressUI(); - - resolve(tempPath); - return; - } - - chunks.push(value); - receivedLength += value.length; - - if (contentLength) { - state.downloadProgress = 30 + (receivedLength / contentLength) * 40; - updateProgressUI(); - } - - read(); - }); + protocol.get(url, function (res) { + if (res.statusCode !== 200) { + file.close(); + fs.unlink(tempPath, function () {}); + reject(new Error('Download HTTP ' + res.statusCode)); + return; } - read(); - }) - .catch(reject); + var total = parseInt(res.headers['content-length'] || '0', 10); + var received = 0; + + res.on('data', function (chunk) { + received += chunk.length; + if (total > 0) { + state.downloadProgress = 10 + (received / total) * 75; + updateProgressUI(); + } + }); + + res.pipe(file); + + file.on('finish', function () { + file.close(function () { resolve(tempPath); }); + }); + + file.on('error', function (err) { + fs.unlink(tempPath, function () {}); + reject(err); + }); + }).on('error', function (err) { + fs.unlink(tempPath, function () {}); + reject(err); + }); + } catch (err) { + reject(new Error('Node.js unavailable for download: ' + err.message)); + } }); } -function getTempPath() { - if (navigator.platform.includes("Win")) { - return process.env.TEMP || "C:\\Users\\%USERNAME%\\AppData\\Local\\Temp"; - } else { - return "/tmp"; - } -} +/** + * Uses csInterface.evalScript to call the Premiere Pro ExtendScript layer + * and import the given local file into the active project. + * + * @param {string} filePath - Absolute local path to the downloaded proxy file + * @returns {Promise} + */ +function importFileToPremiereProject(filePath) { + return new Promise(function (resolve, reject) { + // Escape backslashes for Windows paths inside the script string literal + var safePath = filePath.replace(/\\/g, '\\\\'); -async function importFileToPremiereProject(filePath) { - return new Promise((resolve, reject) => { - const script = ` - var result = {}; - try { - if (app.project) { - app.project.importFiles(["${filePath}"]); - result.success = true; - result.message = "File imported successfully"; - } else { - result.success = false; - result.message = "No active project"; - } - } catch (error) { - result.success = false; - result.message = error.message; - } - JSON.stringify(result); - `; + var script = [ + '(function() {', + ' var result = { success: false, message: "" };', + ' try {', + ' if (!app.project) {', + ' result.message = "No active Premiere Pro project";', + ' return JSON.stringify(result);', + ' }', + ' var f = new File("' + safePath + '");', + ' if (!f.exists) {', + ' result.message = "File not found: ' + safePath + '";', + ' return JSON.stringify(result);', + ' }', + ' app.project.importFiles(["' + safePath + '"]);', + ' result.success = true;', + ' result.message = "Imported successfully";', + ' } catch (e) {', + ' result.message = e.message;', + ' }', + ' return JSON.stringify(result);', + '})();', + ].join('\n'); - csInterface.evalScript(script, (result) => { + csInterface.evalScript(script, function (resultStr) { try { - const parsed = JSON.parse(result); - if (parsed.success) { - resolve(parsed); - } else { - reject(new Error(parsed.message)); - } - } catch (error) { - reject(error); + var parsed = JSON.parse(resultStr); + if (parsed.success) resolve(parsed); + else reject(new Error(parsed.message)); + } catch (e) { + reject(new Error('ExtendScript error: ' + resultStr)); } }); }); @@ -578,70 +568,55 @@ async function importFileToPremiereProject(filePath) { // ============================================================================ function handleAssetClick(e) { - const card = e.target.closest(".asset-card"); + var card = e.target.closest('.asset-card'); if (!card) return; - // Clear previous selection - document.querySelectorAll(".asset-card.selected").forEach((el) => { - el.classList.remove("selected"); + document.querySelectorAll('.asset-card.selected').forEach(function (el) { + el.classList.remove('selected'); }); + card.classList.add('selected'); - // Select new card - card.classList.add("selected"); - - // Show details - const assetId = card.dataset.assetId; - const asset = state.assets.find((a) => a.id === assetId); - - if (asset) { - showAssetDetails(asset); - } + var assetId = card.dataset.assetId; + var asset = state.assets.find(function (a) { return a.id === assetId; }); + if (asset) showAssetDetails(asset); } function showProgress(label, percent) { - elements.progressContainer.classList.add("visible"); + elements.progressContainer.classList.add('visible'); elements.progressLabel.textContent = label; state.downloadProgress = percent; updateProgressUI(); } function hideProgress() { - elements.progressContainer.classList.remove("visible"); + elements.progressContainer.classList.remove('visible'); state.downloadProgress = 0; + updateProgressUI(); } function updateProgressUI() { - elements.progressFill.style.width = `${state.downloadProgress}%`; + elements.progressFill.style.width = state.downloadProgress + '%'; } function showErrorMessage(message) { - const container = document.createElement("div"); - container.className = "error-message"; - container.textContent = message; - - const searchArea = document.querySelector(".search-filter-area"); - searchArea.insertBefore(container, searchArea.firstChild); - - setTimeout(() => { - container.remove(); - }, 5000); + _showFlash(message, 'error-message'); } function showSuccessMessage(message) { - const container = document.createElement("div"); - container.className = "success-message"; - container.textContent = message; + _showFlash(message, 'success-message'); +} - const searchArea = document.querySelector(".search-filter-area"); - searchArea.insertBefore(container, searchArea.firstChild); - - setTimeout(() => { - container.remove(); - }, 5000); +function _showFlash(message, className) { + var el = document.createElement('div'); + el.className = className; + el.textContent = message; + var anchor = document.querySelector('.search-filter-area'); + anchor.insertBefore(el, anchor.firstChild); + setTimeout(function () { el.remove(); }, 5000); } function logMessage(message) { - console.log(`[MAM Panel] ${message}`); + console.log('[MAM Panel] ' + message); } // ============================================================================ @@ -649,34 +624,45 @@ function logMessage(message) { // ============================================================================ function debounce(func, delay) { - let timeout; - return function executedFunction(...args) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; + var timeout; + return function () { + var args = arguments; clearTimeout(timeout); - timeout = setTimeout(later, delay); + timeout = setTimeout(function () { func.apply(null, args); }, delay); }; } -function formatDuration(seconds) { - if (!seconds) return "N/A"; - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} - if (hours > 0) { - return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; - } else { - return `${minutes}:${String(secs).padStart(2, "0")}`; +/** + * Strips characters illegal in file names on Windows/macOS. + */ +function sanitizeFilename(name) { + return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_'); +} + +function formatDuration(seconds) { + if (!seconds) return 'N/A'; + var h = Math.floor(seconds / 3600); + var m = Math.floor((seconds % 3600) / 60); + var s = Math.floor(seconds % 60); + if (h > 0) { + return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); } + return m + ':' + String(s).padStart(2, '0'); } function formatFileSize(bytes) { - if (!bytes) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; + if (!bytes) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }