timeline features
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 1m13s

This commit is contained in:
2026-02-09 18:27:20 -05:00
parent 587688e5f8
commit cbd5de8cb3

View File

@@ -131,12 +131,20 @@
.video-preview video { .video-preview video {
max-width: 100%; max-width: 100%;
max-height: 400px; max-height: 500px;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
display: block; display: block;
} }
.video-controls {
margin: 15px 0;
padding: 15px;
background: white;
border-radius: 8px;
text-align: center;
}
.crop-canvas { .crop-canvas {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -151,7 +159,6 @@
} }
.crop-mode-btn { .crop-mode-btn {
margin: 10px 0;
padding: 10px 20px; padding: 10px 20px;
background: #28a745; background: #28a745;
color: white; color: white;
@@ -177,6 +184,112 @@
font-weight: 600; font-weight: 600;
} }
.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 { .video-info {
background: white; background: white;
padding: 15px; padding: 15px;
@@ -357,14 +470,37 @@
<video id="video-preview" controls></video> <video id="video-preview" controls></video>
<canvas id="crop-canvas" class="crop-canvas"></canvas> <canvas id="crop-canvas" class="crop-canvas"></canvas>
</div> </div>
<div class="video-controls">
<button class="crop-mode-btn" id="crop-mode-btn">🎯 Click to Draw Crop Area</button> <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> <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> <div class="video-info" id="video-info"></div>
<!-- Trim Controls --> <!-- Trim Controls -->
<div class="form-group"> <div class="form-group">
<label>⏱️ Trim Video</label> <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 class="form-row">
<div> <div>
<label style="font-weight: normal;">Start Time (seconds)</label> <label style="font-weight: normal;">Start Time (seconds)</label>
@@ -454,6 +590,11 @@
let cropStartX = 0; let cropStartX = 0;
let cropStartY = 0; let cropStartY = 0;
let cropRect = null; let cropRect = null;
let trimMode = false;
let isDraggingStart = false;
let isDraggingEnd = false;
let timelineStartPos = 0;
let timelineEndPos = 100;
// Upload area handling // Upload area handling
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
@@ -464,6 +605,12 @@
const cropCtx = cropCanvas.getContext('2d'); const cropCtx = cropCanvas.getContext('2d');
const cropModeBtn = document.getElementById('crop-mode-btn'); const cropModeBtn = document.getElementById('crop-mode-btn');
const cropHint = document.getElementById('crop-hint'); 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('click', () => videoInput.click());
@@ -667,6 +814,107 @@
cropCtx.clearRect(0, 0, cropCanvas.width, cropCanvas.height); 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();
});
// Resolution presets // Resolution presets
document.querySelectorAll('.preset-btn').forEach(btn => { document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@@ -760,6 +1008,7 @@
videoInfo = null; videoInfo = null;
cropMode = false; cropMode = false;
cropRect = null; cropRect = null;
trimMode = false;
document.getElementById('video-preview').src = ''; document.getElementById('video-preview').src = '';
videoInput.value = ''; videoInput.value = '';
@@ -768,6 +1017,9 @@
cropModeBtn.classList.remove('active'); cropModeBtn.classList.remove('active');
cropModeBtn.textContent = '🎯 Click to Draw Crop Area'; cropModeBtn.textContent = '🎯 Click to Draw Crop Area';
cropHint.style.display = 'none'; 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('upload-section').classList.remove('hidden');
document.getElementById('edit-section').classList.add('hidden'); document.getElementById('edit-section').classList.add('hidden');