loreal-video-optimizer/frontend/app.js
DJP 5e3d921f06 Add red warning and outline for aspect ratio changes on comparison page
Features:
- Red-bordered warning message on comparison page when aspect ratio changed
- Red outline with glow effect around optimized video player
- Visual indicators clearly show when video aspect ratio differs from original
- Warning states: "Aspect Ratio Changed: This video was converted to a different aspect ratio"
- State properly tracked and reset when uploading new files

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

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

486 lines
17 KiB
JavaScript
Raw Permalink 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;
let aspectRatioChanged = false;
// 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('muteOriginal').addEventListener('click', () => toggleMute('original'));
document.getElementById('muteOptimized').addEventListener('click', () => toggleMute('optimized'));
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})`;
// Check for aspect ratio mismatch and show warning
const warningDiv = document.getElementById('aspectRatioWarning');
if (currentVideoInfo && currentVideoInfo.aspect_ratio !== aspectRatio) {
warningDiv.style.display = 'block';
aspectRatioChanged = true;
} else {
warningDiv.style.display = 'none';
aspectRatioChanged = false;
}
}
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}`;
// Display video specifications
displayVideoSpecs('originalSpecs', {
platform: 'Unknown',
resolution: `${currentVideoInfo.width}×${currentVideoInfo.height}`,
codec: currentVideoInfo.codec.toUpperCase(),
bitrate: `${currentVideoInfo.bitrate} kbps`,
duration: formatDuration(currentVideoInfo.duration),
fileSize: formatBytes(data.input_size),
aspectRatio: currentVideoInfo.aspect_ratio
});
const conversionDetails = data.conversion_details;
displayVideoSpecs('optimizedSpecs', {
resolution: conversionDetails.resolution,
codec: conversionDetails.codec.replace('lib', '').toUpperCase(),
bitrate: conversionDetails.bitrate,
duration: formatDuration(conversionDetails.duration),
fileSize: formatBytes(data.output_size),
platform: conversionDetails.platform.charAt(0).toUpperCase() + conversionDetails.platform.slice(1),
aspectRatio: conversionDetails.aspect_ratio
});
// Show warning and red outline if aspect ratio was changed
const comparisonWarning = document.getElementById('aspectRatioChangedWarning');
if (aspectRatioChanged) {
comparisonWarning.style.display = 'block';
optimizedVideo.classList.add('aspect-changed');
} else {
comparisonWarning.style.display = 'none';
optimizedVideo.classList.remove('aspect-changed');
}
// Reload videos
originalVideo.load();
optimizedVideo.load();
}
function displayVideoSpecs(elementId, specs) {
const specsElement = document.getElementById(elementId);
specsElement.innerHTML = `
${specs.platform ? `<div class="spec-row"><span class="spec-label">Platform:</span><span class="spec-value">${specs.platform}</span></div>` : ''}
<div class="spec-row"><span class="spec-label">Resolution:</span><span class="spec-value">${specs.resolution}</span></div>
<div class="spec-row"><span class="spec-label">Aspect Ratio:</span><span class="spec-value">${specs.aspectRatio}</span></div>
<div class="spec-row"><span class="spec-label">Codec:</span><span class="spec-value">${specs.codec}</span></div>
<div class="spec-row"><span class="spec-label">Bitrate:</span><span class="spec-value">${specs.bitrate}</span></div>
<div class="spec-row"><span class="spec-label">Duration:</span><span class="spec-value">${specs.duration}</span></div>
<div class="spec-row"><span class="spec-label">File Size:</span><span class="spec-value">${specs.fileSize}</span></div>
`;
}
// Video Playback Controls
function syncPlayback() {
originalVideo.currentTime = 0;
optimizedVideo.currentTime = 0;
originalVideo.play();
optimizedVideo.play();
}
function pauseAll() {
originalVideo.pause();
optimizedVideo.pause();
}
// Mute Controls
function toggleMute(type) {
const video = type === 'original' ? originalVideo : optimizedVideo;
const button = document.getElementById(type === 'original' ? 'muteOriginal' : 'muteOptimized');
video.muted = !video.muted;
if (video.muted) {
button.textContent = '🔇 Unmute';
button.classList.add('muted');
} else {
button.textContent = '🔊 Mute';
button.classList.remove('muted');
}
}
// 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;
aspectRatioChanged = false;
fileInput.value = '';
platformSelect.value = '';
aspectRatioSelect.value = '';
bitrateInput.value = '';
formatInfo.innerHTML = '';
// Reset display
uploadSection.style.display = 'block';
configSection.style.display = 'none';
comparisonSection.style.display = 'none';
}