diff --git a/services/premiere-plugin/js/main.js b/services/premiere-plugin/js/main.js new file mode 100644 index 0000000..2b57f08 --- /dev/null +++ b/services/premiere-plugin/js/main.js @@ -0,0 +1,682 @@ +/** + * 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]; +}