Files
video-edit/templates/index.html
Mike Johnston e8eb1fe472
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m9s
percent based resolution
2026-02-09 18:33:21 -05:00

1109 lines
38 KiB
HTML
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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Video Editor</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 40px;
}
.section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
border: 2px solid #e9ecef;
}
.section.hidden {
display: none;
}
.section h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.5em;
}
.upload-area {
border: 3px dashed #667eea;
border-radius: 8px;
padding: 40px;
text-align: center;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
background: #f8f9fa;
border-color: #764ba2;
}
.upload-area.dragover {
background: #e7f3ff;
border-color: #667eea;
}
.upload-icon {
font-size: 3em;
margin-bottom: 15px;
}
input[type="file"] {
display: none;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 6px;
font-size: 1em;
cursor: pointer;
transition: transform 0.2s;
font-weight: 600;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
background: #6c757d;
}
.video-preview {
margin: 20px 0;
text-align: center;
position: relative;
display: inline-block;
}
.video-preview video {
max-width: 100%;
max-height: 500px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
display: block;
}
.video-controls {
margin: 15px 0;
padding: 15px;
background: white;
border-radius: 8px;
text-align: center;
}
.crop-canvas {
position: absolute;
top: 0;
left: 0;
cursor: crosshair;
border-radius: 8px;
display: none;
}
.crop-canvas.active {
display: block;
}
.crop-mode-btn {
padding: 10px 20px;
background: #28a745;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
.crop-mode-btn:hover {
background: #218838;
}
.crop-mode-btn.active {
background: #dc3545;
}
.crop-hint {
color: #28a745;
font-size: 0.9em;
margin: 5px 0;
font-weight: 600;
}
.scale-slider-container {
margin: 15px 0;
}
.scale-slider {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e9ecef;
outline: none;
-webkit-appearance: none;
}
.scale-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.scale-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.scale-display {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.scale-value {
font-size: 1.2em;
font-weight: 600;
color: #667eea;
}
.resolution-preview {
background: white;
padding: 10px;
border-radius: 6px;
margin-top: 10px;
font-size: 0.9em;
color: #495057;
}
.timeline-container {
margin: 15px 0;
padding: 20px;
background: white;
border-radius: 8px;
}
.timeline-label {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 0.9em;
color: #495057;
}
.timeline-wrapper {
position: relative;
height: 60px;
background: #f8f9fa;
border-radius: 8px;
margin: 10px 0;
cursor: pointer;
border: 2px solid #e9ecef;
}
.timeline-track {
position: absolute;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
opacity: 0.3;
border-radius: 6px;
}
.timeline-handle {
position: absolute;
width: 4px;
height: 100%;
background: #667eea;
cursor: ew-resize;
z-index: 2;
}
.timeline-handle::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 30px;
background: #667eea;
border-radius: 4px;
border: 2px solid white;
}
.timeline-handle.start::after {
content: '⏴';
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.timeline-handle.end::after {
content: '⏵';
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
}
.timeline-time {
position: absolute;
top: -25px;
background: #667eea;
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
white-space: nowrap;
transform: translateX(-50%);
}
.trim-mode-btn {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
margin-right: 10px;
}
.trim-mode-btn:hover {
background: #0056b3;
}
.trim-mode-btn.active {
background: #dc3545;
}
.video-info {
background: white;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
.video-info p {
margin: 5px 0;
color: #495057;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #495057;
font-weight: 600;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.progress-container {
margin: 20px 0;
display: none;
}
.progress-bar {
width: 100%;
height: 30px;
background: #e9ecef;
border-radius: 15px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
.alert {
padding: 15px;
border-radius: 6px;
margin: 15px 0;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.range-container {
margin: 20px 0;
}
.range-slider {
width: 100%;
}
.crop-controls {
background: white;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.preset-resolutions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 15px;
}
.preset-btn {
padding: 8px 16px;
background: white;
border: 2px solid #667eea;
color: #667eea;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
}
.preset-btn:hover,
.preset-btn.active {
background: #667eea;
color: white;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 20px;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎬 Simple Video Editor</h1>
<p>Trim, crop, and convert your videos with ease</p>
</div>
<div class="content">
<!-- Step 1: Upload Video -->
<div class="section" id="upload-section">
<h2>1. Select Video</h2>
<div class="upload-area" id="upload-area">
<div class="upload-icon">📹</div>
<h3>Drag and drop your video here</h3>
<p>or click to browse</p>
<p style="margin-top: 10px; color: #6c757d; font-size: 0.9em;">
Supported formats: MP4, AVI, MOV, MKV, WMV, FLV, WebM
</p>
<input type="file" id="video-input" accept="video/*">
</div>
<div class="loader" id="upload-loader"></div>
</div>
<!-- Step 2: Edit Video -->
<div class="section hidden" id="edit-section">
<h2>2. Edit Video</h2>
<div class="video-preview">
<video id="video-preview" controls></video>
<canvas id="crop-canvas" class="crop-canvas"></canvas>
</div>
<div class="video-controls">
<button class="crop-mode-btn" id="crop-mode-btn">🎯 Click to Draw Crop Area</button>
<p class="crop-hint" id="crop-hint" style="display: none;">Click and drag on the video to select crop area</p>
</div>
<div class="video-info" id="video-info"></div>
<!-- Trim Controls -->
<div class="form-group">
<label>⏱️ Trim Video</label>
<div class="timeline-container">
<button class="trim-mode-btn" id="trim-mode-btn">✂️ Use Timeline to Select Trim</button>
<div id="timeline-scrubber" style="display: none;">
<div class="timeline-label">
<span>Start: <strong id="timeline-start-display">0.0s</strong></span>
<span>Duration: <strong id="timeline-duration-display">0.0s</strong></span>
<span>End: <strong id="timeline-end-display">0.0s</strong></span>
</div>
<div class="timeline-wrapper" id="timeline-wrapper">
<div class="timeline-track" id="timeline-track"></div>
<div class="timeline-handle start" id="timeline-start-handle">
<div class="timeline-time" id="start-time-label">0.0s</div>
</div>
<div class="timeline-handle end" id="timeline-end-handle">
<div class="timeline-time" id="end-time-label">0.0s</div>
</div>
</div>
<p style="font-size: 0.9em; color: #6c757d; margin-top: 10px; text-align: center;">Drag the handles to set start and end times</p>
</div>
</div>
<div class="form-row">
<div>
<label style="font-weight: normal;">Start Time (seconds)</label>
<input type="number" id="start-time" value="0" min="0" step="0.1">
</div>
<div>
<label style="font-weight: normal;">End Time (seconds)</label>
<input type="number" id="end-time" min="0" step="0.1">
</div>
</div>
</div>
<!-- Scale Controls -->
<div class="form-group">
<label>📐 Output Scale</label>
<div class="scale-slider-container">
<div class="scale-display">
<span>Scale: <span class="scale-value" id="scale-value-display">100%</span></span>
<span id="resolution-info" style="color: #6c757d;">Output: <strong id="output-resolution">--</strong></span>
</div>
<input type="range" class="scale-slider" id="scale-slider" min="10" max="100" value="100" step="5">
<div class="resolution-preview" id="resolution-preview">
<p style="margin: 0;">💡 <strong>How it works:</strong> Scale is applied to the cropped area (or original if no crop). At 50%, a 200x400 crop becomes 100x200.</p>
</div>
<div class="preset-resolutions" style="margin-top: 15px;">
<button class="preset-btn" data-scale="100">100% (Original)</button>
<button class="preset-btn" data-scale="75">75%</button>
<button class="preset-btn" data-scale="50">50%</button>
<button class="preset-btn" data-scale="25">25%</button>
</div>
</div>
</div>
<!-- Crop Controls -->
<div class="form-group">
<label>✂️ Crop Video (optional)</label>
<div class="crop-controls">
<div class="form-row">
<div>
<label style="font-weight: normal;">X Position</label>
<input type="number" id="crop-x" value="0" min="0">
</div>
<div>
<label style="font-weight: normal;">Y Position</label>
<input type="number" id="crop-y" value="0" min="0">
</div>
</div>
<div class="form-row" style="margin-top: 10px;">
<div>
<label style="font-weight: normal;">Crop Width</label>
<input type="number" id="crop-width" placeholder="Full width" min="0">
</div>
<div>
<label style="font-weight: normal;">Crop Height</label>
<input type="number" id="crop-height" placeholder="Full height" min="0">
</div>
</div>
</div>
</div>
<div class="button-group">
<button class="btn" id="process-btn">🎬 Process Video</button>
<button class="btn btn-secondary" id="reset-btn">🔄 Start Over</button>
</div>
<div class="loader" id="process-loader"></div>
<div id="alert-container"></div>
</div>
<!-- Step 3: Download -->
<div class="section hidden" id="download-section">
<h2>3. Download Processed Video</h2>
<p style="margin-bottom: 20px;">Your video has been processed successfully!</p>
<div class="button-group">
<button class="btn" id="download-btn">⬇️ Download Video</button>
<button class="btn btn-secondary" id="new-video-btn">🎬 Process Another Video</button>
</div>
</div>
</div>
</div>
<script>
let currentFileId = null;
let videoInfo = null;
let cropMode = false;
let cropStartX = 0;
let cropStartY = 0;
let cropRect = null;
let trimMode = false;
let isDraggingStart = false;
let isDraggingEnd = false;
let timelineStartPos = 0;
let timelineEndPos = 100;
let scalePercentage = 100;
// Upload area handling
const uploadArea = document.getElementById('upload-area');
const videoInput = document.getElementById('video-input');
const uploadLoader = document.getElementById('upload-loader');
const processLoader = document.getElementById('process-loader');
const cropCanvas = document.getElementById('crop-canvas');
const cropCtx = cropCanvas.getContext('2d');
const cropModeBtn = document.getElementById('crop-mode-btn');
const cropHint = document.getElementById('crop-hint');
const trimModeBtn = document.getElementById('trim-mode-btn');
const timelineScrubber = document.getElementById('timeline-scrubber');
const timelineWrapper = document.getElementById('timeline-wrapper');
const timelineTrack = document.getElementById('timeline-track');
const startHandle = document.getElementById('timeline-start-handle');
const endHandle = document.getElementById('timeline-end-handle');
uploadArea.addEventListener('click', () => videoInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files[0]);
}
});
videoInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileUpload(e.target.files[0]);
}
});
async function handleFileUpload(file) {
const formData = new FormData();
formData.append('video', file);
uploadLoader.style.display = 'block';
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Upload failed');
}
currentFileId = data.file_id;
videoInfo = data.info;
// Show video preview
const videoPreview = document.getElementById('video-preview');
videoPreview.src = URL.createObjectURL(file);
// Update video info
const videoInfoDiv = document.getElementById('video-info');
videoInfoDiv.innerHTML = `
<p><strong>File:</strong> ${data.original_name}</p>
<p><strong>Resolution:</strong> ${videoInfo.width}x${videoInfo.height}</p>
<p><strong>Duration:</strong> ${videoInfo.duration.toFixed(2)} seconds</p>
<p><strong>Codec:</strong> ${videoInfo.codec}</p>
`;
// Set default values
document.getElementById('end-time').value = videoInfo.duration.toFixed(2);
scalePercentage = 100;
scaleSlider.value = 100;
updateScaleDisplay();
// Setup canvas for cropping
setupCropCanvas();
// Show edit section
document.getElementById('upload-section').classList.add('hidden');
document.getElementById('edit-section').classList.remove('hidden');
} catch (error) {
alert('Error uploading video: ' + error.message);
} finally {
uploadLoader.style.display = 'none';
}
}
// Setup crop canvas
function setupCropCanvas() {
const video = document.getElementById('video-preview');
// Wait for video to load metadata
video.addEventListener('loadedmetadata', () => {
resizeCropCanvas();
});
// Resize canvas when window resizes
window.addEventListener('resize', resizeCropCanvas);
}
function resizeCropCanvas() {
const video = document.getElementById('video-preview');
cropCanvas.width = video.offsetWidth;
cropCanvas.height = video.offsetHeight;
}
// Crop mode toggle
cropModeBtn.addEventListener('click', () => {
cropMode = !cropMode;
if (cropMode) {
cropCanvas.classList.add('active');
cropModeBtn.classList.add('active');
cropModeBtn.textContent = '❌ Cancel Crop Selection';
cropHint.style.display = 'block';
document.getElementById('video-preview').pause();
} else {
cropCanvas.classList.remove('active');
cropModeBtn.classList.remove('active');
cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
cropHint.style.display = 'none';
clearCropCanvas();
}
});
// Canvas drawing for crop selection
let isDrawing = false;
let startX, startY;
cropCanvas.addEventListener('mousedown', (e) => {
if (!cropMode) return;
isDrawing = true;
const rect = cropCanvas.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
});
cropCanvas.addEventListener('mousemove', (e) => {
if (!cropMode || !isDrawing) return;
const rect = cropCanvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
drawCropRect(startX, startY, currentX - startX, currentY - startY);
});
cropCanvas.addEventListener('mouseup', (e) => {
if (!cropMode || !isDrawing) return;
isDrawing = false;
const rect = cropCanvas.getBoundingClientRect();
const endX = e.clientX - rect.left;
const endY = e.clientY - rect.top;
const width = Math.abs(endX - startX);
const height = Math.abs(endY - startY);
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
// Calculate actual video coordinates
const video = document.getElementById('video-preview');
const scaleX = videoInfo.width / video.offsetWidth;
const scaleY = videoInfo.height / video.offsetHeight;
const actualX = Math.round(x * scaleX);
const actualY = Math.round(y * scaleY);
const actualWidth = Math.round(width * scaleX);
const actualHeight = Math.round(height * scaleY);
// Update form fields
document.getElementById('crop-x').value = actualX;
document.getElementById('crop-y').value = actualY;
document.getElementById('crop-width').value = actualWidth;
document.getElementById('crop-height').value = actualHeight;
// Exit crop mode
cropMode = false;
cropModeBtn.classList.remove('active');
cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
cropHint.style.display = 'none';
// Keep the rectangle visible for a moment
setTimeout(() => {
cropCanvas.classList.remove('active');
}, 500);
});
function drawCropRect(x, y, width, height) {
clearCropCanvas();
// Draw semi-transparent overlay
cropCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
cropCtx.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
// Clear the selected area
cropCtx.clearRect(x, y, width, height);
// Draw border around selection
cropCtx.strokeStyle = '#667eea';
cropCtx.lineWidth = 3;
cropCtx.strokeRect(x, y, width, height);
// Draw dimensions text
cropCtx.fillStyle = '#667eea';
cropCtx.font = 'bold 14px sans-serif';
const text = `${Math.abs(Math.round(width))} × ${Math.abs(Math.round(height))}`;
cropCtx.fillText(text, x + 5, y - 5);
}
function clearCropCanvas() {
cropCtx.clearRect(0, 0, cropCanvas.width, cropCanvas.height);
}
// Timeline trim mode
trimModeBtn.addEventListener('click', () => {
trimMode = !trimMode;
if (trimMode) {
timelineScrubber.style.display = 'block';
trimModeBtn.classList.add('active');
trimModeBtn.textContent = '❌ Cancel Timeline Selection';
initializeTimeline();
} else {
timelineScrubber.style.display = 'none';
trimModeBtn.classList.remove('active');
trimModeBtn.textContent = '✂️ Use Timeline to Select Trim';
}
});
function initializeTimeline() {
timelineStartPos = 0;
timelineEndPos = 100;
updateTimeline();
}
function updateTimeline() {
// Update track position and width
timelineTrack.style.left = timelineStartPos + '%';
timelineTrack.style.width = (timelineEndPos - timelineStartPos) + '%';
// Update handle positions
startHandle.style.left = timelineStartPos + '%';
endHandle.style.left = timelineEndPos + '%';
// Calculate actual times
const duration = videoInfo.duration;
const startTime = (timelineStartPos / 100) * duration;
const endTime = (timelineEndPos / 100) * duration;
const selectedDuration = endTime - startTime;
// Update labels
document.getElementById('start-time-label').textContent = startTime.toFixed(1) + 's';
document.getElementById('end-time-label').textContent = endTime.toFixed(1) + 's';
document.getElementById('timeline-start-display').textContent = startTime.toFixed(1) + 's';
document.getElementById('timeline-end-display').textContent = endTime.toFixed(1) + 's';
document.getElementById('timeline-duration-display').textContent = selectedDuration.toFixed(1) + 's';
// Update form inputs
document.getElementById('start-time').value = startTime.toFixed(1);
document.getElementById('end-time').value = endTime.toFixed(1);
}
// Timeline dragging
startHandle.addEventListener('mousedown', (e) => {
e.stopPropagation();
isDraggingStart = true;
});
endHandle.addEventListener('mousedown', (e) => {
e.stopPropagation();
isDraggingEnd = true;
});
document.addEventListener('mousemove', (e) => {
if (!isDraggingStart && !isDraggingEnd) return;
const rect = timelineWrapper.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100));
if (isDraggingStart) {
timelineStartPos = Math.min(percentage, timelineEndPos - 1);
} else if (isDraggingEnd) {
timelineEndPos = Math.max(percentage, timelineStartPos + 1);
}
updateTimeline();
});
document.addEventListener('mouseup', () => {
isDraggingStart = false;
isDraggingEnd = false;
});
// Click on timeline to set range
timelineWrapper.addEventListener('click', (e) => {
if (e.target.classList.contains('timeline-handle')) return;
const rect = timelineWrapper.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
// Find closest handle and move it
const distToStart = Math.abs(percentage - timelineStartPos);
const distToEnd = Math.abs(percentage - timelineEndPos);
if (distToStart < distToEnd) {
timelineStartPos = Math.min(percentage, timelineEndPos - 1);
} else {
timelineEndPos = Math.max(percentage, timelineStartPos + 1);
}
updateTimeline();
});
// Scale slider
const scaleSlider = document.getElementById('scale-slider');
const scaleValueDisplay = document.getElementById('scale-value-display');
const outputResolution = document.getElementById('output-resolution');
function updateScaleDisplay() {
scalePercentage = parseInt(scaleSlider.value);
scaleValueDisplay.textContent = scalePercentage + '%';
// Calculate output resolution
const cropWidth = parseInt(document.getElementById('crop-width').value) || videoInfo.width;
const cropHeight = parseInt(document.getElementById('crop-height').value) || videoInfo.height;
const outputWidth = Math.round((cropWidth * scalePercentage) / 100);
const outputHeight = Math.round((cropHeight * scalePercentage) / 100);
outputResolution.textContent = `${outputWidth}x${outputHeight}`;
}
scaleSlider.addEventListener('input', updateScaleDisplay);
// Scale presets
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const scale = btn.dataset.scale;
if (scale) {
scaleSlider.value = scale;
updateScaleDisplay();
}
});
});
// Update scale display when crop values change
['crop-width', 'crop-height'].forEach(id => {
document.getElementById(id).addEventListener('input', updateScaleDisplay);
});
// Process video
document.getElementById('process-btn').addEventListener('click', async () => {
const startTime = parseFloat(document.getElementById('start-time').value) || 0;
const endTime = parseFloat(document.getElementById('end-time').value) || videoInfo.duration;
const cropX = parseInt(document.getElementById('crop-x').value) || 0;
const cropY = parseInt(document.getElementById('crop-y').value) || 0;
const cropWidth = parseInt(document.getElementById('crop-width').value) || 0;
const cropHeight = parseInt(document.getElementById('crop-height').value) || 0;
const payload = {
file_id: currentFileId,
start_time: startTime,
end_time: endTime,
scale_percentage: scalePercentage
};
if (cropWidth > 0 && cropHeight > 0) {
payload.crop = {
x: cropX,
y: cropY,
width: cropWidth,
height: cropHeight
};
}
processLoader.style.display = 'block';
document.getElementById('process-btn').disabled = true;
try {
const response = await fetch('/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Processing failed');
}
// Show download section
document.getElementById('edit-section').classList.add('hidden');
document.getElementById('download-section').classList.remove('hidden');
} catch (error) {
const alertContainer = document.getElementById('alert-container');
alertContainer.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`;
} finally {
processLoader.style.display = 'none';
document.getElementById('process-btn').disabled = false;
}
});
// Download video
document.getElementById('download-btn').addEventListener('click', () => {
window.location.href = `/download/${currentFileId}`;
});
// Reset buttons
document.getElementById('reset-btn').addEventListener('click', resetApp);
document.getElementById('new-video-btn').addEventListener('click', resetApp);
async function resetApp() {
if (currentFileId) {
await fetch(`/cleanup/${currentFileId}`, { method: 'DELETE' });
}
currentFileId = null;
videoInfo = null;
cropMode = false;
cropRect = null;
trimMode = false;
document.getElementById('video-preview').src = '';
videoInput.value = '';
clearCropCanvas();
cropCanvas.classList.remove('active');
cropModeBtn.classList.remove('active');
cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
cropHint.style.display = 'none';
timelineScrubber.style.display = 'none';
trimModeBtn.classList.remove('active');
trimModeBtn.textContent = '✂️ Use Timeline to Select Trim';
document.getElementById('upload-section').classList.remove('hidden');
document.getElementById('edit-section').classList.add('hidden');
document.getElementById('download-section').classList.add('hidden');
}
</script>
</body>
</html>