loreal-video-optimizer/frontend/app.js
DJP 129ea3ec1e Initial commit: Video Optimizer for L'Oréal
Complete video optimization tool with:
- 21 platform configurations (Meta, TikTok, YouTube, Pinterest, Snapchat, Amazon)
- FFmpeg-powered video conversion with H264, H265, and VP9 codecs
- Python Flask backend with REST API
- HTML/JS frontend with drag-drop interface
- Black + #FFC407 color scheme with Montserrat font
- Side-by-side video comparison player
- Filename auto-detection for platform and aspect ratio
- MAMP-compatible setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:52:11 -04:00

411 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Video Optimizer Frontend JavaScript
// API Configuration (imported from config.js)
const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api';
// State
let currentFileId = null;
let currentPlatforms = [];
let currentVideoInfo = null;
// DOM Elements
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const uploadSection = document.getElementById('uploadSection');
const configSection = document.getElementById('configSection');
const comparisonSection = document.getElementById('comparisonSection');
const videoInfo = document.getElementById('videoInfo');
const platformSelect = document.getElementById('platformSelect');
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
const bitrateInput = document.getElementById('bitrateInput');
const bitrateHint = document.getElementById('bitrateHint');
const formatInfo = document.getElementById('formatInfo');
const convertBtn = document.getElementById('convertBtn');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const originalVideo = document.getElementById('originalVideo');
const optimizedVideo = document.getElementById('optimizedVideo');
const originalSource = document.getElementById('originalSource');
const optimizedSource = document.getElementById('optimizedSource');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadPlatforms();
setupEventListeners();
});
// Event Listeners
function setupEventListeners() {
// Drag and drop
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', handleDragOver);
dropZone.addEventListener('dragleave', handleDragLeave);
dropZone.addEventListener('drop', handleDrop);
// File input
fileInput.addEventListener('change', handleFileSelect);
// Platform/aspect ratio selection
platformSelect.addEventListener('change', handlePlatformChange);
aspectRatioSelect.addEventListener('change', handleAspectRatioChange);
// Convert button
convertBtn.addEventListener('click', handleConvert);
// Comparison controls
document.getElementById('syncPlayBtn').addEventListener('click', syncPlayback);
document.getElementById('pauseAllBtn').addEventListener('click', pauseAll);
document.getElementById('downloadOriginal').addEventListener('click', () => downloadFile('original'));
document.getElementById('downloadOptimized').addEventListener('click', () => downloadFile('optimized'));
document.getElementById('newFileBtn').addEventListener('click', resetApp);
}
// Drag and Drop Handlers
function handleDragOver(e) {
e.preventDefault();
dropZone.classList.add('dragover');
}
function handleDragLeave(e) {
e.preventDefault();
dropZone.classList.remove('dragover');
}
function handleDrop(e) {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
}
function handleFileSelect(e) {
const files = e.target.files;
if (files.length > 0) {
handleFile(files[0]);
}
}
// File Handling
async function handleFile(file) {
if (!file.type.startsWith('video/')) {
alert('Please select a valid video file');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
showLoading();
const response = await fetch(`${API_BASE}/upload`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
currentFileId = data.file_id;
currentVideoInfo = data.video_info;
displayVideoInfo(data);
// Auto-select platform and aspect ratio if detected
if (data.detected_platform) {
platformSelect.value = data.detected_platform;
handlePlatformChange();
}
if (data.detected_aspect_ratio) {
aspectRatioSelect.value = data.detected_aspect_ratio;
handleAspectRatioChange();
}
uploadSection.style.display = 'none';
configSection.style.display = 'block';
} else {
alert('Error uploading file: ' + data.error);
}
} catch (error) {
alert('Error uploading file: ' + error.message);
} finally {
hideLoading();
}
}
// Platform Management
async function loadPlatforms() {
try {
const response = await fetch(`${API_BASE}/platforms`);
const data = await response.json();
currentPlatforms = data.platforms;
// Populate platform select
platformSelect.innerHTML = '<option value="">Select Platform...</option>';
data.platforms.forEach(platform => {
const option = document.createElement('option');
option.value = platform.key;
option.textContent = platform.name;
platformSelect.appendChild(option);
});
} catch (error) {
console.error('Error loading platforms:', error);
}
}
function handlePlatformChange() {
const platformKey = platformSelect.value;
if (!platformKey) {
aspectRatioSelect.innerHTML = '<option value="">Select Platform First</option>';
aspectRatioSelect.disabled = true;
convertBtn.disabled = true;
formatInfo.innerHTML = '';
return;
}
const platform = currentPlatforms.find(p => p.key === platformKey);
if (platform) {
// Populate aspect ratio select
aspectRatioSelect.innerHTML = '<option value="">Select Aspect Ratio...</option>';
platform.formats.forEach(format => {
const option = document.createElement('option');
option.value = format.ratio;
option.textContent = `${format.ratio} (${format.size})`;
aspectRatioSelect.appendChild(option);
});
aspectRatioSelect.disabled = false;
// Show codec info
formatInfo.innerHTML = `
<h4>Platform: ${platform.name}</h4>
<p><strong>Codec:</strong> ${platform.codec}</p>
`;
}
validateForm();
}
function handleAspectRatioChange() {
const platformKey = platformSelect.value;
const aspectRatio = aspectRatioSelect.value;
if (!platformKey || !aspectRatio) {
convertBtn.disabled = true;
return;
}
const platform = currentPlatforms.find(p => p.key === platformKey);
const format = platform.formats.find(f => f.ratio === aspectRatio);
if (format) {
// Update format info
formatInfo.innerHTML = `
<h4>Platform: ${platform.name}</h4>
<p><strong>Codec:</strong> ${platform.codec}</p>
<p><strong>Resolution:</strong> ${format.size}</p>
<p><strong>Recommended Bitrate:</strong> ${format.bitrate}</p>
<p><strong>Bitrate Range:</strong> ${format.bitrate_min} - ${format.bitrate_max}</p>
<p><strong>Audio Bitrate:</strong> ${format.audio}</p>
${format.note ? `<p><strong>Note:</strong> ${format.note}</p>` : ''}
`;
// Update bitrate hint
bitrateHint.textContent = `Recommended: ${format.bitrate} (Range: ${format.bitrate_min} - ${format.bitrate_max})`;
}
validateForm();
}
function validateForm() {
const platformKey = platformSelect.value;
const aspectRatio = aspectRatioSelect.value;
convertBtn.disabled = !(platformKey && aspectRatio);
}
// Video Conversion
async function handleConvert() {
const platformKey = platformSelect.value;
const aspectRatio = aspectRatioSelect.value;
const customBitrate = bitrateInput.value.trim() || null;
if (!currentFileId || !platformKey || !aspectRatio) {
alert('Please complete all required fields');
return;
}
try {
convertBtn.disabled = true;
progressBar.style.display = 'block';
progressFill.style.width = '50%';
progressText.textContent = 'Converting video...';
const response = await fetch(`${API_BASE}/convert`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
file_id: currentFileId,
platform: platformKey,
aspect_ratio: aspectRatio,
custom_bitrate: customBitrate
})
});
const data = await response.json();
if (data.success) {
progressFill.style.width = '100%';
progressText.textContent = 'Conversion complete!';
// Display comparison
displayComparison(data);
setTimeout(() => {
configSection.style.display = 'none';
comparisonSection.style.display = 'block';
}, 1000);
} else {
alert('Conversion error: ' + data.error);
convertBtn.disabled = false;
}
} catch (error) {
alert('Conversion error: ' + error.message);
convertBtn.disabled = false;
} finally {
setTimeout(() => {
progressBar.style.display = 'none';
progressFill.style.width = '0%';
}, 1500);
}
}
// Display Functions
function displayVideoInfo(data) {
const info = data.video_info;
const detected = [];
if (data.detected_platform) {
const platform = currentPlatforms.find(p => p.key === data.detected_platform);
detected.push(`Platform: ${platform ? platform.name : data.detected_platform}`);
}
if (data.detected_aspect_ratio) {
detected.push(`Aspect Ratio: ${data.detected_aspect_ratio}`);
}
videoInfo.innerHTML = `
<h3>📹 ${data.filename}</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">Resolution</span>
<span class="info-value">${info.width} × ${info.height}</span>
</div>
<div class="info-item">
<span class="info-label">Duration</span>
<span class="info-value">${formatDuration(info.duration)}</span>
</div>
<div class="info-item">
<span class="info-label">File Size</span>
<span class="info-value">${formatBytes(info.size)}</span>
</div>
<div class="info-item">
<span class="info-label">Bitrate</span>
<span class="info-value">${info.bitrate} kbps</span>
</div>
<div class="info-item">
<span class="info-label">Codec</span>
<span class="info-value">${info.codec}</span>
</div>
<div class="info-item">
<span class="info-label">Aspect Ratio</span>
<span class="info-value">${info.aspect_ratio}</span>
</div>
</div>
${detected.length > 0 ? `<p style="margin-top: 1rem; color: var(--primary-yellow);">🎯 Auto-detected: ${detected.join(', ')}</p>` : ''}
`;
}
function displayComparison(data) {
// Update stats
document.getElementById('originalSize').textContent = formatBytes(data.input_size);
document.getElementById('optimizedSize').textContent = formatBytes(data.output_size);
document.getElementById('reduction').textContent = `${data.size_reduction_percent}%`;
// Set video sources
originalSource.src = `${API_BASE}/stream/original/${currentFileId}`;
optimizedSource.src = `${API_BASE}/stream/optimized/${currentFileId}`;
// Reload videos
originalVideo.load();
optimizedVideo.load();
}
// Video Playback Controls
function syncPlayback() {
originalVideo.currentTime = 0;
optimizedVideo.currentTime = 0;
originalVideo.play();
optimizedVideo.play();
}
function pauseAll() {
originalVideo.pause();
optimizedVideo.pause();
}
// Download
function downloadFile(type) {
window.open(`${API_BASE}/download/${type}/${currentFileId}`, '_blank');
}
// Utility Functions
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
function formatDuration(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
function showLoading() {
uploadSection.classList.add('loading');
}
function hideLoading() {
uploadSection.classList.remove('loading');
}
function resetApp() {
// Clean up files
if (currentFileId) {
fetch(`${API_BASE}/cleanup/${currentFileId}`, { method: 'DELETE' });
}
// Reset state
currentFileId = null;
currentVideoInfo = null;
fileInput.value = '';
platformSelect.value = '';
aspectRatioSelect.value = '';
bitrateInput.value = '';
formatInfo.innerHTML = '';
// Reset display
uploadSection.style.display = 'block';
configSection.style.display = 'none';
comparisonSection.style.display = 'none';
}