/** * Wild Dragon MAM - Premiere Pro Panel * Main JavaScript file for the CEP panel */ // ============================================================================ // 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, downloadProgress: 0, isDownloading: false }; // ============================================================================ // DOM Elements // ============================================================================ 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") }; } // ============================================================================ // Initialization // ============================================================================ document.addEventListener("DOMContentLoaded", () => { initDOMElements(); setupEventListeners(); restoreSettings(); 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.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); } function restoreSettings() { elements.serverUrlInput.value = state.serverUrl; } // ============================================================================ // Server Connection // ============================================================================ async function connectToServer() { if (state.isConnecting) return; state.isConnecting = true; updateConnectionStatus("connecting"); elements.connectBtn.disabled = true; try { const response = await fetch(`${state.serverUrl}/api/health`, { method: "GET", headers: { "Accept": "application/json" } }); if (response.ok) { state.isConnected = true; updateConnectionStatus("connected"); elements.connectBtn.textContent = "Reconnect"; logMessage("Connected to Wild Dragon MAM"); // Fetch initial data await fetchProjects(); await fetchAssets(); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error("Connection error:", error); state.isConnected = false; updateConnectionStatus("disconnected"); elements.connectBtn.textContent = "Connect"; showErrorMessage(`Failed to connect: ${error.message}`); } finally { state.isConnecting = false; elements.connectBtn.disabled = false; } } 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"); } } // ============================================================================ // API Calls // ============================================================================ async function fetchProjects() { try { const response = await fetch(`${state.serverUrl}/api/projects`, { headers: { "Accept": "application/json" } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); state.projects = data.projects || []; // Update project filter const oldSelectedProject = 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); }); elements.projectFilter.value = oldSelectedProject; logMessage(`Loaded ${state.projects.length} projects`); } catch (error) { console.error("Error fetching projects:", error); showErrorMessage("Failed to fetch projects"); } } async function fetchAssets(page = 0) { if (!state.isConnected) { showErrorMessage("Not connected to server"); return; } try { const params = new URLSearchParams({ offset: page * state.pageSize, limit: state.pageSize }); 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" } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); 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"); } } async function fetchAssetDetails(assetId) { if (!state.isConnected) return null; try { const response = await fetch(`${state.serverUrl}/api/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); 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; } } // ============================================================================ // UI Rendering // ============================================================================ function renderAssets() { elements.assetGrid.innerHTML = ""; if (state.assets.length === 0) { elements.emptyState.style.display = "flex"; return; } elements.emptyState.style.display = "none"; state.assets.forEach((asset) => { const card = createAssetCard(asset); elements.assetGrid.appendChild(card); }); } function createAssetCard(asset) { const card = document.createElement("div"); card.className = "asset-card"; if (state.selectedAsset?.id === asset.id) { card.classList.add("selected"); } card.dataset.assetId = asset.id; // Thumbnail 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"; } card.appendChild(thumbnail); // Info 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 meta = document.createElement("div"); meta.className = "asset-meta"; meta.innerHTML = ` ${asset.duration ? formatDuration(asset.duration) : "N/A"} ${asset.codec || "N/A"} `; 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); card.appendChild(info); return card; } function showAssetDetails(asset) { state.selectedAsset = asset; 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"; // 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); }); } else { elements.detailsTags.innerHTML = 'No tags'; } // Update import button state elements.importBtn.disabled = false; } function hideAssetDetails() { state.selectedAsset = null; elements.detailsPanel.classList.add("hidden"); elements.importBtn.disabled = true; } // ============================================================================ // Search and Filter // ============================================================================ function handleSearch(e) { state.searchQuery = e.target.value; state.currentPage = 0; fetchAssets(); } function handleProjectFilter(e) { state.selectedProject = e.target.value; state.currentPage = 0; fetchAssets(); } // ============================================================================ // Import Functionality // ============================================================================ async function importSelectedAsset() { if (!state.selectedAsset) { showErrorMessage("No asset selected"); return; } await importAsset(state.selectedAsset); } async function importAllAssets() { if (state.assets.length === 0) { showErrorMessage("No assets to import"); return; } for (const asset of state.assets) { await importAsset(asset); } 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; } // Get signed URL for download showProgress("Getting download link...", 0); const signedUrl = await getSignedUrl(asset.id, proxyId); 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); await importFileToPremiereProject(filePath); hideProgress(); showSuccessMessage(`Imported: ${asset.filename}`); } catch (error) { console.error("Import error:", error); showErrorMessage(`Import failed: ${error.message}`); } finally { elements.importBtn.disabled = state.selectedAsset === null; } } 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 fetch(url) .then((response) => { if (!response.ok) throw new Error(`HTTP ${response.status}`); 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(); }); } read(); }) .catch(reject); }); } function getTempPath() { if (navigator.platform.includes("Win")) { return process.env.TEMP || "C:\\Users\\%USERNAME%\\AppData\\Local\\Temp"; } else { return "/tmp"; } } 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); `; csInterface.evalScript(script, (result) => { try { const parsed = JSON.parse(result); if (parsed.success) { resolve(parsed); } else { reject(new Error(parsed.message)); } } catch (error) { reject(error); } }); }); } // ============================================================================ // UI Helpers // ============================================================================ function handleAssetClick(e) { const card = e.target.closest(".asset-card"); if (!card) return; // Clear previous selection document.querySelectorAll(".asset-card.selected").forEach((el) => { el.classList.remove("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); } } function showProgress(label, percent) { elements.progressContainer.classList.add("visible"); elements.progressLabel.textContent = label; state.downloadProgress = percent; updateProgressUI(); } function hideProgress() { elements.progressContainer.classList.remove("visible"); state.downloadProgress = 0; } function updateProgressUI() { 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); } function showSuccessMessage(message) { const container = document.createElement("div"); container.className = "success-message"; container.textContent = message; const searchArea = document.querySelector(".search-filter-area"); searchArea.insertBefore(container, searchArea.firstChild); setTimeout(() => { container.remove(); }, 5000); } function logMessage(message) { console.log(`[MAM Panel] ${message}`); } // ============================================================================ // Utility Functions // ============================================================================ function debounce(func, delay) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, 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); if (hours > 0) { return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`; } else { return `${minutes}:${String(secs).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]; }