682 lines
20 KiB
JavaScript
682 lines
20 KiB
JavaScript
/**
|
|
* 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 = '<option value="all">All Projects</option>';
|
|
|
|
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 = `
|
|
<span>${asset.duration ? formatDuration(asset.duration) : "N/A"}</span>
|
|
<span>${asset.codec || "N/A"}</span>
|
|
`;
|
|
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 = '<span style="color: var(--text-secondary);">No tags</span>';
|
|
}
|
|
|
|
// 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];
|
|
}
|