Files
video-edit/templates/index.html
Mike Johnston fa287261d0
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m9s
prevent cache
2026-02-09 22:20:16 -05:00

1344 lines
48 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 auto;
text-align: center;
position: relative;
display: flex;
justify-content: center;
}
.video-wrapper {
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;
}
.trim-buttons {
display: flex;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
.trim-btn {
flex: 1;
min-width: 200px;
padding: 12px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.trim-btn:hover {
background: #0056b3;
transform: translateY(-1px);
}
.trim-btn:active {
transform: translateY(0);
}
.quick-actions {
display: flex;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
.quick-action-btn {
flex: 1;
min-width: 150px;
padding: 10px 16px;
background: white;
color: #667eea;
border: 2px solid #667eea;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.quick-action-btn:hover {
background: #667eea;
color: white;
transform: translateY(-1px);
}
.quick-action-btn.active {
background: #667eea;
color: white;
}
.current-time-display {
background: white;
padding: 10px 15px;
border-radius: 6px;
text-align: center;
font-size: 1.1em;
color: #667eea;
font-weight: 600;
border: 2px solid #e9ecef;
}
.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;
}
.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">
<div class="video-wrapper">
<video id="video-preview" controls></video>
<canvas id="crop-canvas" class="crop-canvas"></canvas>
</div>
</div>
<div class="video-info" id="video-info"></div>
<!-- Trim Controls -->
<div class="form-group">
<label>⏱️ Trim Video</label>
<div class="current-time-display">
Video Position: <span id="current-time-display">0:00</span>
</div>
<div class="trim-buttons">
<button class="trim-btn" id="set-start-btn">
⏮️ Set Start Here
</button>
<button class="trim-btn" id="set-end-btn">
⏭️ Set End Here
</button>
</div>
<p style="font-size: 0.9em; color: #6c757d; text-align: center; margin: 10px 0;">
💡 Play the video and click the buttons to set trim points at the current playback position
</p>
<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>
<!-- Crop Controls -->
<div class="form-group">
<label>✂️ Crop Video</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>
<!-- 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>
<!-- Compression Quality Controls -->
<div class="form-group">
<label>💾 Compression Quality</label>
<div class="scale-slider-container">
<div class="scale-display">
<span>Quality: <span class="scale-value" id="quality-value-display">High</span></span>
<span style="color: #6c757d;">CRF: <strong id="crf-value-display">23</strong></span>
</div>
<input type="range" class="scale-slider" id="quality-slider" min="18" max="42" value="23" step="1">
<div class="resolution-preview">
<p style="margin: 0;">💡 <strong>Lower CRF</strong> = Better quality, larger file. <strong>Higher CRF</strong> = More compressed, smaller file.</p>
</div>
<div class="preset-resolutions" style="margin-top: 15px;">
<button class="preset-btn" data-quality="18">Overkill</button>
<button class="preset-btn" data-quality="21">Quality</button>
<button class="preset-btn" data-quality="23">Balanced</button>
<button class="preset-btn" data-quality="26">Compressed</button>
<button class="preset-btn" data-quality="30">Small File</button>
<button class="preset-btn" data-quality="40">Dog 💩</button>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="form-group">
<label>⚡ Quick Actions</label>
<div class="quick-actions">
<button class="quick-action-btn" id="mute-audio-btn" title="Remove audio track from video">
🔇 Mute Audio
</button>
<button class="quick-action-btn" id="rotate-btn" title="Rotate video 90° clockwise">
🔄 Rotate 90°
</button>
</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="video-preview">
<div class="video-wrapper">
<video id="processed-video-preview" controls style="max-width: 100%; max-height: 500px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);"></video>
</div>
</div>
<div class="button-group">
<button class="btn" id="download-btn">⬇️ Download Video</button>
<button class="btn btn-secondary" id="back-to-edit-btn">✏️ Make More Changes</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 scalePercentage = 100;
let rotationDegrees = 0;
let muteAudio = false;
let compressionQuality = 23;
let isDraggingHandle = false;
let isDraggingBox = false;
let activeHandle = null;
let dragOffsetX = 0;
let dragOffsetY = 0;
const handleSize = 12;
// 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 videoPreview = document.getElementById('video-preview');
const currentTimeDisplay = document.getElementById('current-time-display');
const setStartBtn = document.getElementById('set-start-btn');
const setEndBtn = document.getElementById('set-end-btn');
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>
<div style="margin-top: 15px; text-align: center;">
<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 to select • Drag corners to resize • Drag center to move</p>
</div>
`;
// Set default values
document.getElementById('end-time').value = videoInfo.duration.toFixed(2);
scalePercentage = 100;
scaleSlider.value = 100;
updateScaleDisplay();
// Setup canvas for cropping
setupCropCanvas();
// Setup crop mode button (now that it exists)
setupCropButton();
// 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;
}
// Setup crop button event listener
function setupCropButton() {
const cropModeBtn = document.getElementById('crop-mode-btn');
const cropHint = document.getElementById('crop-hint');
cropModeBtn.addEventListener('click', () => {
cropMode = !cropMode;
if (cropMode) {
cropCanvas.classList.add('active');
cropModeBtn.classList.add('active');
cropModeBtn.textContent = '✅ Done with Crop';
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();
cropRect = null;
}
});
}
// Canvas drawing for crop selection
let isDrawing = false;
let startX, startY;
cropCanvas.addEventListener('mousedown', (e) => {
if (!cropMode) return;
const rect = cropCanvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Check if clicking on a handle
if (cropRect) {
const handle = getHandleAtPosition(mouseX, mouseY);
if (handle) {
isDraggingHandle = true;
activeHandle = handle;
return;
}
// Check if clicking inside the box to move it
if (isInsideRect(mouseX, mouseY)) {
isDraggingBox = true;
dragOffsetX = mouseX - cropRect.x;
dragOffsetY = mouseY - cropRect.y;
cropCanvas.style.cursor = 'move';
return;
}
}
// Start new selection
isDrawing = true;
cropRect = null;
startX = mouseX;
startY = mouseY;
});
cropCanvas.addEventListener('mousemove', (e) => {
if (!cropMode) return;
const rect = cropCanvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Update cursor based on position
if (!isDrawing && !isDraggingHandle && !isDraggingBox && cropRect) {
const handle = getHandleAtPosition(mouseX, mouseY);
if (handle) {
cropCanvas.style.cursor = getHandleCursor(handle);
} else if (isInsideRect(mouseX, mouseY)) {
cropCanvas.style.cursor = 'move';
} else {
cropCanvas.style.cursor = 'crosshair';
}
}
// Handle resizing
if (isDraggingHandle && cropRect) {
resizeCropRect(mouseX, mouseY);
updateFormFields();
return;
}
// Handle moving
if (isDraggingBox && cropRect) {
moveCropRect(mouseX, mouseY);
updateFormFields();
return;
}
// Handle initial drawing
if (isDrawing) {
const width = mouseX - startX;
const height = mouseY - startY;
cropRect = {
x: Math.min(startX, mouseX),
y: Math.min(startY, mouseY),
width: Math.abs(width),
height: Math.abs(height)
};
drawCropRect();
}
});
cropCanvas.addEventListener('mouseup', (e) => {
if (!cropMode) return;
if (isDrawing || isDraggingHandle || isDraggingBox) {
updateFormFields();
}
isDrawing = false;
isDraggingHandle = false;
isDraggingBox = false;
activeHandle = null;
// Keep crop mode active and canvas visible
drawCropRect();
});
function drawCropRect() {
clearCropCanvas();
if (!cropRect) return;
const { x, y, width, height } = cropRect;
// 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 resize handles
drawHandle(x, y); // Top-left
drawHandle(x + width, y); // Top-right
drawHandle(x, y + height); // Bottom-left
drawHandle(x + width, y + height); // Bottom-right
// Calculate actual video dimensions for display
const video = document.getElementById('video-preview');
const scaleX = videoInfo.width / video.offsetWidth;
const scaleY = videoInfo.height / video.offsetHeight;
const actualWidth = Math.abs(Math.round(width * scaleX));
const actualHeight = Math.abs(Math.round(height * scaleY));
// Draw dimensions text (showing actual video pixels)
cropCtx.fillStyle = '#667eea';
cropCtx.font = 'bold 14px sans-serif';
const text = `${actualWidth} × ${actualHeight}`;
const textY = y > 25 ? y - 10 : y + height + 20;
cropCtx.fillText(text, x + 5, textY);
}
function drawHandle(x, y) {
cropCtx.fillStyle = '#ffffff';
cropCtx.strokeStyle = '#667eea';
cropCtx.lineWidth = 2;
const offset = handleSize / 2;
cropCtx.fillRect(x - offset, y - offset, handleSize, handleSize);
cropCtx.strokeRect(x - offset, y - offset, handleSize, handleSize);
}
function getHandleAtPosition(x, y) {
if (!cropRect) return null;
const handles = [
{ name: 'tl', x: cropRect.x, y: cropRect.y },
{ name: 'tr', x: cropRect.x + cropRect.width, y: cropRect.y },
{ name: 'bl', x: cropRect.x, y: cropRect.y + cropRect.height },
{ name: 'br', x: cropRect.x + cropRect.width, y: cropRect.y + cropRect.height }
];
for (const handle of handles) {
if (Math.abs(x - handle.x) <= handleSize && Math.abs(y - handle.y) <= handleSize) {
return handle.name;
}
}
return null;
}
function getHandleCursor(handle) {
const cursors = {
'tl': 'nw-resize',
'tr': 'ne-resize',
'bl': 'sw-resize',
'br': 'se-resize'
};
return cursors[handle] || 'default';
}
function isInsideRect(x, y) {
if (!cropRect) return false;
return x >= cropRect.x && x <= cropRect.x + cropRect.width &&
y >= cropRect.y && y <= cropRect.y + cropRect.height;
}
function resizeCropRect(mouseX, mouseY) {
const minSize = 20;
switch (activeHandle) {
case 'tl':
const newWidth = cropRect.x + cropRect.width - mouseX;
const newHeight = cropRect.y + cropRect.height - mouseY;
if (newWidth > minSize) {
cropRect.width = newWidth;
cropRect.x = mouseX;
}
if (newHeight > minSize) {
cropRect.height = newHeight;
cropRect.y = mouseY;
}
break;
case 'tr':
const widthTR = mouseX - cropRect.x;
const heightTR = cropRect.y + cropRect.height - mouseY;
if (widthTR > minSize) cropRect.width = widthTR;
if (heightTR > minSize) {
cropRect.height = heightTR;
cropRect.y = mouseY;
}
break;
case 'bl':
const widthBL = cropRect.x + cropRect.width - mouseX;
const heightBL = mouseY - cropRect.y;
if (widthBL > minSize) {
cropRect.width = widthBL;
cropRect.x = mouseX;
}
if (heightBL > minSize) cropRect.height = heightBL;
break;
case 'br':
const widthBR = mouseX - cropRect.x;
const heightBR = mouseY - cropRect.y;
if (widthBR > minSize) cropRect.width = widthBR;
if (heightBR > minSize) cropRect.height = heightBR;
break;
}
// Constrain to canvas bounds
cropRect.x = Math.max(0, Math.min(cropRect.x, cropCanvas.width - cropRect.width));
cropRect.y = Math.max(0, Math.min(cropRect.y, cropCanvas.height - cropRect.height));
cropRect.width = Math.min(cropRect.width, cropCanvas.width - cropRect.x);
cropRect.height = Math.min(cropRect.height, cropCanvas.height - cropRect.y);
drawCropRect();
}
function moveCropRect(mouseX, mouseY) {
const newX = mouseX - dragOffsetX;
const newY = mouseY - dragOffsetY;
// Constrain to canvas bounds
cropRect.x = Math.max(0, Math.min(newX, cropCanvas.width - cropRect.width));
cropRect.y = Math.max(0, Math.min(newY, cropCanvas.height - cropRect.height));
drawCropRect();
}
function updateFormFields() {
if (!cropRect) return;
// 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(cropRect.x * scaleX);
const actualY = Math.round(cropRect.y * scaleY);
const actualWidth = Math.round(cropRect.width * scaleX);
const actualHeight = Math.round(cropRect.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;
// Update scale display since crop changed
updateScaleDisplay();
}
function clearCropCanvas() {
cropCtx.clearRect(0, 0, cropCanvas.width, cropCanvas.height);
}
// Update current time display
videoPreview.addEventListener('timeupdate', () => {
const currentTime = videoPreview.currentTime;
const minutes = Math.floor(currentTime / 60);
const seconds = Math.floor(currentTime % 60);
const milliseconds = Math.floor((currentTime % 1) * 10);
currentTimeDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds}`;
});
// Quick actions
document.getElementById('mute-audio-btn').addEventListener('click', function() {
muteAudio = !muteAudio;
if (muteAudio) {
this.classList.add('active');
this.textContent = '🔇 Audio Muted';
} else {
this.classList.remove('active');
this.textContent = '🔇 Mute Audio';
}
});
document.getElementById('rotate-btn').addEventListener('click', function() {
rotationDegrees = (rotationDegrees + 90) % 360;
if (rotationDegrees === 0) {
this.classList.remove('active');
this.textContent = '🔄 Rotate 90°';
} else {
this.classList.add('active');
this.textContent = `🔄 Rotated ${rotationDegrees}°`;
}
});
// Set start time button
setStartBtn.addEventListener('click', () => {
const currentTime = videoPreview.currentTime;
document.getElementById('start-time').value = currentTime.toFixed(1);
// Flash button feedback
setStartBtn.style.background = '#28a745';
setTimeout(() => {
setStartBtn.style.background = '#007bff';
}, 200);
});
// Set end time button
setEndBtn.addEventListener('click', () => {
const currentTime = videoPreview.currentTime;
document.getElementById('end-time').value = currentTime.toFixed(1);
// Flash button feedback
setEndBtn.style.background = '#28a745';
setTimeout(() => {
setEndBtn.style.background = '#007bff';
}, 200);
});
// Scale slider
const scaleSlider = document.getElementById('scale-slider');
const scaleValueDisplay = document.getElementById('scale-value-display');
const outputResolution = document.getElementById('output-resolution');
const qualitySlider = document.getElementById('quality-slider');
const qualityValueDisplay = document.getElementById('quality-value-display');
const crfValueDisplay = document.getElementById('crf-value-display');
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;
// Round to nearest even number for H.264 compatibility
const outputWidth = Math.round((cropWidth * scalePercentage) / 100 / 2) * 2;
const outputHeight = Math.round((cropHeight * scalePercentage) / 100 / 2) * 2;
outputResolution.textContent = `${outputWidth}x${outputHeight}`;
}
scaleSlider.addEventListener('input', updateScaleDisplay);
// Quality slider
function updateQualityDisplay() {
compressionQuality = parseInt(qualitySlider.value);
crfValueDisplay.textContent = compressionQuality;
// Update quality label
let qualityLabel = 'Balanced';
if (compressionQuality <= 19) qualityLabel = 'Overkill';
else if (compressionQuality <= 22) qualityLabel = 'Quality';
else if (compressionQuality <= 25) qualityLabel = 'Balanced';
else if (compressionQuality <= 28) qualityLabel = 'Compressed';
else if (compressionQuality <= 32) qualityLabel = 'Small File';
else qualityLabel = 'Dog 💩';
qualityValueDisplay.textContent = qualityLabel;
}
qualitySlider.addEventListener('input', updateQualityDisplay);
updateQualityDisplay();
// Scale presets
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const scale = btn.dataset.scale;
const quality = btn.dataset.quality;
if (scale) {
scaleSlider.value = scale;
updateScaleDisplay();
}
if (quality) {
qualitySlider.value = quality;
updateQualityDisplay();
}
});
});
// 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,
rotation: rotationDegrees,
mute_audio: muteAudio,
quality: compressionQuality
};
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');
}
// Load processed video preview with cache-busting
const processedVideoPreview = document.getElementById('processed-video-preview');
processedVideoPreview.src = `/preview/${currentFileId}?t=${Date.now()}`;
// 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}`;
});
// Back to edit button
document.getElementById('back-to-edit-btn').addEventListener('click', backToEdit);
// Reset buttons
document.getElementById('reset-btn').addEventListener('click', resetApp);
document.getElementById('new-video-btn').addEventListener('click', resetApp);
function backToEdit() {
// Go back to edit section without resetting values
document.getElementById('download-section').classList.add('hidden');
document.getElementById('edit-section').classList.remove('hidden');
// Clear the processed video preview to free memory
const processedVideoPreview = document.getElementById('processed-video-preview');
processedVideoPreview.src = '';
}
async function resetApp() {
if (currentFileId) {
await fetch(`/cleanup/${currentFileId}`, { method: 'DELETE' });
}
currentFileId = null;
videoInfo = null;
cropMode = false;
cropRect = null;
rotationDegrees = 0;
muteAudio = false;
scalePercentage = 100;
compressionQuality = 23;
videoPreview.src = '';;
videoInput.value = '';
clearCropCanvas();
cropCanvas.classList.remove('active');
// Reset all form inputs
document.getElementById('start-time').value = '0';
document.getElementById('end-time').value = '0';
document.getElementById('crop-x').value = '0';
document.getElementById('crop-y').value = '0';
document.getElementById('crop-width').value = '';
document.getElementById('crop-height').value = '';
document.getElementById('scale-slider').value = '100';
document.getElementById('scale-value-display').textContent = '100%';
document.getElementById('output-resolution').textContent = '--';
document.getElementById('current-time-display').textContent = '0:00';
document.getElementById('quality-slider').value = '23';
updateQualityDisplay();
// Reset quick actions
document.getElementById('mute-audio-btn').classList.remove('active');
document.getElementById('mute-audio-btn').textContent = '🔇 Mute Audio';
document.getElementById('rotate-btn').classList.remove('active');
document.getElementById('rotate-btn').textContent = '🔄 Rotate 90°';
// Clear any alerts
document.getElementById('alert-container').innerHTML = '';
document.getElementById('upload-section').classList.remove('hidden');
document.getElementById('edit-section').classList.add('hidden');
document.getElementById('download-section').classList.add('hidden');
}
</script>
</body>
</html>