feat: Fix chapters (rename text/name to chapterTitle) and fetch/post the correct object

This commit is contained in:
Yiannis Christodoulou 2025-09-19 12:56:45 +03:00
parent c5edfbefb6
commit e29d364fd3
8 changed files with 104 additions and 474 deletions

View File

@ -15,7 +15,6 @@ const App = () => {
isPlaying, isPlaying,
setIsPlaying, setIsPlaying,
isMuted, isMuted,
thumbnails,
trimStart, trimStart,
trimEnd, trimEnd,
splitPoints, splitPoints,
@ -244,7 +243,6 @@ const App = () => {
<TimelineControls <TimelineControls
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
thumbnails={thumbnails}
trimStart={trimStart} trimStart={trimStart}
trimEnd={trimEnd} trimEnd={trimEnd}
splitPoints={splitPoints} splitPoints={splitPoints}

View File

@ -3,11 +3,9 @@ import '../styles/ClipSegments.css';
export interface Segment { export interface Segment {
id: number; id: number;
name: string; chapterTitle: string;
startTime: number; startTime: number;
endTime: number; endTime: number;
thumbnail: string;
chapterTitle?: string;
} }
interface ClipSegmentsProps { interface ClipSegmentsProps {
@ -48,10 +46,6 @@ const ClipSegments = ({ segments, selectedSegmentId }: ClipSegmentsProps) => {
className={`segment-item ${getSegmentColorClass(index)} ${selectedSegmentId === segment.id ? 'selected' : ''}`} className={`segment-item ${getSegmentColorClass(index)} ${selectedSegmentId === segment.id ? 'selected' : ''}`}
> >
<div className="segment-content"> <div className="segment-content">
<div
className="segment-thumbnail"
style={{ backgroundImage: `url(${segment.thumbnail})` }}
></div>
<div className="segment-info"> <div className="segment-info">
<div className="segment-title"> <div className="segment-title">
{segment.chapterTitle ? ( {segment.chapterTitle ? (

View File

@ -1,6 +1,6 @@
import { useRef, useEffect, useState, useCallback } from 'react'; import { useRef, useEffect, useState, useCallback } from 'react';
import { formatTime, formatDetailedTime } from '../lib/timeUtils'; import { formatTime, formatDetailedTime } from '../lib/timeUtils';
import { generateThumbnail, generateSolidColor } from '../lib/videoUtils'; import { generateSolidColor } from '../lib/videoUtils';
import { Segment } from './ClipSegments'; import { Segment } from './ClipSegments';
import Modal from './Modal'; import Modal from './Modal';
import { autoSaveVideo } from '../services/videoApi'; import { autoSaveVideo } from '../services/videoApi';
@ -38,7 +38,7 @@ interface TimelineControlsProps {
selectedSegmentId?: number | null; selectedSegmentId?: number | null;
onSelectedSegmentChange?: (segmentId: number | null) => void; onSelectedSegmentChange?: (segmentId: number | null) => void;
onSegmentUpdate?: (segmentId: number, updates: Partial<Segment>) => void; onSegmentUpdate?: (segmentId: number, updates: Partial<Segment>) => void;
onChapterSave?: (chapters: { name: string; from: string; to: string }[]) => void; onChapterSave?: (chapters: { chapterTitle: string; from: string; to: string }[]) => void;
onTrimStartChange: (time: number) => void; onTrimStartChange: (time: number) => void;
onTrimEndChange: (time: number) => void; onTrimEndChange: (time: number) => void;
onZoomChange: (level: number) => void; onZoomChange: (level: number) => void;
@ -104,7 +104,6 @@ const constrainTooltipPosition = (positionPercent: number) => {
const TimelineControls = ({ const TimelineControls = ({
currentTime, currentTime,
duration, duration,
thumbnails,
trimStart, trimStart,
trimEnd, trimEnd,
splitPoints, splitPoints,
@ -168,26 +167,41 @@ const TimelineControls = ({
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null); const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
const clipSegmentsRef = useRef(clipSegments); const clipSegmentsRef = useRef(clipSegments);
// Redirect timer refs
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
const redirectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Keep clipSegmentsRef updated // Keep clipSegmentsRef updated
useEffect(() => { useEffect(() => {
clipSegmentsRef.current = clipSegments; clipSegmentsRef.current = clipSegments;
}, [clipSegments]); }, [clipSegments]);
// Function to cancel redirect timers
const cancelRedirect = useCallback(() => {
if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
}
if (redirectTimeoutRef.current) {
clearTimeout(redirectTimeoutRef.current);
redirectTimeoutRef.current = null;
}
logger.debug('Redirect cancelled by user');
}, []);
// Auto-save function // Auto-save function
const performAutoSave = useCallback(async () => { const performAutoSave = useCallback(async () => {
try { try {
setIsAutoSaving(true); setIsAutoSaving(true);
// Format segments data for API request - use ref to get latest segments // Format segments data for API request - use ref to get latest segments
const segments = clipSegmentsRef.current.map((segment) => ({ const chapters = clipSegmentsRef.current.map((chapter) => ({
startTime: formatDetailedTime(segment.startTime), startTime: formatDetailedTime(chapter.startTime),
endTime: formatDetailedTime(segment.endTime), endTime: formatDetailedTime(chapter.endTime),
name: segment.name, chapterTitle: chapter.chapterTitle,
chapterTitle: segment.chapterTitle,
text: segment.chapterTitle,
})); }));
logger.debug('segments', segments); logger.debug('chapters', chapters);
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null; const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
// For testing, use '1234' if no mediaId is available // For testing, use '1234' if no mediaId is available
@ -195,20 +209,22 @@ const TimelineControls = ({
logger.debug('mediaId', finalMediaId); logger.debug('mediaId', finalMediaId);
if (!finalMediaId || segments.length === 0) { if (!finalMediaId || chapters.length === 0) {
logger.debug('No mediaId or segments, skipping auto-save'); logger.debug('No mediaId or segments, skipping auto-save');
setIsAutoSaving(false); setIsAutoSaving(false);
return; return;
} }
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, segments }); logger.debug('Auto-saving segments:', { mediaId: finalMediaId, chapters });
const response = await autoSaveVideo(finalMediaId, { segments }); const response = await autoSaveVideo(finalMediaId, { chapters });
if (response.success) { console.log('response autoSaveVideo edw', response);
if (response.success === true) {
logger.debug('Auto-save successful'); logger.debug('Auto-save successful');
// Format the timestamp for display // Format the timestamp for display
const date = new Date(response.timestamp); const date = new Date(response.updated_at || new Date().toISOString());
const formattedTime = date const formattedTime = date
.toLocaleString('en-US', { .toLocaleString('en-US', {
year: 'numeric', year: 'numeric',
@ -224,10 +240,10 @@ const TimelineControls = ({
setLastAutoSaveTime(formattedTime); setLastAutoSaveTime(formattedTime);
logger.debug('Auto-save successful:', formattedTime); logger.debug('Auto-save successful:', formattedTime);
} else { } else {
logger.error('Auto-save failed:', response.error); logger.error('Auto-save failed: (TimelineControls.tsx)');
} }
} catch (error) { } catch (error) {
logger.error('Auto-save error:', error); logger.error('Auto-save error: (TimelineControls.tsx)', error);
} finally { } finally {
setIsAutoSaving(false); setIsAutoSaving(false);
} }
@ -255,6 +271,7 @@ const TimelineControls = ({
// Update editing title when selected segment changes // Update editing title when selected segment changes
useEffect(() => { useEffect(() => {
console.log('edw selectedSegment', selectedSegment);
if (selectedSegment) { if (selectedSegment) {
setEditingChapterTitle(selectedSegment.chapterTitle || ''); setEditingChapterTitle(selectedSegment.chapterTitle || '');
} else { } else {
@ -274,7 +291,7 @@ const TimelineControls = ({
}; };
// Handle save chapters // Handle save chapters
const handleSaveChapters = () => { /* const handleSaveChapters = () => {
if (!onChapterSave) return; if (!onChapterSave) return;
// Convert segments to chapter format // Convert segments to chapter format
@ -286,10 +303,10 @@ const TimelineControls = ({
onChapterSave(chapters); onChapterSave(chapters);
setChapterHasUnsavedChanges(false); setChapterHasUnsavedChanges(false);
}; }; */
// Helper function for time adjustment buttons to maintain playback state // Helper function for time adjustment buttons to maintain playback state
const handleTimeAdjustment = (offsetSeconds: number) => (e: React.MouseEvent) => { /* const handleTimeAdjustment = (offsetSeconds: number) => (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
// Calculate new time based on offset (positive or negative) // Calculate new time based on offset (positive or negative)
@ -313,7 +330,7 @@ const TimelineControls = ({
videoRef.current.play(); videoRef.current.play();
setIsPlayingSegment(true); setIsPlayingSegment(true);
} }
}; }; */
// Enhanced helper for continuous time adjustment when button is held down // Enhanced helper for continuous time adjustment when button is held down
const handleContinuousTimeAdjustment = (offsetSeconds: number) => { const handleContinuousTimeAdjustment = (offsetSeconds: number) => {
@ -484,7 +501,7 @@ const TimelineControls = ({
const chapters = clipSegments const chapters = clipSegments
.filter((segment) => segment.chapterTitle && segment.chapterTitle.trim()) .filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
.map((segment) => ({ .map((segment) => ({
name: segment.chapterTitle || `Chapter ${segment.id}`, chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`,
from: formatDetailedTime(segment.startTime), from: formatDetailedTime(segment.startTime),
to: formatDetailedTime(segment.endTime), to: formatDetailedTime(segment.endTime),
})); }));
@ -969,12 +986,14 @@ const TimelineControls = ({
const loadSavedSegments = () => { const loadSavedSegments = () => {
// Get savedSegments directly from window.MEDIA_DATA // Get savedSegments directly from window.MEDIA_DATA
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) || null; let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) || null;
console.log('MEDIA_DATA edw1', (window as any).MEDIA_DATA);
console.log('savedData edw1', savedData);
// If no saved segments, use default segments // If no saved segments, use default segments
if (!savedData) { /* if (!savedData) {
logger.debug('No saved segments found in MEDIA_DATA, using default segments'); logger.debug('No saved segments found in MEDIA_DATA, using default segments');
savedData = { savedData = {
segments: [ chapters: [
{ {
startTime: '00:00:00.000', startTime: '00:00:00.000',
endTime: '00:00:10.000', endTime: '00:00:10.000',
@ -993,22 +1012,19 @@ const TimelineControls = ({
], ],
updated_at: '2025-06-24 14:59:14', updated_at: '2025-06-24 14:59:14',
}; };
} } */
logger.debug('Loading saved segments:', savedData);
try { try {
if (savedData && savedData.segments && savedData.segments.length > 0) { if (savedData && savedData.chapters && savedData.chapters.length > 0) {
logger.debug('Found saved segments:', savedData); logger.debug('Found saved segments:', savedData);
console.log('savedData edw', savedData);
// Convert the saved segments to the format expected by the component // Convert the saved segments to the format expected by the component
const convertedSegments: Segment[] = savedData.segments.map((seg: any, index: number) => ({ const convertedSegments: Segment[] = savedData.chapters.map((seg: any , index: number) => ({
id: Date.now() + index, // Generate unique IDs id: Date.now() + index, // Generate unique IDs
name: seg.name || `Segment ${index + 1}`, chapterTitle: seg.chapterTitle || `Chapter ${index + 1}`,
startTime: parseTimeString(seg.startTime), startTime: parseTimeString(seg.startTime),
endTime: parseTimeString(seg.endTime), endTime: parseTimeString(seg.endTime),
thumbnail: '',
chapterTitle: seg.chapterTitle || '', // Preserve chapter title from saved data
})); }));
// Dispatch event to update segments // Dispatch event to update segments
@ -1188,34 +1204,6 @@ const TimelineControls = ({
}; };
}, [duration, trimStart, trimEnd, onTrimStartChange, onTrimEndChange]); }, [duration, trimStart, trimEnd, onTrimStartChange, onTrimEndChange]);
// Render solid color backgrounds evenly spread across timeline
const renderThumbnails = () => {
// Create thumbnail sections even if we don't have actual thumbnail data
const numSections = thumbnails.length || 10; // Default to 10 sections if no thumbnails
return Array.from({ length: numSections }).map((_, index) => {
const segmentDuration = duration / numSections;
const segmentStartTime = index * segmentDuration;
const segmentEndTime = segmentStartTime + segmentDuration;
const midpointTime = (segmentStartTime + segmentEndTime) / 2;
// Get a solid color based on the segment position
const backgroundColor = generateSolidColor(midpointTime, duration);
return (
<div
key={index}
className="timeline-thumbnail"
style={{
width: `${100 / numSections}%`,
backgroundColor: backgroundColor,
// Remove background image and use solid color instead
}}
/>
);
});
};
// Render split points // Render split points
const renderSplitPoints = () => { const renderSplitPoints = () => {
return splitPoints.map((point, index) => { return splitPoints.map((point, index) => {
@ -1443,7 +1431,7 @@ const TimelineControls = ({
} }
// Only process tooltip display if clicked on the timeline background or thumbnails, not on other UI elements // Only process tooltip display if clicked on the timeline background or thumbnails, not on other UI elements
if (e.target === timelineRef.current || (e.target as HTMLElement).classList.contains('timeline-thumbnail')) { if (e.target === timelineRef.current) {
// Check if there's a segment at the clicked position // Check if there's a segment at the clicked position
if (segmentAtClickedTime) { if (segmentAtClickedTime) {
setSelectedSegmentId(segmentAtClickedTime.id); setSelectedSegmentId(segmentAtClickedTime.id);
@ -1549,15 +1537,6 @@ const TimelineControls = ({
const position = Math.max(0, Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width)); const position = Math.max(0, Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width));
const newTime = position * duration; const newTime = position * duration;
// Create a temporary segment with the current drag position to check against
const draggedSegment = {
id: segmentId,
startTime: isLeft ? newTime : originalStartTime,
endTime: isLeft ? originalEndTime : newTime,
name: '',
thumbnail: '',
};
// Check if the current marker position intersects with where the segment will be // Check if the current marker position intersects with where the segment will be
const currentSegmentStart = isLeft ? newTime : originalStartTime; const currentSegmentStart = isLeft ? newTime : originalStartTime;
const currentSegmentEnd = isLeft ? originalEndTime : newTime; const currentSegmentEnd = isLeft ? originalEndTime : newTime;
@ -2121,10 +2100,9 @@ const TimelineControls = ({
// Create a full video segment // Create a full video segment
const fullVideoSegment: Segment = { const fullVideoSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'Full Video', chapterTitle: 'Full Video',
startTime: 0, startTime: 0,
endTime: duration, endTime: duration,
thumbnail: '',
}; };
// Create and dispatch the update event to replace all segments with the full video segment // Create and dispatch the update event to replace all segments with the full video segment
@ -2522,15 +2500,15 @@ const TimelineControls = ({
// Add a useEffect for auto-redirection // Add a useEffect for auto-redirection
useEffect(() => { useEffect(() => {
let countdownInterval: NodeJS.Timeout; // Clear any existing timers first
let redirectTimeout: NodeJS.Timeout; cancelRedirect();
if (showSuccessModal && redirectUrl) { if (showSuccessModal && redirectUrl) {
// Start countdown timer // Start countdown timer
let secondsLeft = 10; let secondsLeft = 10;
// Update the countdown every second // Update the countdown every second
countdownInterval = setInterval(() => { countdownIntervalRef.current = setInterval(() => {
secondsLeft--; secondsLeft--;
const countdownElement = document.querySelector('.countdown'); const countdownElement = document.querySelector('.countdown');
if (countdownElement) { if (countdownElement) {
@ -2538,32 +2516,28 @@ const TimelineControls = ({
} }
if (secondsLeft <= 0) { if (secondsLeft <= 0) {
clearInterval(countdownInterval); if (countdownIntervalRef.current) {
clearInterval(countdownIntervalRef.current);
countdownIntervalRef.current = null;
}
} }
}, 1000); }, 1000);
// Set redirect timeout // Set redirect timeout
redirectTimeout = setTimeout(() => { redirectTimeoutRef.current = setTimeout(() => {
// Redirect to the URL // Redirect to the URL
logger.debug('Automatically redirecting to:', redirectUrl); logger.debug('Automatically redirecting to:', redirectUrl);
window.location.href = redirectUrl; window.location.href = redirectUrl;
}, 10000); // 10 seconds }, 10000); // 10 seconds
} }
// Cleanup on unmount or when success modal closes // Cleanup on unmount
return () => { return () => {
if (countdownInterval) clearInterval(countdownInterval); cancelRedirect();
if (redirectTimeout) clearTimeout(redirectTimeout);
}; };
}, [showSuccessModal, redirectUrl]); }, [showSuccessModal, redirectUrl, cancelRedirect]);
// Effect to handle redirect after success modal is closed // Note: Removed the conflicting redirect effect - redirect is now handled by cancelRedirect function
useEffect(() => {
if (!showSuccessModal && redirectUrl) {
logger.debug('Redirecting to:', redirectUrl);
window.location.href = redirectUrl;
}
}, [redirectUrl, saveType, showSuccessModal]);
return ( return (
<div className={`timeline-container-card ${isPlayingSegments ? 'segments-playback-mode' : ''}`}> <div className={`timeline-container-card ${isPlayingSegments ? 'segments-playback-mode' : ''}`}>
@ -2665,9 +2639,6 @@ const TimelineControls = ({
{/* Split Points */} {/* Split Points */}
{renderSplitPoints()} {renderSplitPoints()}
{/* Thumbnails */}
{renderThumbnails()}
{/* Segment Tooltip */} {/* Segment Tooltip */}
{selectedSegmentId !== null && ( {selectedSegmentId !== null && (
<div <div
@ -3267,10 +3238,9 @@ const TimelineControls = ({
// Create the new segment with a generic name // Create the new segment with a generic name
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: `segment`, chapterTitle: `segment`,
startTime: segmentStartTime, startTime: segmentStartTime,
endTime: segmentEndTime, endTime: segmentEndTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segment to existing segments // Add the new segment to existing segments
@ -3376,10 +3346,9 @@ const TimelineControls = ({
// Create a virtual "segment" for the cutaway area // Create a virtual "segment" for the cutaway area
const cutawaySegment: Segment = { const cutawaySegment: Segment = {
id: -999, // Use a unique negative ID to indicate a virtual segment id: -999, // Use a unique negative ID to indicate a virtual segment
name: 'Cutaway', chapterTitle: 'Cutaway',
startTime: startTime, startTime: startTime,
endTime: endTime, endTime: endTime,
thumbnail: '',
}; };
// Seek to the start of the cutaway (true beginning of this cutaway area) // Seek to the start of the cutaway (true beginning of this cutaway area)
@ -3616,249 +3585,6 @@ const TimelineControls = ({
/> />
</button> </button>
{/* Play/Pause button for empty space */}
{/* <button
className={`tooltip-action-btn ${isPlaying ? 'pause' : 'play'}`}
data-tooltip={isPlaying ? "Pause playback" : "Play from here until next segment"}
onClick={(e) => {
e.stopPropagation();
if (videoRef.current) {
if (isPlaying) {
// If already playing, pause the video
videoRef.current.pause();
setIsPlayingSegment(false);
// Reset continuePastBoundary when stopping playback
setContinuePastBoundary(false);
logger.debug("Pause clicked in empty space - resetting continuePastBoundary flag");
} else {
// Enable continuePastBoundary flag when user explicitly clicks play
// This will allow playback to continue even if we're at segment boundary
setContinuePastBoundary(true);
logger.debug("Setting continuePastBoundary=true to allow playback through boundaries");
// Find the current time and determine cutaway boundaries
// For end, find the next segment after current position
// Make sure we look for any segment that starts after our current position,
// including the first segment if we're before it
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
const currentTime = videoRef.current.currentTime;
const nextSegment = sortedSegments.find(seg => seg.startTime > currentTime);
// Check if we're at a segment boundary that we previously stopped at
const isAtSegmentBoundary = nextSegment && Math.abs(currentTime - nextSegment.startTime) < 0.05;
if (isAtSegmentBoundary && nextSegment) {
// We're at the start of a segment - just continue into the segment rather than staying in cutaway
logger.debug(`At segment boundary: Moving into segment ${nextSegment.id}`);
// Update UI to show segment tooltip instead of empty space tooltip
setSelectedSegmentId(nextSegment.id);
setShowEmptySpaceTooltip(false);
// Set this segment as the active segment for boundary checking
setActiveSegment(nextSegment);
// Play from this segment directly
videoRef.current.play()
.then(() => {
setIsPlayingSegment(true);
logger.debug("Playing from segment start after boundary");
})
.catch(err => {
console.error("Error starting playback:", err);
});
return; // Exit early as we've handled this special case
}
// Define end boundary (either next segment start or video end)
const endTime = nextSegment ? nextSegment.startTime : duration;
// Special handling for when we're already at a segment boundary
// If we're at or extremely close to the segment boundary already,
// we need to nudge the position slightly back to allow playback
let adjustedCurrentTime = currentTime;
if (nextSegment && Math.abs(currentTime - nextSegment.startTime) < 0.05) {
logger.debug(`Already at boundary (${formatDetailedTime(currentTime)}), nudging position back slightly`);
adjustedCurrentTime = Math.max(0, currentTime - 0.1); // Move 100ms back
videoRef.current.currentTime = adjustedCurrentTime;
onSeek(adjustedCurrentTime);
logger.debug(`Position adjusted to ${formatDetailedTime(adjustedCurrentTime)}`);
}
// Create a virtual "segment" for the cutaway area
const cutawaySegment: Segment = {
id: -999, // Use a consistent negative ID for virtual segments
name: "Cutaway",
startTime: adjustedCurrentTime, // Use the potentially adjusted time
endTime: endTime,
thumbnail: ""
};
// IMPORTANT: First reset isPlayingSegment to false to ensure clean state
setIsPlayingSegment(false);
// Then set active segment for boundary checking
// We use setTimeout to ensure this happens in the next tick
// after the isPlayingSegment value is updated
setTimeout(() => {
setActiveSegment(cutawaySegment);
}, 0);
// Add a manual boundary check specifically for cutaway playback
// This ensures we detect when we reach the next segment's start
const checkCutawayBoundary = () => {
if (!videoRef.current) return;
// Check if we've entered a segment (i.e., reached a boundary)
const currentPosition = videoRef.current.currentTime;
const segments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
// Find the next segment we're approaching - use a wider detection range
// to catch the boundary earlier
const nextSegment = segments.find(seg => seg.startTime > currentPosition - 0.3);
// Also check if we've entered a different segment - we need to detect this too
const segmentAtCurrentTime = segments.find(
seg => currentPosition >= seg.startTime && currentPosition <= seg.endTime
);
// If we've moved directly into a segment during playback, we need to update the active segment
if (segmentAtCurrentTime && activeSegment?.id !== segmentAtCurrentTime.id) {
logger.debug(`Entered segment ${segmentAtCurrentTime.id} during cutaway playback`);
setActiveSegment(segmentAtCurrentTime);
setSelectedSegmentId(segmentAtCurrentTime.id);
setShowEmptySpaceTooltip(false);
// Remove our boundary checker since we're now in a standard segment
videoRef.current.removeEventListener('timeupdate', checkCutawayBoundary);
// Reset continuation flags
setContinuePastBoundary(false);
sessionStorage.removeItem('continuingPastSegment');
return;
}
// We need to detect boundaries much earlier to allow for time to react
// This is a key fix - we need to detect the boundary BEFORE we reach it
// But don't stop if we're in continuePastBoundary mode
const shouldStop = nextSegment &&
(currentPosition >= nextSegment.startTime - 0.25) &&
(currentPosition <= nextSegment.startTime + 0.1) &&
!continuePastBoundary;
// Add logging to show boundary check decisions
if (nextSegment && (currentPosition >= nextSegment.startTime - 0.25) &&
(currentPosition <= nextSegment.startTime + 0.1)) {
logger.debug(`Approaching boundary at ${formatDetailedTime(nextSegment.startTime)}, continuePastBoundary=${continuePastBoundary}, willStop=${shouldStop}`);
}
// If we've entered a segment, stop at its boundary
if (shouldStop && nextSegment) {
logger.debug(`CUTAWAY MANUAL BOUNDARY CHECK: Current position ${formatDetailedTime(currentPosition)} approaching segment at ${formatDetailedTime(nextSegment.startTime)} (distance: ${Math.abs(currentPosition - nextSegment.startTime).toFixed(3)}s) - STOPPING`);
videoRef.current.pause();
// Force exact time position with high precision
setTimeout(() => {
if (videoRef.current) {
// First seek directly to exact start time, no offset
videoRef.current.currentTime = nextSegment.startTime;
// Update UI immediately to match video position
onSeek(nextSegment.startTime);
// Also update tooltip time displays
setDisplayTime(nextSegment.startTime);
setClickedTime(nextSegment.startTime);
// Reset continuePastBoundary when stopping at a boundary
setContinuePastBoundary(false);
// Update tooltip to show the segment at the boundary
setSelectedSegmentId(nextSegment.id);
setShowEmptySpaceTooltip(false);
setActiveSegment(nextSegment);
// Force multiple adjustments to ensure exact precision
const verifyPosition = () => {
if (videoRef.current) {
// Always force the exact time in every verification
videoRef.current.currentTime = nextSegment.startTime;
// Make sure we update the UI to reflect the corrected position
onSeek(nextSegment.startTime);
// Update the displayTime and clickedTime state to match exact position
setDisplayTime(nextSegment.startTime);
setClickedTime(nextSegment.startTime);
logger.debug(`Position corrected to exact segment boundary: ${formatDetailedTime(videoRef.current.currentTime)} (target: ${formatDetailedTime(nextSegment.startTime)})`);
}
};
// Apply multiple correction attempts with increasing delays
setTimeout(verifyPosition, 10); // Immediate correction
setTimeout(verifyPosition, 20); // First correction
setTimeout(verifyPosition, 50); // Second correction
setTimeout(verifyPosition, 100); // Third correction
setTimeout(verifyPosition, 200); // Final correction
// Also add event listeners to ensure position is corrected whenever video state changes
videoRef.current.addEventListener('seeked', verifyPosition);
videoRef.current.addEventListener('canplay', verifyPosition);
videoRef.current.addEventListener('waiting', verifyPosition);
// Remove these event listeners after a short time
setTimeout(() => {
if (videoRef.current) {
videoRef.current.removeEventListener('seeked', verifyPosition);
videoRef.current.removeEventListener('canplay', verifyPosition);
videoRef.current.removeEventListener('waiting', verifyPosition);
}
}, 300);
}
}, 10);
setIsPlayingSegment(false);
setActiveSegment(null);
// Remove our boundary checker
videoRef.current.removeEventListener('timeupdate', checkCutawayBoundary);
return;
}
};
// Start our manual boundary checker
videoRef.current.addEventListener('timeupdate', checkCutawayBoundary);
// Start playing from current position with boundary restrictions
// Use a timeout to ensure active segment is set before playback starts
setTimeout(() => {
if (videoRef.current) {
videoRef.current.play()
.then(() => {
setIsPlayingSegment(true);
logger.debug("Play clicked in empty space - position:",
formatDetailedTime(currentTime),
"will stop at:", formatDetailedTime(endTime),
nextSegment ? `(start of segment ${nextSegment.id})` : "(end of video)"
);
})
.catch(err => {
console.error("Error starting playback:", err);
});
}
}, 50);
}
}
}}
>
{isPlaying ? (
<img src={pauseIcon} alt="Pause" style={{width: '24px', height: '24px'}} />
) : (
<img src={playIcon} alt="Play" style={{width: '24px', height: '24px'}} />
)}
</button> */}
{/* Play/Pause button for empty space - Same as main play/pause button */} {/* Play/Pause button for empty space - Same as main play/pause button */}
<button <button
className={`tooltip-action-btn ${isPlaying ? 'pause' : 'play'} ${ className={`tooltip-action-btn ${isPlaying ? 'pause' : 'play'} ${
@ -3997,10 +3723,9 @@ const TimelineControls = ({
// We're in a gap, create a new segment from gap start to clicked time // We're in a gap, create a new segment from gap start to clicked time
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: gapStart, startTime: gapStart,
endTime: clickedTime, endTime: clickedTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segment to existing segments // Add the new segment to existing segments
@ -4031,10 +3756,9 @@ const TimelineControls = ({
// Create a new segment from start of video to clicked time // Create a new segment from start of video to clicked time
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: 0, startTime: 0,
endTime: clickedTime, endTime: clickedTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segment to existing segments // Add the new segment to existing segments
@ -4095,10 +3819,9 @@ const TimelineControls = ({
// No segments exist; create a new segment from start to clicked time // No segments exist; create a new segment from start to clicked time
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: 0, startTime: 0,
endTime: clickedTime, endTime: clickedTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Create and dispatch the update event // Create and dispatch the update event
@ -4214,10 +3937,9 @@ const TimelineControls = ({
// We're in a gap, create a new segment from clicked time to gap end // We're in a gap, create a new segment from clicked time to gap end
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: clickedTime, startTime: clickedTime,
endTime: gapEnd, endTime: gapEnd,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segment to existing segments // Add the new segment to existing segments
@ -4248,10 +3970,9 @@ const TimelineControls = ({
// Create a new segment from clicked time to first segment start // Create a new segment from clicked time to first segment start
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: clickedTime, startTime: clickedTime,
endTime: sortedByStart[0].startTime, endTime: sortedByStart[0].startTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segment to existing segments // Add the new segment to existing segments
@ -4283,10 +4004,9 @@ const TimelineControls = ({
// Create a new segment from clicked time to end of video // Create a new segment from clicked time to end of video
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: clickedTime, startTime: clickedTime,
endTime: duration, endTime: duration,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segment to existing segments // Add the new segment to existing segments
@ -4347,10 +4067,9 @@ const TimelineControls = ({
// No segments exist; create a new segment from clicked time to end // No segments exist; create a new segment from clicked time to end
const newSegment: Segment = { const newSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: clickedTime, startTime: clickedTime,
endTime: duration, endTime: duration,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Create and dispatch the update event // Create and dispatch the update event
@ -4760,7 +4479,10 @@ const TimelineControls = ({
{/* Success Modal */} {/* Success Modal */}
<Modal <Modal
isOpen={showSuccessModal} isOpen={showSuccessModal}
onClose={() => setShowSuccessModal(false)} onClose={() => {
cancelRedirect();
setShowSuccessModal(false);
}}
title="Video Edited Successfully" title="Video Edited Successfully"
> >
<div className="modal-success-content"> <div className="modal-success-content">

View File

@ -1,5 +1,4 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { generateThumbnail } from '@/lib/videoUtils';
import { formatDetailedTime } from '@/lib/timeUtils'; import { formatDetailedTime } from '@/lib/timeUtils';
import logger from '@/lib/logger'; import logger from '@/lib/logger';
import type { Segment } from '@/components/ClipSegments'; import type { Segment } from '@/components/ClipSegments';
@ -34,7 +33,6 @@ const useVideoChapters = () => {
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
// Timeline state // Timeline state
const [thumbnails, setThumbnails] = useState<string[]>([]);
const [trimStart, setTrimStart] = useState(0); const [trimStart, setTrimStart] = useState(0);
const [trimEnd, setTrimEnd] = useState(0); const [trimEnd, setTrimEnd] = useState(0);
const [splitPoints, setSplitPoints] = useState<number[]>([]); const [splitPoints, setSplitPoints] = useState<number[]>([]);
@ -105,30 +103,22 @@ const useVideoChapters = () => {
const startTime = parseTimeToSeconds(chapter.startTime); const startTime = parseTimeToSeconds(chapter.startTime);
const endTime = parseTimeToSeconds(chapter.endTime); const endTime = parseTimeToSeconds(chapter.endTime);
// Generate thumbnail for this segment
const segmentThumbnail = await generateThumbnail(video, (startTime + endTime) / 2);
const segment: Segment = { const segment: Segment = {
id: i + 1, id: i + 1,
name: `segment-${i + 1}`, chapterTitle: chapter.chapterTitle,
startTime: startTime, startTime: startTime,
endTime: endTime, endTime: endTime,
thumbnail: segmentThumbnail,
chapterTitle: chapter.text,
}; };
initialSegments.push(segment); initialSegments.push(segment);
} }
} else { } else {
// Create a default segment that spans the entire video (fallback)
const segmentThumbnail = await generateThumbnail(video, video.duration / 2);
const initialSegment: Segment = { const initialSegment: Segment = {
id: 1, id: 1,
name: 'segment', chapterTitle: 'segment',
startTime: 0, startTime: 0,
endTime: video.duration, endTime: video.duration,
thumbnail: segmentThumbnail,
}; };
initialSegments = [initialSegment]; initialSegments = [initialSegment];
@ -145,19 +135,6 @@ const useVideoChapters = () => {
setHistory([initialState]); setHistory([initialState]);
setHistoryPosition(0); setHistoryPosition(0);
setClipSegments(initialSegments); setClipSegments(initialSegments);
// Generate timeline thumbnails
const count = 6;
const interval = video.duration / count;
const placeholders: string[] = [];
for (let i = 0; i < count; i++) {
const time = interval * i + interval / 2;
const thumbnail = await generateThumbnail(video, time);
placeholders.push(thumbnail);
}
setThumbnails(placeholders);
}; };
initializeEditor(); initializeEditor();
@ -470,22 +447,18 @@ const useVideoChapters = () => {
newSegments.splice(segmentIndex, 1); newSegments.splice(segmentIndex, 1);
// Create first half of the split segment - no thumbnail needed
const firstHalf: Segment = { const firstHalf: Segment = {
id: Date.now(), id: Date.now(),
name: `${segmentToSplit.name}-A`, chapterTitle: `${segmentToSplit.chapterTitle}-A`,
startTime: segmentToSplit.startTime, startTime: segmentToSplit.startTime,
endTime: timeToSplit, endTime: timeToSplit,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Create second half of the split segment - no thumbnail needed
const secondHalf: Segment = { const secondHalf: Segment = {
id: Date.now() + 1, id: Date.now() + 1,
name: `${segmentToSplit.name}-B`, chapterTitle: `${segmentToSplit.chapterTitle}-B`,
startTime: timeToSplit, startTime: timeToSplit,
endTime: segmentToSplit.endTime, endTime: segmentToSplit.endTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segments // Add the new segments
@ -513,13 +486,11 @@ const useVideoChapters = () => {
// If all segments are deleted, create a new full video segment // If all segments are deleted, create a new full video segment
if (newSegments.length === 0 && videoRef.current) { if (newSegments.length === 0 && videoRef.current) {
// Create a new default segment that spans the entire video // Create a new default segment that spans the entire video
// No need to generate a thumbnail - we'll use dynamic colors
const defaultSegment: Segment = { const defaultSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: 0, startTime: 0,
endTime: videoRef.current.duration, endTime: videoRef.current.duration,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Reset the trim points as well // Reset the trim points as well
@ -576,13 +547,11 @@ const useVideoChapters = () => {
const endTime = i < newSplitPoints.length ? newSplitPoints[i] : duration; const endTime = i < newSplitPoints.length ? newSplitPoints[i] : duration;
if (startTime < endTime) { if (startTime < endTime) {
// No need to generate thumbnails - we'll use dynamic colors
newSegments.push({ newSegments.push({
id: Date.now() + i, id: Date.now() + i,
name: `Segment ${i + 1}`, chapterTitle: `Segment ${i + 1}`,
startTime, startTime,
endTime, endTime,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}); });
startTime = endTime; startTime = endTime;
@ -603,13 +572,11 @@ const useVideoChapters = () => {
// Create a new default segment that spans the entire video // Create a new default segment that spans the entire video
if (!videoRef.current) return; if (!videoRef.current) return;
// No need to generate thumbnails - we'll use dynamic colors
const defaultSegment: Segment = { const defaultSegment: Segment = {
id: Date.now(), id: Date.now(),
name: 'segment', chapterTitle: 'segment',
startTime: 0, startTime: 0,
endTime: duration, endTime: duration,
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
setClipSegments([defaultSegment]); setClipSegments([defaultSegment]);
@ -731,7 +698,7 @@ const useVideoChapters = () => {
}; };
// Handle saving chapters to database // Handle saving chapters to database
const handleChapterSave = async (chapters: { name: string; from: string; to: string }[]) => { const handleChapterSave = async (chapters: { chapterTitle: string; from: string; to: string }[]) => {
try { try {
// Get media ID from window.MEDIA_DATA // Get media ID from window.MEDIA_DATA
const mediaId = (window as any).MEDIA_DATA?.mediaId; const mediaId = (window as any).MEDIA_DATA?.mediaId;
@ -744,7 +711,7 @@ const useVideoChapters = () => {
const backendChapters = chapters.map((chapter) => ({ const backendChapters = chapters.map((chapter) => ({
startTime: chapter.from, startTime: chapter.from,
endTime: chapter.to, endTime: chapter.to,
text: chapter.name, chapterTitle: chapter.chapterTitle,
})); }));
// Create the API request body // Create the API request body
@ -931,7 +898,6 @@ const useVideoChapters = () => {
setIsPlaying, setIsPlaying,
isMuted, isMuted,
isPlayingSegments, isPlayingSegments,
thumbnails,
trimStart, trimStart,
trimEnd, trimEnd,
splitPoints, splitPoints,

View File

@ -176,15 +176,6 @@
right: -4px; right: -4px;
} }
.timeline-thumbnail {
height: 100%;
border-right: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
display: inline-block;
background-size: cover;
background-position: center;
}
.split-point { .split-point {
position: absolute; position: absolute;
width: 2px; width: 2px;

View File

@ -15,30 +15,3 @@ export const generateSolidColor = (time: number, duration: number): string => {
return `hsl(${hue}, ${saturation}%, ${lightness}%)`; return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}; };
/**
* Legacy function kept for compatibility
* Now returns a data URL for a solid color square instead of a video thumbnail
*/
export const generateThumbnail = async (videoElement: HTMLVideoElement, time: number): Promise<string> => {
return new Promise((resolve) => {
// Create a small canvas for the solid color
const canvas = document.createElement('canvas');
canvas.width = 10; // Much smaller - we only need a color
canvas.height = 10;
const ctx = canvas.getContext('2d');
if (ctx) {
// Get the solid color based on time
const color = generateSolidColor(time, videoElement.duration);
// Fill with solid color
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Convert to data URL (much smaller now)
const dataUrl = canvas.toDataURL('image/png', 0.5);
resolve(dataUrl);
});
};

View File

@ -6,25 +6,24 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// Auto-save interface // Auto-save interface
interface AutoSaveRequest { interface AutoSaveRequest {
segments: { chapters: {
startTime: string; startTime: string;
endTime: string; endTime: string;
name?: string; chapterTitle?: string;
}[]; }[];
} }
interface AutoSaveResponse { interface AutoSaveResponse {
success: boolean; success: boolean;
timestamp: string;
error?: string;
status?: string; status?: string;
media_id?: string; timestamp: string;
segments?: { chapters?: {
startTime: string; startTime: string;
endTime: string; endTime: string;
name: string; chapterTitle: string;
}[]; }[];
updated_at?: string; updated_at?: string;
error?: string;
} }
// Auto-save API function // Auto-save API function
@ -36,6 +35,9 @@ export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Pro
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
console.log('data edw', data);
console.log('response edw', response);
logger.debug('response', response); logger.debug('response', response);
if (!response.ok) { if (!response.ok) {
@ -54,13 +56,13 @@ export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Pro
return { return {
success: false, success: false,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
error: errorData.error || 'Auto-save failed', error: errorData.error || 'Auto-save failed (videoApi.ts)',
}; };
} catch (parseError) { } catch (parseError) {
return { return {
success: false, success: false,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
error: 'Auto-save failed', error: 'Auto-save failed (videoApi.ts)',
}; };
} }
} }
@ -68,21 +70,15 @@ export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Pro
// Successful response // Successful response
const jsonResponse = await response.json(); const jsonResponse = await response.json();
console.log('jsonResponse edw', jsonResponse);
// Check if the response has the expected format // Check if the response has the expected format
if (jsonResponse.status === 'success') {
return { return {
success: true, success: true,
timestamp: jsonResponse.updated_at || new Date().toISOString(), timestamp: jsonResponse.updated_at || new Date().toISOString(),
...jsonResponse, ...jsonResponse,
}; };
} else {
return {
success: false,
timestamp: new Date().toISOString(),
error: jsonResponse.error || 'Auto-save failed',
};
}
} catch (error) { } catch (error) {
// For any fetch errors, return mock success response // For any fetch errors, return mock success response
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();

View File

@ -216,16 +216,6 @@
align-items: center; align-items: center;
} }
.segment-thumbnail {
width: 4rem;
height: 2.25rem;
background-size: cover;
background-position: center;
border-radius: 0.25rem;
margin-right: 0.75rem;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
}
.segment-info { .segment-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;