diff --git a/frontend-tools/video-editor/client/src/components/EditingTools.tsx b/frontend-tools/video-editor/client/src/components/EditingTools.tsx index 8f5ca165..c1fe8085 100644 --- a/frontend-tools/video-editor/client/src/components/EditingTools.tsx +++ b/frontend-tools/video-editor/client/src/components/EditingTools.tsx @@ -1,5 +1,5 @@ -import '../styles/EditingTools.css'; -import { useEffect, useState } from 'react'; +import "../styles/EditingTools.css"; +import { useEffect, useState } from "react"; interface EditingToolsProps { onSplit: () => void; @@ -24,7 +24,7 @@ const EditingTools = ({ canUndo, canRedo, isPlaying = false, - isPlayingSegments = false, + isPlayingSegments = false }: EditingToolsProps) => { const [isSmallScreen, setIsSmallScreen] = useState(false); @@ -34,17 +34,17 @@ const EditingTools = ({ }; checkScreenSize(); - window.addEventListener('resize', checkScreenSize); - return () => window.removeEventListener('resize', checkScreenSize); + window.addEventListener("resize", checkScreenSize); + return () => window.removeEventListener("resize", checkScreenSize); }, []); // Handle play button click with iOS fix const handlePlay = () => { // Ensure lastSeekedPosition is used when play is clicked - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { console.log("Play button clicked, current lastSeekedPosition:", window.lastSeekedPosition); } - + // Call the original handler onPlay(); }; @@ -55,15 +55,25 @@ const EditingTools = ({ {/* Left side - Play buttons group */}
{/* Play Segments button */} -
- + {/* Right side - Editing tools */}
- -
- diff --git a/frontend-tools/video-editor/client/src/components/TimelineControls.tsx b/frontend-tools/video-editor/client/src/components/TimelineControls.tsx index 0f9511d8..f93def4a 100644 --- a/frontend-tools/video-editor/client/src/components/TimelineControls.tsx +++ b/frontend-tools/video-editor/client/src/components/TimelineControls.tsx @@ -5,24 +5,24 @@ import { Segment } from "./ClipSegments"; import Modal from "./Modal"; import { trimVideo } from "../services/videoApi"; import logger from "../lib/logger"; -import '../styles/TimelineControls.css'; -import '../styles/TwoRowTooltip.css'; -import playIcon from '../assets/play-icon.svg'; -import pauseIcon from '../assets/pause-icon.svg'; -import playFromBeginningIcon from '../assets/play-from-beginning-icon.svg'; -import segmentEndIcon from '../assets/segment-end-new.svg'; -import segmentStartIcon from '../assets/segment-start-new.svg'; -import segmentNewStartIcon from '../assets/segment-start-new-cutaway.svg'; -import segmentNewEndIcon from '../assets/segment-end-new-cutaway.svg'; +import "../styles/TimelineControls.css"; +import "../styles/TwoRowTooltip.css"; +import playIcon from "../assets/play-icon.svg"; +import pauseIcon from "../assets/pause-icon.svg"; +import playFromBeginningIcon from "../assets/play-from-beginning-icon.svg"; +import segmentEndIcon from "../assets/segment-end-new.svg"; +import segmentStartIcon from "../assets/segment-start-new.svg"; +import segmentNewStartIcon from "../assets/segment-start-new-cutaway.svg"; +import segmentNewEndIcon from "../assets/segment-end-new-cutaway.svg"; // Add styles for the media page link const mediaPageLinkStyles = { - color: '#007bff', - textDecoration: 'none', - fontWeight: 'bold', - '&:hover': { - textDecoration: 'underline', - color: '#0056b3' + color: "#007bff", + textDecoration: "none", + fontWeight: "bold", + "&:hover": { + textDecoration: "underline", + color: "#0056b3" } } as const; @@ -59,19 +59,20 @@ const constrainTooltipPosition = (positionPercent: number) => { const leftTransitionEnd = 25; const rightTransitionStart = 75; const rightTransitionEnd = 100; - + let leftValue: string; let transform: string; - + if (positionPercent <= leftTransitionEnd) { // Left side: smooth transition from center to left-aligned if (positionPercent <= leftTransitionStart) { // Fully left-aligned - leftValue = '0%'; - transform = 'none'; + leftValue = "0%"; + transform = "none"; } else { // Smooth transition zone - const transitionProgress = (positionPercent - leftTransitionStart) / (leftTransitionEnd - leftTransitionStart); + const transitionProgress = + (positionPercent - leftTransitionStart) / (leftTransitionEnd - leftTransitionStart); const translateAmount = -50 * transitionProgress; // Gradually reduce from 0% to -50% leftValue = `${positionPercent}%`; transform = `translateX(${translateAmount}%)`; @@ -80,19 +81,20 @@ const constrainTooltipPosition = (positionPercent: number) => { // Right side: smooth transition from center to right-aligned if (positionPercent >= rightTransitionEnd) { // Fully right-aligned - leftValue = '100%'; - transform = 'translateX(-100%)'; + leftValue = "100%"; + transform = "translateX(-100%)"; } else { // Smooth transition zone - const transitionProgress = (positionPercent - rightTransitionStart) / (rightTransitionEnd - rightTransitionStart); - const translateAmount = -50 - (50 * transitionProgress); // Gradually change from -50% to -100% + const transitionProgress = + (positionPercent - rightTransitionStart) / (rightTransitionEnd - rightTransitionStart); + const translateAmount = -50 - 50 * transitionProgress; // Gradually change from -50% to -100% leftValue = `${positionPercent}%`; transform = `translateX(${translateAmount}%)`; } } else { // Center zone: normal centered positioning leftValue = `${positionPercent}%`; - transform = 'translateX(-50%)'; + transform = "translateX(-50%)"; } return { left: leftValue, transform }; @@ -120,7 +122,7 @@ const TimelineControls = ({ isPlaying, setIsPlaying, onPlayPause, // Add this prop - isPlayingSegments = false, + isPlayingSegments = false }: TimelineControlsProps) => { const timelineRef = useRef(null); const leftHandleRef = useRef(null); @@ -145,9 +147,10 @@ const TimelineControls = ({ e.stopPropagation(); // Calculate new time based on offset (positive or negative) - const newTime = offsetSeconds < 0 - ? Math.max(0, clickedTime + offsetSeconds) // For negative offsets (going back) - : Math.min(duration, clickedTime + offsetSeconds); // For positive offsets (going forward) + const newTime = + offsetSeconds < 0 + ? Math.max(0, clickedTime + offsetSeconds) // For negative offsets (going back) + : Math.min(duration, clickedTime + offsetSeconds); // For positive offsets (going forward) // Save the current playing state before seeking const wasPlaying = isPlayingSegment; @@ -179,9 +182,10 @@ const TimelineControls = ({ // Function to perform time adjustment const adjustTime = () => { // Calculate new time based on fixed offset (positive or negative) - const newTime = adjustmentValue < 0 - ? Math.max(0, lastTimeValue + adjustmentValue) // For negative offsets (going back) - : Math.min(duration, lastTimeValue + adjustmentValue); // For positive offsets (going forward) + const newTime = + adjustmentValue < 0 + ? Math.max(0, lastTimeValue + adjustmentValue) // For negative offsets (going back) + : Math.min(duration, lastTimeValue + adjustmentValue); // For positive offsets (going forward) // Update our last time value for next adjustment lastTimeValue = newTime; @@ -200,7 +204,7 @@ const TimelineControls = ({ if (timelineRef.current) { const rect = timelineRef.current.getBoundingClientRect(); const positionPercent = (newTime / duration) * 100; - const xPos = rect.left + (rect.width * (positionPercent / 100)); + const xPos = rect.left + rect.width * (positionPercent / 100); setTooltipPosition({ x: xPos, y: rect.top - 10 @@ -208,7 +212,7 @@ const TimelineControls = ({ // Find if we're in a segment at the new time const segmentAtTime = clipSegments.find( - seg => newTime >= seg.startTime && newTime <= seg.endTime + (seg) => newTime >= seg.startTime && newTime <= seg.endTime ); if (segmentAtTime) { @@ -259,16 +263,17 @@ const TimelineControls = ({ clearInterval(continuousTimer); continuousTimer = null; } - document.removeEventListener('mouseup', clearTimers); - document.removeEventListener('mouseleave', clearTimers); + document.removeEventListener("mouseup", clearTimers); + document.removeEventListener("mouseleave", clearTimers); }; - document.addEventListener('mouseup', clearTimers); - document.addEventListener('mouseleave', clearTimers); + document.addEventListener("mouseup", clearTimers); + document.addEventListener("mouseleave", clearTimers); }, onTouchStart: (e: React.TouchEvent) => { e.stopPropagation(); - e.preventDefault();21 + e.preventDefault(); + 21; // Update the initial last time value lastTimeValue = clickedTime; @@ -292,12 +297,12 @@ const TimelineControls = ({ clearInterval(continuousTimer); continuousTimer = null; } - document.removeEventListener('touchend', clearTimers); - document.removeEventListener('touchcancel', clearTimers); + document.removeEventListener("touchend", clearTimers); + document.removeEventListener("touchcancel", clearTimers); }; - document.addEventListener('touchend', clearTimers); - document.addEventListener('touchcancel', clearTimers); + document.addEventListener("touchend", clearTimers); + document.addEventListener("touchcancel", clearTimers); }, onClick: (e: React.MouseEvent) => { // This prevents the click event from firing twice @@ -334,16 +339,23 @@ const TimelineControls = ({ try { // Format segments data for API request - const segments = clipSegments.map(segment => ({ + const segments = clipSegments.map((segment) => ({ startTime: formatDetailedTime(segment.startTime), endTime: formatDetailedTime(segment.endTime) })); - const mediaId = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId || null; - const redirectURL = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.redirectURL || null; + const mediaId = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.mediaId) || null; + const redirectURL = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.redirectURL) || null; // Log the request details for debugging - logger.debug("Save request:", { mediaId, segments, saveAsCopy: false, redirectURL }); + logger.debug("Save request:", { + mediaId, + segments, + saveAsCopy: false, + redirectURL + }); const response = await trimVideo(mediaId, { segments, @@ -384,7 +396,8 @@ const TimelineControls = ({ setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : "An error occurred during processing"; + const errorMsg = + error instanceof Error ? error.message : "An error occurred during processing"; logger.debug("Save error (exception):", errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); @@ -399,16 +412,23 @@ const TimelineControls = ({ try { // Format segments data for API request - const segments = clipSegments.map(segment => ({ + const segments = clipSegments.map((segment) => ({ startTime: formatDetailedTime(segment.startTime), endTime: formatDetailedTime(segment.endTime) })); - const mediaId = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId || null; - const redirectUserMediaURL = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.redirectUserMediaURL || null; + const mediaId = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.mediaId) || null; + const redirectUserMediaURL = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.redirectUserMediaURL) || null; // Log the request details for debugging - logger.debug("Save as copy request:", { mediaId, segments, saveAsCopy: true, redirectUserMediaURL }); + logger.debug("Save as copy request:", { + mediaId, + segments, + saveAsCopy: true, + redirectUserMediaURL + }); const response = await trimVideo(mediaId, { segments, @@ -449,7 +469,8 @@ const TimelineControls = ({ setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : "An error occurred during processing"; + const errorMsg = + error instanceof Error ? error.message : "An error occurred during processing"; logger.debug("Save as copy error (exception):", errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); @@ -464,14 +485,16 @@ const TimelineControls = ({ try { // Format segments data for API request, with each segment saved as a separate file - const segments = clipSegments.map(segment => ({ + const segments = clipSegments.map((segment) => ({ startTime: formatDetailedTime(segment.startTime), endTime: formatDetailedTime(segment.endTime), name: segment.name // Include segment name for individual files })); - const mediaId = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId || null; - const redirectUserMediaURL = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.redirectUserMediaURL || null; + const mediaId = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.mediaId) || null; + const redirectUserMediaURL = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.redirectUserMediaURL) || null; // Log the request details for debugging logger.debug("Save segments request:", { @@ -523,7 +546,8 @@ const TimelineControls = ({ setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : "An error occurred during processing"; + const errorMsg = + error instanceof Error ? error.message : "An error occurred during processing"; logger.debug("Save segments error (exception):", errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); @@ -544,7 +568,7 @@ const TimelineControls = ({ // Smooth scroll to the desired position scrollContainerRef.current.scrollTo({ left: desiredScrollPosition, - behavior: 'smooth' + behavior: "smooth" }); // Update tooltip position to stay with the marker @@ -553,7 +577,7 @@ const TimelineControls = ({ // Calculate the visible position of the marker after scrolling const containerRect = scrollContainerRef.current.getBoundingClientRect(); const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - const markerX = visibleTimelineLeft + (currentTimePercent / 100 * rect.width); + const markerX = visibleTimelineLeft + (currentTimePercent / 100) * rect.width; // Only update if we have a tooltip showing if (selectedSegmentId !== null || showEmptySpaceTooltip) { @@ -564,7 +588,14 @@ const TimelineControls = ({ setClickedTime(currentTime); } } - }, [currentTime, zoomLevel, duration, selectedSegmentId, showEmptySpaceTooltip, currentTimePercent]); + }, [ + currentTime, + zoomLevel, + duration, + selectedSegmentId, + showEmptySpaceTooltip, + currentTimePercent + ]); // Effect to check active segment boundaries during playback useEffect(() => { @@ -573,8 +604,9 @@ const TimelineControls = ({ if (!video || !activeSegment || !isPlayingSegment) { // Log why we're skipping if (!video) logger.debug("Skipping segment boundary check - no video element"); - else if (!activeSegment) logger.debug("Skipping segment boundary check - no active segment"); - else if (!isPlayingSegment) logger.debug("Skipping segment boundary check - not in segment playback mode"); + else if (!activeSegment) logger.debug("Skipping segment boundary check - no active segment"); + else if (!isPlayingSegment) + logger.debug("Skipping segment boundary check - not in segment playback mode"); return; } @@ -584,10 +616,13 @@ const TimelineControls = ({ return; } - logger.debug("Segment boundary check ACTIVATED for segment:", + logger.debug( + "Segment boundary check ACTIVATED for segment:", activeSegment.id, - "Start:", formatDetailedTime(activeSegment.startTime), - "End:", formatDetailedTime(activeSegment.endTime) + "Start:", + formatDetailedTime(activeSegment.startTime), + "End:", + formatDetailedTime(activeSegment.endTime) ); const handleTimeUpdate = () => { @@ -595,11 +630,15 @@ const TimelineControls = ({ // Log every second to show we're actually checking if (Math.round(timeLeft * 10) % 10 === 0) { - logger.debug("Segment playback - time remaining:", + logger.debug( + "Segment playback - time remaining:", formatDetailedTime(timeLeft), - "Current:", formatDetailedTime(video.currentTime), - "End:", formatDetailedTime(activeSegment.endTime), - "ContinuePastBoundary:", continuePastBoundary + "Current:", + formatDetailedTime(video.currentTime), + "End:", + formatDetailedTime(activeSegment.endTime), + "ContinuePastBoundary:", + continuePastBoundary ); } @@ -610,7 +649,10 @@ const TimelineControls = ({ setIsPlayingSegment(false); // Reset continuePastBoundary when stopping at boundary setContinuePastBoundary(false); - logger.debug("Passed segment end - setting back to exact boundary:", formatDetailedTime(activeSegment.endTime)); + logger.debug( + "Passed segment end - setting back to exact boundary:", + formatDetailedTime(activeSegment.endTime) + ); return; } @@ -623,11 +665,14 @@ const TimelineControls = ({ video.pause(); video.currentTime = activeSegment.endTime; setIsPlayingSegment(false); - logger.debug("Paused at segment end boundary:", formatDetailedTime(activeSegment.endTime)); + logger.debug( + "Paused at segment end boundary:", + formatDetailedTime(activeSegment.endTime) + ); // Look for the next segment after this one (for potential continuation) const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const nextSegment = sortedSegments.find(seg => seg.startTime > activeSegment.endTime); + const nextSegment = sortedSegments.find((seg) => seg.startTime > activeSegment.endTime); // If there's a next segment immediately after this one, update the tooltip to show that segment if (nextSegment && Math.abs(nextSegment.startTime - activeSegment.endTime) < 0.1) { @@ -640,7 +685,10 @@ const TimelineControls = ({ } } else { // We're continuing past the boundary - logger.debug("Continuing past segment boundary:", formatDetailedTime(activeSegment.endTime)); + logger.debug( + "Continuing past segment boundary:", + formatDetailedTime(activeSegment.endTime) + ); // Reset the flag after we've passed the boundary to ensure we stop at the next boundary if (video.currentTime > activeSegment.endTime) { @@ -648,17 +696,17 @@ const TimelineControls = ({ logger.debug("Past segment boundary - resetting continuePastBoundary flag"); // Remove the active segment to avoid boundary checking until next segment is activated setActiveSegment(null); - sessionStorage.removeItem('continuingPastSegment'); + sessionStorage.removeItem("continuingPastSegment"); } } } }; // Add event listener for timeupdate to check segment boundaries - video.addEventListener('timeupdate', handleTimeUpdate); + video.addEventListener("timeupdate", handleTimeUpdate); return () => { - video.removeEventListener('timeupdate', handleTimeUpdate); + video.removeEventListener("timeupdate", handleTimeUpdate); logger.debug("Segment boundary check DEACTIVATED"); }; }, [activeSegment, isPlayingSegment, continuePastBoundary, clipSegments]); @@ -684,7 +732,7 @@ const TimelineControls = ({ // Check if we're in any segment at current time const segmentAtCurrentTime = clipSegments.find( - seg => currentTime >= seg.startTime && currentTime <= seg.endTime + (seg) => currentTime >= seg.startTime && currentTime <= seg.endTime ); // Update tooltip position based on current time percentage @@ -699,7 +747,7 @@ const TimelineControls = ({ } // Check for the special "continue past segment" state in sessionStorage - const isContinuingPastSegment = sessionStorage.getItem('continuingPastSegment') === 'true'; + const isContinuingPastSegment = sessionStorage.getItem("continuingPastSegment") === "true"; // If we're in a segment now if (segmentAtCurrentTime) { @@ -713,24 +761,34 @@ const TimelineControls = ({ // If the active segment is different from the current segment and it's not a virtual segment // and we're not in "continue past boundary" mode, set this segment as the active segment - if (activeSegment?.id !== segmentAtCurrentTime.id && - !isPlayingVirtualSegment && - !isContinuingPastSegment && - !continuePastBoundary) { + if ( + activeSegment?.id !== segmentAtCurrentTime.id && + !isPlayingVirtualSegment && + !isContinuingPastSegment && + !continuePastBoundary + ) { // We've entered a new segment during normal playback - logger.debug(`Entered a new segment during playback: ${segmentAtCurrentTime.id}, setting as active`); + logger.debug( + `Entered a new segment during playback: ${segmentAtCurrentTime.id}, setting as active` + ); setActiveSegment(segmentAtCurrentTime); setSelectedSegmentId(segmentAtCurrentTime.id); setShowEmptySpaceTooltip(false); // Reset continuation flags to ensure boundary detection works for this new segment setContinuePastBoundary(false); - sessionStorage.removeItem('continuingPastSegment'); + sessionStorage.removeItem("continuingPastSegment"); } // If we're playing a virtual segment and enter a real segment, we've reached our boundary // We should stop playback if (isPlayingVirtualSegment && video && segmentAtCurrentTime) { - logger.debug(`CUTAWAY BOUNDARY REACHED: Current position ${formatDetailedTime(video.currentTime)} at segment ${segmentAtCurrentTime.id} - STOPPING at boundary ${formatDetailedTime(segmentAtCurrentTime.startTime)}`); + logger.debug( + `CUTAWAY BOUNDARY REACHED: Current position ${formatDetailedTime( + video.currentTime + )} at segment ${segmentAtCurrentTime.id} - STOPPING at boundary ${formatDetailedTime( + segmentAtCurrentTime.startTime + )}` + ); video.pause(); // Force exact time position with high precision and multiple attempts setTimeout(() => { @@ -763,28 +821,32 @@ const TimelineControls = ({ setDisplayTime(segmentAtCurrentTime.startTime); setClickedTime(segmentAtCurrentTime.startTime); - logger.debug(`Position corrected to exact segment boundary: ${formatDetailedTime(videoRef.current.currentTime)} (target: ${formatDetailedTime(segmentAtCurrentTime.startTime)})`); + logger.debug( + `Position corrected to exact segment boundary: ${formatDetailedTime( + videoRef.current.currentTime + )} (target: ${formatDetailedTime(segmentAtCurrentTime.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 + 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); + 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); + videoRef.current.removeEventListener("seeked", verifyPosition); + videoRef.current.removeEventListener("canplay", verifyPosition); + videoRef.current.removeEventListener("waiting", verifyPosition); } }, 300); } @@ -806,18 +868,23 @@ const TimelineControls = ({ // or playing a cutaway area // Just update the tooltip, but don't reactivate boundary checking if (selectedSegmentId !== segmentAtCurrentTime.id || showEmptySpaceTooltip) { - logger.debug("Tooltip updated for segment during continued playback:", segmentAtCurrentTime.id, - isPlayingVirtualSegment ? "(cutaway playback - keeping virtual segment)" : ""); + logger.debug( + "Tooltip updated for segment during continued playback:", + segmentAtCurrentTime.id, + isPlayingVirtualSegment ? "(cutaway playback - keeping virtual segment)" : "" + ); setSelectedSegmentId(segmentAtCurrentTime.id); setShowEmptySpaceTooltip(false); // If we're in a different segment now, clear the continuation flag // but only if it's not the same segment we were in before // AND we're not playing a cutaway area - if (!isPlayingVirtualSegment && - sessionStorage.getItem('lastSegmentId') !== segmentAtCurrentTime.id.toString()) { + if ( + !isPlayingVirtualSegment && + sessionStorage.getItem("lastSegmentId") !== segmentAtCurrentTime.id.toString() + ) { logger.debug("Moved to a different segment - ending continuation mode"); - sessionStorage.removeItem('continuingPastSegment'); + sessionStorage.removeItem("continuingPastSegment"); } } } else { @@ -829,7 +896,7 @@ const TimelineControls = ({ setShowEmptySpaceTooltip(false); // Store the current segment ID for comparison later - sessionStorage.setItem('lastSegmentId', segmentAtCurrentTime.id.toString()); + sessionStorage.setItem("lastSegmentId", segmentAtCurrentTime.id.toString()); } } } @@ -864,21 +931,21 @@ const TimelineControls = ({ logger.debug("Video paused at:", formatDetailedTime(currentTime)); } } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTime, isPlayingSegment, activeSegment, selectedSegmentId, clipSegments]); // Close zoom dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; - if (isZoomDropdownOpen && !target.closest('.zoom-dropdown-container')) { + if (isZoomDropdownOpen && !target.closest(".zoom-dropdown-container")) { setIsZoomDropdownOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, [isZoomDropdownOpen]); @@ -908,40 +975,55 @@ const TimelineControls = ({ // Use custom events to indicate drag state const createCustomEvent = (type: string) => { - return new CustomEvent('trim-handle-event', { + return new CustomEvent("trim-handle-event", { detail: { type, isStart: isLeft } }); }; // Dispatch start drag event to signal not to record history during drag - document.dispatchEvent(createCustomEvent('drag-start')); + document.dispatchEvent(createCustomEvent("drag-start")); const onMouseMove = (moveEvent: MouseEvent) => { if (!isDragging) return; const timelineWidth = timelineRect.width; - const position = Math.max(0, Math.min(1, (moveEvent.clientX - timelineRect.left) / timelineWidth)); + const position = Math.max( + 0, + Math.min(1, (moveEvent.clientX - timelineRect.left) / timelineWidth) + ); const newTime = position * duration; // Store position globally for iOS Safari - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.lastSeekedPosition = newTime; } if (isLeft) { if (newTime < trimEnd) { // Don't record in history during drag - this avoids multiple history entries - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { time: newTime, isStart: true, recordHistory: false } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: newTime, + isStart: true, + recordHistory: false + } + }) + ); finalTime = newTime; } } else { if (newTime > trimStart) { // Don't record in history during drag - this avoids multiple history entries - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { time: newTime, isStart: false, recordHistory: false } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: newTime, + isStart: false, + recordHistory: false + } + }) + ); finalTime = newTime; } } @@ -949,45 +1031,49 @@ const TimelineControls = ({ const onMouseUp = () => { isDragging = false; - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); // Now record the final position in history with action type if (isLeft) { // Final update with history recording - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { - time: finalTime, - isStart: true, - recordHistory: true, - action: 'adjust_trim_start' - } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: finalTime, + isStart: true, + recordHistory: true, + action: "adjust_trim_start" + } + }) + ); } else { - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { - time: finalTime, - isStart: false, - recordHistory: true, - action: 'adjust_trim_end' - } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: finalTime, + isStart: false, + recordHistory: true, + action: "adjust_trim_end" + } + }) + ); } // Dispatch end drag event - document.dispatchEvent(createCustomEvent('drag-end')); + document.dispatchEvent(createCustomEvent("drag-end")); }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); }; - leftHandle.addEventListener('mousedown', initDrag(true)); - rightHandle.addEventListener('mousedown', initDrag(false)); + leftHandle.addEventListener("mousedown", initDrag(true)); + rightHandle.addEventListener("mousedown", initDrag(false)); return () => { - leftHandle.removeEventListener('mousedown', initDrag(true)); - rightHandle.removeEventListener('mousedown', initDrag(false)); + leftHandle.removeEventListener("mousedown", initDrag(true)); + rightHandle.removeEventListener("mousedown", initDrag(false)); }; }, [duration, trimStart, trimEnd, onTrimStartChange, onTrimEndChange]); @@ -1011,7 +1097,7 @@ const TimelineControls = ({ className="timeline-thumbnail" style={{ width: `${100 / numSections}%`, - backgroundColor: backgroundColor, + backgroundColor: backgroundColor // Remove background image and use solid color instead }} /> @@ -1023,13 +1109,7 @@ const TimelineControls = ({ const renderSplitPoints = () => { return splitPoints.map((point, index) => { const pointPercent = (point / duration) * 100; - return ( -
- ); + return
; }); }; @@ -1046,8 +1126,8 @@ const TimelineControls = ({ const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); // Find the next and previous segments - const nextSegment = sortedSegments.find(seg => seg.startTime > startTime); - const prevSegment = [...sortedSegments].reverse().find(seg => seg.endTime < startTime); + const nextSegment = sortedSegments.find((seg) => seg.startTime > startTime); + const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < startTime); // Calculate the actual available space let availableSpace; @@ -1076,7 +1156,7 @@ const TimelineControls = ({ if (!timelineRef.current) return; // Find if we're in a segment at the current position with a small tolerance - const segmentAtPosition = clipSegments.find(seg => { + const segmentAtPosition = clipSegments.find((seg) => { const isWithinSegment = currentPosition >= seg.startTime && currentPosition <= seg.endTime; const isVeryCloseToStart = Math.abs(currentPosition - seg.startTime) < 0.001; const isVeryCloseToEnd = Math.abs(currentPosition - seg.endTime) < 0.001; @@ -1085,8 +1165,8 @@ const TimelineControls = ({ // Find the next and previous segments const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const nextSegment = sortedSegments.find(seg => seg.startTime > currentPosition); - const prevSegment = [...sortedSegments].reverse().find(seg => seg.endTime < currentPosition); + const nextSegment = sortedSegments.find((seg) => seg.startTime > currentPosition); + const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < currentPosition); if (segmentAtPosition) { // We're in or exactly at a segment boundary @@ -1119,10 +1199,10 @@ const TimelineControls = ({ if (zoomLevel > 1 && scrollContainerRef.current) { // For zoomed timeline, adjust for scroll position const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - xPos = visibleTimelineLeft + (rect.width * (positionPercent / 100)); + xPos = visibleTimelineLeft + rect.width * (positionPercent / 100); } else { // For non-zoomed timeline, use simple calculation - xPos = rect.left + (rect.width * (positionPercent / 100)); + xPos = rect.left + rect.width * (positionPercent / 100); } setTooltipPosition({ @@ -1167,11 +1247,15 @@ const TimelineControls = ({ const newTime = position * duration; // Log the position for debugging - logger.debug("Timeline clicked at:", formatDetailedTime(newTime), - "distance from end:", formatDetailedTime(duration - newTime)); + logger.debug( + "Timeline clicked at:", + formatDetailedTime(newTime), + "distance from end:", + formatDetailedTime(duration - newTime) + ); // Store position globally for iOS Safari (this is critical for first-time visits) - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.lastSeekedPosition = newTime; } @@ -1183,7 +1267,7 @@ const TimelineControls = ({ setDisplayTime(newTime); // Find if we clicked in a segment with a small tolerance for boundaries - const segmentAtClickedTime = clipSegments.find(seg => { + const segmentAtClickedTime = clipSegments.find((seg) => { // Standard check for being inside a segment const isInside = newTime >= seg.startTime && newTime <= seg.endTime; // Additional checks for being exactly at the start or end boundary (with small tolerance) @@ -1205,25 +1289,30 @@ const TimelineControls = ({ // Update the current segment index if we clicked into a segment if (segmentAtClickedTime) { const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const targetSegmentIndex = orderedSegments.findIndex(seg => seg.id === segmentAtClickedTime.id); - + const targetSegmentIndex = orderedSegments.findIndex( + (seg) => seg.id === segmentAtClickedTime.id + ); + if (targetSegmentIndex !== -1) { // Dispatch a custom event to update the current segment index - const updateSegmentIndexEvent = new CustomEvent('update-segment-index', { + const updateSegmentIndexEvent = new CustomEvent("update-segment-index", { detail: { segmentIndex: targetSegmentIndex } }); document.dispatchEvent(updateSegmentIndexEvent); - logger.debug(`Segments playback mode: updating segment index to ${targetSegmentIndex} for timeline click in segment ${segmentAtClickedTime.id}`); + logger.debug( + `Segments playback mode: updating segment index to ${targetSegmentIndex} for timeline click in segment ${segmentAtClickedTime.id}` + ); } } - + logger.debug("Segments playback mode: resuming playback after timeline click"); - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Resumed segments playback after timeline seeking"); }) - .catch(err => { + .catch((err) => { console.error("Error resuming segments playback:", err); setIsPlayingSegment(false); }); @@ -1231,12 +1320,13 @@ const TimelineControls = ({ // Resume playback if it was playing before (but not during segments playback) else if (wasPlaying && !isPlayingSegments) { logger.debug("Resuming playback after timeline click"); - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Resumed playback after seeking"); }) - .catch(err => { + .catch((err) => { console.error("Error resuming playback:", err); setIsPlayingSegment(false); }); @@ -1244,7 +1334,10 @@ const TimelineControls = ({ } // 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 || + (e.target as HTMLElement).classList.contains("timeline-thumbnail") + ) { // Check if there's a segment at the clicked position if (segmentAtClickedTime) { setSelectedSegmentId(segmentAtClickedTime.id); @@ -1263,7 +1356,7 @@ const TimelineControls = ({ // For zoomed timeline, calculate the visible position const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; const clickPosPercent = newTime / duration; - xPos = visibleTimelineLeft + (clickPosPercent * rect.width); + xPos = visibleTimelineLeft + clickPosPercent * rect.width; } else { // For 1x zoom, use the client X xPos = e.clientX; @@ -1271,7 +1364,7 @@ const TimelineControls = ({ setTooltipPosition({ x: xPos, - y: rect.top - 10 // Position tooltip above the timeline + y: rect.top - 10 // Position tooltip above the timeline }); // Always show the empty space tooltip in cutaway areas @@ -1279,8 +1372,8 @@ const TimelineControls = ({ // Log the cutaway area details const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const prevSegment = [...sortedSegments].reverse().find(seg => seg.endTime < newTime); - const nextSegment = sortedSegments.find(seg => seg.startTime > newTime); + const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < newTime); + const nextSegment = sortedSegments.find((seg) => seg.startTime > newTime); logger.debug("Clicked in cutaway area:", { position: formatDetailedTime(newTime), @@ -1293,330 +1386,384 @@ const TimelineControls = ({ }; // Handle segment resize - works with both mouse and touch events - const handleSegmentResize = (segmentId: number, isLeft: boolean) => (e: React.MouseEvent | React.TouchEvent) => { - // Remove the check that prevents interaction during preview mode - // This allows users to resize segments while previewing + const handleSegmentResize = + (segmentId: number, isLeft: boolean) => (e: React.MouseEvent | React.TouchEvent) => { + // Remove the check that prevents interaction during preview mode + // This allows users to resize segments while previewing - e.preventDefault(); - e.stopPropagation(); // Prevent triggering parent's events + e.preventDefault(); + e.stopPropagation(); // Prevent triggering parent's events - if (!timelineRef.current) return; + if (!timelineRef.current) return; - const timelineRect = timelineRef.current.getBoundingClientRect(); - const timelineWidth = timelineRect.width; + const timelineRect = timelineRef.current.getBoundingClientRect(); + const timelineWidth = timelineRect.width; - // Find the segment that's being resized - const segment = clipSegments.find(seg => seg.id === segmentId); - if (!segment) return; + // Find the segment that's being resized + const segment = clipSegments.find((seg) => seg.id === segmentId); + if (!segment) return; - const originalStartTime = segment.startTime; - const originalEndTime = segment.endTime; + const originalStartTime = segment.startTime; + const originalEndTime = segment.endTime; - // Store the original segment state to compare after dragging - const segmentBeforeDrag = {...segment}; + // Store the original segment state to compare after dragging + const segmentBeforeDrag = { ...segment }; - // Add a visual indicator that we're in resize mode (for mouse devices) - document.body.style.cursor = 'ew-resize'; + // Add a visual indicator that we're in resize mode (for mouse devices) + document.body.style.cursor = "ew-resize"; - // Add a temporary overlay to help with dragging outside the element - const overlay = document.createElement('div'); - overlay.style.position = 'fixed'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100vw'; - overlay.style.height = '100vh'; - overlay.style.zIndex = '1000'; - overlay.style.cursor = 'ew-resize'; - document.body.appendChild(overlay); + // Add a temporary overlay to help with dragging outside the element + const overlay = document.createElement("div"); + overlay.style.position = "fixed"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.zIndex = "1000"; + overlay.style.cursor = "ew-resize"; + document.body.appendChild(overlay); - // Track dragging state and final positions - let isDragging = true; - let finalStartTime = originalStartTime; - let finalEndTime = originalEndTime; + // Track dragging state and final positions + let isDragging = true; + let finalStartTime = originalStartTime; + let finalEndTime = originalEndTime; - // Dispatch an event to signal drag start - document.dispatchEvent(new CustomEvent('segment-drag-start', { - detail: { segmentId } - })); + // Dispatch an event to signal drag start + document.dispatchEvent( + new CustomEvent("segment-drag-start", { + detail: { segmentId } + }) + ); - // Keep the tooltip visible during drag - // Function to handle both mouse and touch movements - const handleDragMove = (clientX: number) => { - if (!isDragging || !timelineRef.current) return; + // Keep the tooltip visible during drag + // Function to handle both mouse and touch movements + const handleDragMove = (clientX: number) => { + if (!isDragging || !timelineRef.current) return; - const updatedTimelineRect = timelineRef.current.getBoundingClientRect(); - const position = Math.max(0, Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width)); - const newTime = position * duration; + const updatedTimelineRect = timelineRef.current.getBoundingClientRect(); + const position = Math.max( + 0, + Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width) + ); + 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: '' - }; + // 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 - const currentSegmentStart = isLeft ? newTime : originalStartTime; - const currentSegmentEnd = isLeft ? originalEndTime : newTime; - const isMarkerInSegment = currentTime >= currentSegmentStart && currentTime <= currentSegmentEnd; + // Check if the current marker position intersects with where the segment will be + const currentSegmentStart = isLeft ? newTime : originalStartTime; + const currentSegmentEnd = isLeft ? originalEndTime : newTime; + const isMarkerInSegment = + currentTime >= currentSegmentStart && currentTime <= currentSegmentEnd; - // Update tooltip based on marker intersection - if (isMarkerInSegment) { - // Show segment tooltip if marker is inside the segment - setSelectedSegmentId(segmentId); - setShowEmptySpaceTooltip(false); - } else { - // Show cutaway tooltip if marker is outside the segment - setSelectedSegmentId(null); - // Calculate available space for cutaway tooltip - const availableSpace = calculateAvailableSpace(currentTime); - setAvailableSegmentDuration(availableSpace); - setShowEmptySpaceTooltip(true); - } - - // Find neighboring segments (exclude the current one) - const otherSegments = clipSegments.filter(seg => seg.id !== segmentId); - - // Calculate new start/end times based on drag direction - let newStartTime = originalStartTime; - let newEndTime = originalEndTime; - - if (isLeft) { - // Dragging left handle - adjust start time - newStartTime = Math.min(newTime, originalEndTime - 0.5); - - // Find the closest left neighbor - const leftNeighbors = otherSegments - .filter(seg => seg.endTime <= originalStartTime) - .sort((a, b) => b.endTime - a.endTime); - - const leftNeighbor = leftNeighbors[0]; - - // Prevent overlapping with left neighbor - if (leftNeighbor && newStartTime < leftNeighbor.endTime) { - newStartTime = leftNeighbor.endTime; + // Update tooltip based on marker intersection + if (isMarkerInSegment) { + // Show segment tooltip if marker is inside the segment + setSelectedSegmentId(segmentId); + setShowEmptySpaceTooltip(false); + } else { + // Show cutaway tooltip if marker is outside the segment + setSelectedSegmentId(null); + // Calculate available space for cutaway tooltip + const availableSpace = calculateAvailableSpace(currentTime); + setAvailableSegmentDuration(availableSpace); + setShowEmptySpaceTooltip(true); } - // Snap to the nearest segment with a small threshold - const snapThreshold = 0.3; // seconds + // Find neighboring segments (exclude the current one) + const otherSegments = clipSegments.filter((seg) => seg.id !== segmentId); - if (leftNeighbor && Math.abs(newStartTime - leftNeighbor.endTime) < snapThreshold) { - newStartTime = leftNeighbor.endTime; - } + // Calculate new start/end times based on drag direction + let newStartTime = originalStartTime; + let newEndTime = originalEndTime; - // Update final value for history recording - finalStartTime = newStartTime; - } else { - // Dragging right handle - adjust end time - newEndTime = Math.max(newTime, originalStartTime + 0.5); + if (isLeft) { + // Dragging left handle - adjust start time + newStartTime = Math.min(newTime, originalEndTime - 0.5); - // Find the closest right neighbor - const rightNeighbors = otherSegments - .filter(seg => seg.startTime >= originalEndTime) - .sort((a, b) => a.startTime - b.startTime); + // Find the closest left neighbor + const leftNeighbors = otherSegments + .filter((seg) => seg.endTime <= originalStartTime) + .sort((a, b) => b.endTime - a.endTime); - const rightNeighbor = rightNeighbors[0]; + const leftNeighbor = leftNeighbors[0]; - // Prevent overlapping with right neighbor - if (rightNeighbor && newEndTime > rightNeighbor.startTime) { - newEndTime = rightNeighbor.startTime; - } - - // Snap to the nearest segment with a small threshold - const snapThreshold = 0.3; // seconds - - if (rightNeighbor && Math.abs(newEndTime - rightNeighbor.startTime) < snapThreshold) { - newEndTime = rightNeighbor.startTime; - } - - // Update final value for history recording - finalEndTime = newEndTime; - } - - // Create a new segments array with the updated segment - const updatedSegments = clipSegments.map(seg => { - if (seg.id === segmentId) { - return { - ...seg, - startTime: newStartTime, - endTime: newEndTime - }; - } - return seg; - }); - - // Create a custom event to update the segments WITHOUT recording in history during drag - const updateEvent = new CustomEvent('update-segments', { - detail: { - segments: updatedSegments, - recordHistory: false // Don't record intermediate states - } - }); - document.dispatchEvent(updateEvent); - - // During dragging, check if the current tooltip needs to be updated based on segment position - if (selectedSegmentId === segmentId && videoRef.current) { - const currentTime = videoRef.current.currentTime; - const segment = updatedSegments.find(seg => seg.id === segmentId); - - if (segment) { - // Check if playhead position is now outside the segment after dragging - const isInsideSegment = currentTime >= segment.startTime && currentTime <= segment.endTime; - - // Log the current position information for debugging - logger.debug(`During drag - playhead at ${formatDetailedTime(currentTime)} is ${isInsideSegment ? 'inside' : 'outside'} segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime(segment.endTime)})`); - - if (!isInsideSegment && isPlayingSegment) { - logger.debug("Playhead position is outside segment after dragging - updating tooltip"); - // Stop playback if we were playing and dragged the segment away from playhead - videoRef.current.pause(); - setIsPlayingSegment(false); - setActiveSegment(null); + // Prevent overlapping with left neighbor + if (leftNeighbor && newStartTime < leftNeighbor.endTime) { + newStartTime = leftNeighbor.endTime; } - // Update display time to stay in bounds of the segment - if (currentTime < segment.startTime) { - logger.debug(`Adjusting display time to segment start: ${formatDetailedTime(segment.startTime)}`); - setDisplayTime(segment.startTime); + // Snap to the nearest segment with a small threshold + const snapThreshold = 0.3; // seconds - // Update UI state to reflect that playback will be from segment start - setClickedTime(segment.startTime); - } else if (currentTime > segment.endTime) { - logger.debug(`Adjusting display time to segment end: ${formatDetailedTime(segment.endTime)}`); - setDisplayTime(segment.endTime); - - // Update UI state to reflect that playback will be from segment end - setClickedTime(segment.endTime); + if (leftNeighbor && Math.abs(newStartTime - leftNeighbor.endTime) < snapThreshold) { + newStartTime = leftNeighbor.endTime; } + + // Update final value for history recording + finalStartTime = newStartTime; + } else { + // Dragging right handle - adjust end time + newEndTime = Math.max(newTime, originalStartTime + 0.5); + + // Find the closest right neighbor + const rightNeighbors = otherSegments + .filter((seg) => seg.startTime >= originalEndTime) + .sort((a, b) => a.startTime - b.startTime); + + const rightNeighbor = rightNeighbors[0]; + + // Prevent overlapping with right neighbor + if (rightNeighbor && newEndTime > rightNeighbor.startTime) { + newEndTime = rightNeighbor.startTime; + } + + // Snap to the nearest segment with a small threshold + const snapThreshold = 0.3; // seconds + + if (rightNeighbor && Math.abs(newEndTime - rightNeighbor.startTime) < snapThreshold) { + newEndTime = rightNeighbor.startTime; + } + + // Update final value for history recording + finalEndTime = newEndTime; } - } - }; - // Function to handle the end of dragging (for both mouse and touch) - const handleDragEnd = () => { - if (!isDragging) return; + // Create a new segments array with the updated segment + const updatedSegments = clipSegments.map((seg) => { + if (seg.id === segmentId) { + return { + ...seg, + startTime: newStartTime, + endTime: newEndTime + }; + } + return seg; + }); - isDragging = false; + // Create a custom event to update the segments WITHOUT recording in history during drag + const updateEvent = new CustomEvent("update-segments", { + detail: { + segments: updatedSegments, + recordHistory: false // Don't record intermediate states + } + }); + document.dispatchEvent(updateEvent); - // Clean up event listeners for both mouse and touch - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('touchmove', handleTouchMove); - document.removeEventListener('touchend', handleTouchEnd); - document.removeEventListener('touchcancel', handleTouchEnd); + // During dragging, check if the current tooltip needs to be updated based on segment position + if (selectedSegmentId === segmentId && videoRef.current) { + const currentTime = videoRef.current.currentTime; + const segment = updatedSegments.find((seg) => seg.id === segmentId); - // Reset styles - document.body.style.cursor = ''; - if (document.body.contains(overlay)) { - document.body.removeChild(overlay); - } + if (segment) { + // Check if playhead position is now outside the segment after dragging + const isInsideSegment = + currentTime >= segment.startTime && currentTime <= segment.endTime; - // Record the final position in history as a single action - const finalSegments = clipSegments.map(seg => { - if (seg.id === segmentId) { - return { - ...seg, - startTime: finalStartTime, - endTime: finalEndTime - }; - } - return seg; - }); + // Log the current position information for debugging + logger.debug( + `During drag - playhead at ${formatDetailedTime(currentTime)} is ${ + isInsideSegment ? "inside" : "outside" + } segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime( + segment.endTime + )})` + ); - // Now we can create a history record for the complete drag operation - const actionType = isLeft ? 'adjust_segment_start' : 'adjust_segment_end'; - document.dispatchEvent(new CustomEvent('update-segments', { - detail: { - segments: finalSegments, - recordHistory: true, - action: actionType - } - })); - - // After drag is complete, do a final check to see if playhead is inside the segment - if (selectedSegmentId === segmentId && videoRef.current) { - const currentTime = videoRef.current.currentTime; - const segment = finalSegments.find(seg => seg.id === segmentId); - - if (segment) { - const isInsideSegment = currentTime >= segment.startTime && currentTime <= segment.endTime; - - logger.debug(`Drag complete - playhead at ${formatDetailedTime(currentTime)} is ${isInsideSegment ? 'inside' : 'outside'} segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime(segment.endTime)})`); - - // Check if playhead status changed during drag - const wasInsideSegmentBefore = currentTime >= segmentBeforeDrag.startTime && currentTime <= segmentBeforeDrag.endTime; - - logger.debug(`Playhead was ${wasInsideSegmentBefore ? 'inside' : 'outside'} segment before drag, now ${isInsideSegment ? 'inside' : 'outside'}`); - - // Update UI elements based on segment position - if (!isInsideSegment) { - // If we were playing and the playhead is now outside the segment, stop playback - if (isPlayingSegment) { + if (!isInsideSegment && isPlayingSegment) { + logger.debug( + "Playhead position is outside segment after dragging - updating tooltip" + ); + // Stop playback if we were playing and dragged the segment away from playhead videoRef.current.pause(); setIsPlayingSegment(false); setActiveSegment(null); - setContinuePastBoundary(false); - logger.debug("Stopped playback because playhead is outside segment after drag completion"); } - // Update display time to be within the segment's bounds + // Update display time to stay in bounds of the segment if (currentTime < segment.startTime) { - logger.debug(`Final adjustment - setting display time to segment start: ${formatDetailedTime(segment.startTime)}`); + logger.debug( + `Adjusting display time to segment start: ${formatDetailedTime(segment.startTime)}` + ); setDisplayTime(segment.startTime); + + // Update UI state to reflect that playback will be from segment start setClickedTime(segment.startTime); } else if (currentTime > segment.endTime) { - logger.debug(`Final adjustment - setting display time to segment end: ${formatDetailedTime(segment.endTime)}`); + logger.debug( + `Adjusting display time to segment end: ${formatDetailedTime(segment.endTime)}` + ); setDisplayTime(segment.endTime); + + // Update UI state to reflect that playback will be from segment end setClickedTime(segment.endTime); } } - // Special case: playhead was outside segment before, but now it's inside - can start playback - else if (!wasInsideSegmentBefore && isInsideSegment) { - logger.debug("Playhead moved INTO segment during drag - can start playback"); - setActiveSegment(segment); - // Removed automatic playback - user needs to manually start playback + } + }; + + // Function to handle the end of dragging (for both mouse and touch) + const handleDragEnd = () => { + if (!isDragging) return; + + isDragging = false; + + // Clean up event listeners for both mouse and touch + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + document.removeEventListener("touchcancel", handleTouchEnd); + + // Reset styles + document.body.style.cursor = ""; + if (document.body.contains(overlay)) { + document.body.removeChild(overlay); + } + + // Record the final position in history as a single action + const finalSegments = clipSegments.map((seg) => { + if (seg.id === segmentId) { + return { + ...seg, + startTime: finalStartTime, + endTime: finalEndTime + }; } - // Another special case: playhead was inside segment before, but now is also inside but at a different position - else if (wasInsideSegmentBefore && isInsideSegment && - (segment.startTime !== segmentBeforeDrag.startTime || segment.endTime !== segmentBeforeDrag.endTime)) { - logger.debug("Segment boundaries changed while playhead remained inside - updating activeSegment"); - // Update the active segment reference to ensure boundary detection works with new bounds - setActiveSegment(segment); + return seg; + }); + + // Now we can create a history record for the complete drag operation + const actionType = isLeft ? "adjust_segment_start" : "adjust_segment_end"; + document.dispatchEvent( + new CustomEvent("update-segments", { + detail: { + segments: finalSegments, + recordHistory: true, + action: actionType + } + }) + ); + + // After drag is complete, do a final check to see if playhead is inside the segment + if (selectedSegmentId === segmentId && videoRef.current) { + const currentTime = videoRef.current.currentTime; + const segment = finalSegments.find((seg) => seg.id === segmentId); + + if (segment) { + const isInsideSegment = + currentTime >= segment.startTime && currentTime <= segment.endTime; + + logger.debug( + `Drag complete - playhead at ${formatDetailedTime(currentTime)} is ${ + isInsideSegment ? "inside" : "outside" + } segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime( + segment.endTime + )})` + ); + + // Check if playhead status changed during drag + const wasInsideSegmentBefore = + currentTime >= segmentBeforeDrag.startTime && + currentTime <= segmentBeforeDrag.endTime; + + logger.debug( + `Playhead was ${ + wasInsideSegmentBefore ? "inside" : "outside" + } segment before drag, now ${isInsideSegment ? "inside" : "outside"}` + ); + + // Update UI elements based on segment position + if (!isInsideSegment) { + // If we were playing and the playhead is now outside the segment, stop playback + if (isPlayingSegment) { + videoRef.current.pause(); + setIsPlayingSegment(false); + setActiveSegment(null); + setContinuePastBoundary(false); + logger.debug( + "Stopped playback because playhead is outside segment after drag completion" + ); + } + + // Update display time to be within the segment's bounds + if (currentTime < segment.startTime) { + logger.debug( + `Final adjustment - setting display time to segment start: ${formatDetailedTime( + segment.startTime + )}` + ); + setDisplayTime(segment.startTime); + setClickedTime(segment.startTime); + } else if (currentTime > segment.endTime) { + logger.debug( + `Final adjustment - setting display time to segment end: ${formatDetailedTime( + segment.endTime + )}` + ); + setDisplayTime(segment.endTime); + setClickedTime(segment.endTime); + } + } + // Special case: playhead was outside segment before, but now it's inside - can start playback + else if (!wasInsideSegmentBefore && isInsideSegment) { + logger.debug("Playhead moved INTO segment during drag - can start playback"); + setActiveSegment(segment); + } + // Another special case: playhead was inside segment before, but now is also inside but at a different position + else if ( + wasInsideSegmentBefore && + isInsideSegment && + (segment.startTime !== segmentBeforeDrag.startTime || + segment.endTime !== segmentBeforeDrag.endTime) + ) { + logger.debug( + "Segment boundaries changed while playhead remained inside - updating activeSegment" + ); + // Update the active segment reference to ensure boundary detection works with new bounds + setActiveSegment(segment); + } } } - } - }; + }; - // Mouse-specific event handlers - const handleMouseMove = (moveEvent: MouseEvent) => { - handleDragMove(moveEvent.clientX); - }; + // Mouse-specific event handlers + const handleMouseMove = (moveEvent: MouseEvent) => { + handleDragMove(moveEvent.clientX); + }; - const handleMouseUp = () => { - handleDragEnd(); - }; + const handleMouseUp = () => { + handleDragEnd(); + }; - // Touch-specific event handlers - const handleTouchMove = (moveEvent: TouchEvent) => { - if (moveEvent.touches.length > 0) { - moveEvent.preventDefault(); // Prevent scrolling while dragging - handleDragMove(moveEvent.touches[0].clientX); - } - }; + // Touch-specific event handlers + const handleTouchMove = (moveEvent: TouchEvent) => { + if (moveEvent.touches.length > 0) { + moveEvent.preventDefault(); // Prevent scrolling while dragging + handleDragMove(moveEvent.touches[0].clientX); + } + }; - const handleTouchEnd = () => { - handleDragEnd(); - }; + const handleTouchEnd = () => { + handleDragEnd(); + }; - // Register event listeners for both mouse and touch - document.addEventListener('mousemove', handleMouseMove, { passive: false }); - document.addEventListener('mouseup', handleMouseUp); - document.addEventListener('touchmove', handleTouchMove, { passive: false }); - document.addEventListener('touchend', handleTouchEnd); - document.addEventListener('touchcancel', handleTouchEnd); - }; + // Register event listeners for both mouse and touch + document.addEventListener("mousemove", handleMouseMove, { + passive: false + }); + document.addEventListener("mouseup", handleMouseUp); + document.addEventListener("touchmove", handleTouchMove, { + passive: false + }); + document.addEventListener("touchend", handleTouchEnd); + document.addEventListener("touchcancel", handleTouchEnd); + }; // Handle segment click to show the tooltip const handleSegmentClick = (segmentId: number) => (e: React.MouseEvent) => { @@ -1624,7 +1771,7 @@ const TimelineControls = ({ // This allows users to click segments while previewing // Don't show tooltip if clicked on handle - if ((e.target as HTMLElement).classList.contains('clip-segment-handle')) { + if ((e.target as HTMLElement).classList.contains("clip-segment-handle")) { return; } @@ -1644,7 +1791,7 @@ const TimelineControls = ({ setSelectedSegmentId(segmentId); // Find the segment in our data - const segment = clipSegments.find(seg => seg.id === segmentId); + const segment = clipSegments.find((seg) => seg.id === segmentId); if (!segment) return; // Find the segment element in the DOM @@ -1655,7 +1802,7 @@ const TimelineControls = ({ const relativeX = (e.clientX - segmentRect.left) / segmentRect.width; // Convert to time based on segment's start and end times - const clickTime = segment.startTime + (relativeX * (segment.endTime - segment.startTime)); + const clickTime = segment.startTime + relativeX * (segment.endTime - segment.startTime); // Ensure time is within segment bounds const boundedTime = Math.max(segment.startTime, Math.min(segment.endTime, clickTime)); @@ -1669,22 +1816,38 @@ const TimelineControls = ({ if (videoRef.current) { const currentVideoTime = videoRef.current.currentTime; const isPlayheadInsideSegment = - currentVideoTime >= segment.startTime && - currentVideoTime <= segment.endTime; + currentVideoTime >= segment.startTime && currentVideoTime <= segment.endTime; - logger.debug(`Segment click - playhead at ${formatDetailedTime(currentVideoTime)} is ${isPlayheadInsideSegment ? 'inside' : 'outside'} segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime(segment.endTime)})`); + logger.debug( + `Segment click - playhead at ${formatDetailedTime(currentVideoTime)} is ${ + isPlayheadInsideSegment ? "inside" : "outside" + } segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime( + segment.endTime + )})` + ); // If playhead is outside the segment, update the display time to segment boundary if (!isPlayheadInsideSegment) { // Adjust the display time based on which end is closer to the playhead - if (Math.abs(currentVideoTime - segment.startTime) < Math.abs(currentVideoTime - segment.endTime)) { + if ( + Math.abs(currentVideoTime - segment.startTime) < + Math.abs(currentVideoTime - segment.endTime) + ) { // Playhead is closer to segment start - logger.debug(`Playhead outside segment - adjusting to segment start: ${formatDetailedTime(segment.startTime)}`); + logger.debug( + `Playhead outside segment - adjusting to segment start: ${formatDetailedTime( + segment.startTime + )}` + ); setDisplayTime(segment.startTime); // Don't update clickedTime here since we already set it to the clicked position } else { // Playhead is closer to segment end - logger.debug(`Playhead outside segment - adjusting to segment end: ${formatDetailedTime(segment.endTime)}`); + logger.debug( + `Playhead outside segment - adjusting to segment end: ${formatDetailedTime( + segment.endTime + )}` + ); setDisplayTime(segment.endTime); // Don't update clickedTime here since we already set it to the clicked position } @@ -1700,26 +1863,29 @@ const TimelineControls = ({ if (isPlayingSegments && wasPlaying) { // Update the current segment index for segments playback mode const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const targetSegmentIndex = orderedSegments.findIndex(seg => seg.id === segmentId); - + const targetSegmentIndex = orderedSegments.findIndex((seg) => seg.id === segmentId); + if (targetSegmentIndex !== -1) { // Dispatch a custom event to update the current segment index - const updateSegmentIndexEvent = new CustomEvent('update-segment-index', { + const updateSegmentIndexEvent = new CustomEvent("update-segment-index", { detail: { segmentIndex: targetSegmentIndex } }); document.dispatchEvent(updateSegmentIndexEvent); - logger.debug(`Segments playback mode: updating segment index to ${targetSegmentIndex} for segment ${segmentId}`); + logger.debug( + `Segments playback mode: updating segment index to ${targetSegmentIndex} for segment ${segmentId}` + ); } - + // In segments playback mode, we want to continue the segments playback from the new position // The segments playback will naturally handle continuing to the next segments logger.debug("Segments playback mode: continuing playback from new position"); - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Continued segments playback after segment click"); }) - .catch(err => { + .catch((err) => { console.error("Error continuing segments playback after segment click:", err); }); } @@ -1730,12 +1896,13 @@ const TimelineControls = ({ // Reset the continuePastBoundary flag when clicking on a segment to ensure boundaries work setContinuePastBoundary(false); // Continue playing from the new position - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Continued preview playback after segment click"); }) - .catch(err => { + .catch((err) => { console.error("Error resuming playback after segment click:", err); }); } @@ -1760,12 +1927,12 @@ const TimelineControls = ({ const clickedPosPixel = (boundedTime / duration) * timelineWidth; // Center the view on the clicked position - const targetScrollLeft = Math.max(0, clickedPosPixel - (containerWidth / 2)); + const targetScrollLeft = Math.max(0, clickedPosPixel - containerWidth / 2); // Smooth scroll to the clicked point scrollContainerRef.current.scrollTo({ left: targetScrollLeft, - behavior: 'smooth' + behavior: "smooth" }); // Update tooltip position after scrolling completes @@ -1774,7 +1941,8 @@ const TimelineControls = ({ // Calculate new position based on viewport const updatedRect = timelineRef.current.getBoundingClientRect(); const timePercent = boundedTime / duration; - const newPosition = (timePercent * timelineWidth) - scrollContainerRef.current.scrollLeft + updatedRect.left; + const newPosition = + timePercent * timelineWidth - scrollContainerRef.current.scrollLeft + updatedRect.left; setTooltipPosition({ x: newPosition, @@ -1809,78 +1977,84 @@ const TimelineControls = ({ return (
Segment {index + 1}
-
{formatTime(segment.startTime)} - {formatTime(segment.endTime)}
-
Duration: {formatTime(segment.endTime - segment.startTime)}
+
+ {formatTime(segment.startTime)} - {formatTime(segment.endTime)} +
+
+ Duration: {formatTime(segment.endTime - segment.startTime)} +
{/* Resize handles with both mouse and touch support */} - {isPlayingSegments ? null : <> -
{ - e.stopPropagation(); - handleSegmentResize(segment.id, true)(e); - }} - onTouchStart={(e) => { - e.stopPropagation(); - handleSegmentResize(segment.id, true)(e); - }} - >
-
{ - e.stopPropagation(); - handleSegmentResize(segment.id, false)(e); - }} - onTouchStart={(e) => { - e.stopPropagation(); - handleSegmentResize(segment.id, false)(e); - }} - >
- } + {isPlayingSegments ? null : ( + <> +
{ + e.stopPropagation(); + handleSegmentResize(segment.id, true)(e); + }} + onTouchStart={(e) => { + e.stopPropagation(); + handleSegmentResize(segment.id, true)(e); + }} + >
+
{ + e.stopPropagation(); + handleSegmentResize(segment.id, false)(e); + }} + onTouchStart={(e) => { + e.stopPropagation(); + handleSegmentResize(segment.id, false)(e); + }} + >
+ + )}
); }); }; - // Add a new useEffect hook to listen for segment deletion events + // Add a new useEffect hook to listen for segment deletion events useEffect(() => { const handleSegmentDelete = (event: CustomEvent) => { const { segmentId } = event.detail; // Check if this was the last segment before deletion - const remainingSegments = clipSegments.filter(seg => seg.id !== segmentId); + const remainingSegments = clipSegments.filter((seg) => seg.id !== segmentId); if (remainingSegments.length === 0) { // Create a full video segment const fullVideoSegment: Segment = { id: Date.now(), - name: 'Full Video', + name: "Full Video", startTime: 0, endTime: duration, - thumbnail: '' + thumbnail: "" }; // Create and dispatch the update event to replace all segments with the full video segment - const updateEvent = new CustomEvent('update-segments', { + const updateEvent = new CustomEvent("update-segments", { detail: { segments: [fullVideoSegment], recordHistory: true, - action: 'create_full_video_segment' + action: "create_full_video_segment" } }); document.dispatchEvent(updateEvent); @@ -1896,7 +2070,7 @@ const TimelineControls = ({ if (timelineRef.current) { const rect = timelineRef.current.getBoundingClientRect(); const posPercent = (currentTime / duration) * 100; - const xPosition = rect.left + (rect.width * (posPercent / 100)); + const xPosition = rect.left + rect.width * (posPercent / 100); setTooltipPosition({ x: xPosition, @@ -1911,7 +2085,7 @@ const TimelineControls = ({ } } else if (selectedSegmentId === segmentId) { // Handle normal segment deletion - const deletedSegment = clipSegments.find(seg => seg.id === segmentId); + const deletedSegment = clipSegments.find((seg) => seg.id === segmentId); if (!deletedSegment) return; // Calculate available space after deletion @@ -1926,7 +2100,7 @@ const TimelineControls = ({ if (timelineRef.current) { const rect = timelineRef.current.getBoundingClientRect(); const posPercent = (currentTime / duration) * 100; - const xPosition = rect.left + (rect.width * (posPercent / 100)); + const xPosition = rect.left + rect.width * (posPercent / 100); setTooltipPosition({ x: xPosition, @@ -1942,11 +2116,11 @@ const TimelineControls = ({ }; // Add event listener for the custom delete-segment event - document.addEventListener('delete-segment', handleSegmentDelete as EventListener); + document.addEventListener("delete-segment", handleSegmentDelete as EventListener); // Clean up event listener on component unmount return () => { - document.removeEventListener('delete-segment', handleSegmentDelete as EventListener); + document.removeEventListener("delete-segment", handleSegmentDelete as EventListener); }; }, [selectedSegmentId, clipSegments, currentTime, duration, timelineRef]); @@ -1970,7 +2144,7 @@ const TimelineControls = ({ let nextSegment = null; // First, check if we're inside a segment with high precision - currentSegment = clipSegments.find(seg => { + currentSegment = clipSegments.find((seg) => { const isWithinSegment = currentPosition >= seg.startTime && currentPosition <= seg.endTime; const isAtExactStart = Math.abs(currentPosition - seg.startTime) < 0.001; // Within 1ms of start const isAtExactEnd = Math.abs(currentPosition - seg.endTime) < 0.001; // Within 1ms of end @@ -1979,7 +2153,7 @@ const TimelineControls = ({ // Find the next segment with high precision nextSegment = clipSegments - .filter(seg => { + .filter((seg) => { const isAfterCurrent = seg.startTime > currentPosition; const isNotAtExactPosition = Math.abs(seg.startTime - currentPosition) > 0.001; return isAfterCurrent && isNotAtExactPosition; @@ -2047,7 +2221,7 @@ const TimelineControls = ({ } // Remove our boundary checker - video.removeEventListener('timeupdate', checkBoundary); + video.removeEventListener("timeupdate", checkBoundary); setIsPlaying(false); setIsPlayingSegment(false); // Reset continuePastBoundary flag when stopping at boundary @@ -2057,21 +2231,22 @@ const TimelineControls = ({ }; // Start our boundary checker - video.addEventListener('timeupdate', checkBoundary); + video.addEventListener("timeupdate", checkBoundary); // Start playing - video.play() + video + .play() .then(() => { setIsPlaying(true); setIsPlayingSegment(true); logger.debug("Playback started:", { from: formatDetailedTime(currentPosition), to: formatDetailedTime(stopTime), - currentSegment: currentSegment ? `Segment ${currentSegment.id}` : 'None', - nextSegment: nextSegment ? `Segment ${nextSegment.id}` : 'None' + currentSegment: currentSegment ? `Segment ${currentSegment.id}` : "None", + nextSegment: nextSegment ? `Segment ${nextSegment.id}` : "None" }); }) - .catch(err => { + .catch((err) => { console.error("Error playing video:", err); }); }; @@ -2081,12 +2256,12 @@ const TimelineControls = ({ setIsPlayingSegment(false); }; - video.addEventListener('play', handlePlay); - video.addEventListener('pause', handlePause); + video.addEventListener("play", handlePlay); + video.addEventListener("pause", handlePause); return () => { - video.removeEventListener('play', handlePlay); - video.removeEventListener('pause', handlePause); + video.removeEventListener("play", handlePlay); + video.removeEventListener("pause", handlePause); }; }, [clipSegments, duration, onSeek]); @@ -2102,7 +2277,7 @@ const TimelineControls = ({ const boundedTime = Math.max(0, Math.min(duration, time)); // Store position globally for iOS Safari - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.lastSeekedPosition = boundedTime; } }; @@ -2162,7 +2337,7 @@ const TimelineControls = ({ updateTooltipForPosition(newTime); // Store position globally for iOS Safari - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { (window as any).lastSeekedPosition = newTime; } @@ -2174,13 +2349,13 @@ const TimelineControls = ({ const handleMouseUp = () => { setIsDragging(false); isDraggingRef.current = false; // Update ref immediately - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); }; // Add event listeners to track movement and release - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); }; // Handle touch events for mobile devices @@ -2231,7 +2406,7 @@ const TimelineControls = ({ updateTooltipForPosition(newTime); // Store position globally for mobile browsers - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { (window as any).lastSeekedPosition = newTime; } @@ -2243,21 +2418,23 @@ const TimelineControls = ({ const handleTouchEnd = () => { setIsDragging(false); isDraggingRef.current = false; // Update ref immediately - document.removeEventListener('touchmove', handleTouchMove); - document.removeEventListener('touchend', handleTouchEnd); - document.removeEventListener('touchcancel', handleTouchEnd); + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + document.removeEventListener("touchcancel", handleTouchEnd); }; // Add event listeners to track movement and release - document.addEventListener('touchmove', handleTouchMove, { passive: false }); - document.addEventListener('touchend', handleTouchEnd); - document.addEventListener('touchcancel', handleTouchEnd); + document.addEventListener("touchmove", handleTouchMove, { + passive: false + }); + document.addEventListener("touchend", handleTouchEnd); + document.addEventListener("touchcancel", handleTouchEnd); }; // Add a useEffect to log the redirect URL whenever it changes useEffect(() => { if (redirectUrl) { - logger.debug('Redirect URL updated:', { + logger.debug("Redirect URL updated:", { redirectUrl, saveType, isSuccessModalOpen: showSuccessModal @@ -2277,7 +2454,7 @@ const TimelineControls = ({ // Update the countdown every second countdownInterval = setInterval(() => { secondsLeft--; - const countdownElement = document.querySelector('.countdown'); + const countdownElement = document.querySelector(".countdown"); if (countdownElement) { countdownElement.textContent = secondsLeft.toString(); } @@ -2293,7 +2470,7 @@ const TimelineControls = ({ if (onSave) onSave(); // Redirect to the URL - logger.debug('Automatically redirecting to:', redirectUrl); + logger.debug("Automatically redirecting to:", redirectUrl); window.location.href = redirectUrl; }, 10000); // 10 seconds } @@ -2306,24 +2483,28 @@ const TimelineControls = ({ }, [showSuccessModal, redirectUrl, onSave]); return ( -
+
{/* Current Timecode with Milliseconds */}
Timeline
- {/* Current time display removed as requested */}
- Total Segments: {formatDetailedTime(clipSegments.reduce((sum, segment) => sum + (segment.endTime - segment.startTime), 0))} + Total Segments:{" "} + + {formatDetailedTime( + clipSegments.reduce((sum, segment) => sum + (segment.endTime - segment.startTime), 0) + )} +
{/* Timeline Container with Scrollable Wrapper */}
1 ? 'auto' : 'hidden' + overflow: zoomLevel > 1 ? "auto" : "hidden" }} >
{/* Current Position Marker */} -
+
{/* Top circle for popup toggle */}
currentTime >= seg.startTime && currentTime <= seg.endTime + (seg) => currentTime >= seg.startTime && currentTime <= seg.endTime ); // Toggle tooltip visibility with a single click @@ -2370,35 +2548,29 @@ const TimelineControls = ({ }} > - {selectedSegmentId || showEmptySpaceTooltip ? '-' : '+'} + {selectedSegmentId || showEmptySpaceTooltip ? "-" : "+"}
{/* Bottom circle for dragging */} {isPlayingSegments ? null : ( -
- â‹® -
+
+ â‹® +
)}
{/* Trim Line Markers - hidden when segments exist */} {clipSegments.length === 0 && ( <> -
+
-
+
@@ -2416,59 +2588,96 @@ const TimelineControls = ({ {/* Segment Tooltip */} {selectedSegmentId !== null && (
{ + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} > {/* First row with time adjustment buttons */}
- -
{formatDetailedTime(displayTime)}
- + +
{ + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} + > + {formatDetailedTime(displayTime)} +
+
{/* Second row with action buttons */}
- {/*
@@ -2721,44 +3051,73 @@ const TimelineControls = ({ {/* Empty space tooltip - positioned absolutely within timeline container */} {showEmptySpaceTooltip && selectedSegmentId === null && (
{ + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} > {/* First row with time adjustment buttons - same as segment tooltip */}
-
{formatDetailedTime(clickedTime)}
+
{ + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} + > + {formatDetailedTime(clickedTime)} +
@@ -2768,8 +3127,24 @@ const TimelineControls = ({
{/* New segment button - Moved to first position */} {/* Go to start button - play from beginning of cutaway (until next segment) */} - {/* Play/Pause button for empty space */} {/* {/* Segment end adjustment button (always shown) */} {/* Segment start adjustment button (always shown) */}
@@ -3718,26 +4267,31 @@ const TimelineControls = ({ placeholder="00:00:00.000" data-tooltip="Enter time in format: hh:mm:ss.ms" onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { const input = e.currentTarget.value; try { // Parse time format like "00:30:15.250" or "30:15.250" or "30:15" - const parts = input.split(':'); - let hours = 0, minutes = 0, seconds = 0, milliseconds = 0; + const parts = input.split(":"); + let hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0; if (parts.length === 3) { // Format: HH:MM:SS.ms hours = parseInt(parts[0]); minutes = parseInt(parts[1]); - const secParts = parts[2].split('.'); + const secParts = parts[2].split("."); seconds = parseInt(secParts[0]); - if (secParts.length > 1) milliseconds = parseInt(secParts[1].padEnd(3, '0').substring(0, 3)); + if (secParts.length > 1) + milliseconds = parseInt(secParts[1].padEnd(3, "0").substring(0, 3)); } else if (parts.length === 2) { // Format: MM:SS.ms minutes = parseInt(parts[0]); - const secParts = parts[1].split('.'); + const secParts = parts[1].split("."); seconds = parseInt(secParts[0]); - if (secParts.length > 1) milliseconds = parseInt(secParts[1].padEnd(3, '0').substring(0, 3)); + if (secParts.length > 1) + milliseconds = parseInt(secParts[1].padEnd(3, "0").substring(0, 3)); } const totalSeconds = hours * 3600 + minutes * 60 + seconds + milliseconds / 1000; @@ -3747,8 +4301,9 @@ const TimelineControls = ({ // Create a helper function to show tooltip that uses the same logic as the millisecond buttons const showTooltipAtTime = (timeInSeconds: number) => { // Find the segment at the given time using improved matching - const segmentAtTime = clipSegments.find(seg => { - const isWithinSegment = timeInSeconds >= seg.startTime && timeInSeconds <= seg.endTime; + const segmentAtTime = clipSegments.find((seg) => { + const isWithinSegment = + timeInSeconds >= seg.startTime && timeInSeconds <= seg.endTime; const isAtExactStart = Math.abs(timeInSeconds - seg.startTime) < 0.001; // Within 1ms of start const isAtExactEnd = Math.abs(timeInSeconds - seg.endTime) < 0.001; // Within 1ms of end return isWithinSegment || isAtExactStart || isAtExactEnd; @@ -3763,16 +4318,21 @@ const TimelineControls = ({ if (zoomLevel > 1) { // For zoomed timeline, calculate position based on visible area - const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - const markerVisibleX = visibleTimelineLeft + ((timeInSeconds / duration) * rect.width); + const visibleTimelineLeft = + rect.left - scrollContainerRef.current.scrollLeft; + const markerVisibleX = + visibleTimelineLeft + (timeInSeconds / duration) * rect.width; xPos = markerVisibleX; } else { // For non-zoomed timeline, use the simple calculation - const positionPercent = (timeInSeconds / duration); - xPos = rect.left + (rect.width * positionPercent); + const positionPercent = timeInSeconds / duration; + xPos = rect.left + rect.width * positionPercent; } - setTooltipPosition({ x: xPos, y: rect.top - 10 }); + setTooltipPosition({ + x: xPos, + y: rect.top - 10 + }); setClickedTime(timeInSeconds); if (segmentAtTime) { @@ -3803,8 +4363,9 @@ const TimelineControls = ({ // Helper function to show the appropriate tooltip at the current time position const showTooltipAtCurrentTime = () => { // Find the segment at the current time (after seeking) - using improved matching for better precision - const segmentAtCurrentTime = clipSegments.find(seg => { - const isWithinSegment = currentTime >= seg.startTime && currentTime <= seg.endTime; + const segmentAtCurrentTime = clipSegments.find((seg) => { + const isWithinSegment = + currentTime >= seg.startTime && currentTime <= seg.endTime; const isAtExactStart = Math.abs(currentTime - seg.startTime) < 0.001; // Within 1ms of start const isAtExactEnd = Math.abs(currentTime - seg.endTime) < 0.001; // Within 1ms of end return isWithinSegment || isAtExactStart || isAtExactEnd; @@ -3820,15 +4381,19 @@ const TimelineControls = ({ if (zoomLevel > 1) { // For zoomed timeline, calculate position based on visible area const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - const markerVisibleX = visibleTimelineLeft + ((currentTime / duration) * rect.width); + const markerVisibleX = + visibleTimelineLeft + (currentTime / duration) * rect.width; xPos = markerVisibleX; } else { // For non-zoomed timeline, use the simple calculation - const positionPercent = (currentTime / duration); - xPos = rect.left + (rect.width * positionPercent); + const positionPercent = currentTime / duration; + xPos = rect.left + rect.width * positionPercent; } - setTooltipPosition({ x: xPos, y: rect.top - 10 }); + setTooltipPosition({ + x: xPos, + y: rect.top - 10 + }); setClickedTime(currentTime); if (segmentAtCurrentTime) { @@ -3935,11 +4500,19 @@ const TimelineControls = ({ {isZoomDropdownOpen && ( -
- {[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096].map(level => ( +
+ {[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096].map((level) => (
{ onZoomChange(level); setIsZoomDropdownOpen(false); @@ -4027,7 +4600,8 @@ const TimelineControls = ({ } >

- You're about to replace the original video with this trimmed version. This can't be undone. + You're about to replace the original video with this trimmed version. This can't be + undone.

@@ -4058,16 +4632,13 @@ const TimelineControls = ({ } >

- You're about to save a new copy with your edits. The original video will stay the same. Find the new file in your My Media folder - named after the original file. + You're about to save a new copy with your edits. The original video will stay the + same. Find the new file in your My Media folder - named after the original file.

{/* Processing Modal */} - {}} - title="Processing Video" - > + {}} title="Processing Video">
@@ -4103,7 +4674,8 @@ const TimelineControls = ({ } >

- You're about to save each segment as a separate video. Find the new files in your My Media folder - named after the original file. + You're about to save each segment as a separate video. Find the new files in your My + Media folder - named after the original file.

@@ -4122,9 +4694,11 @@ const TimelineControls = ({ {saveType === "segments" ? "You will be redirected to your " : "You will be redirected to your "} - media page + + media page + {" in "} - 10 seconds. {' '} + 10 seconds.{" "} {saveType === "segments" ? "The new video(s) will soon be there." : "Changes to the video might take a few minutes to be applied."} @@ -4140,22 +4714,40 @@ const TimelineControls = ({ >
- +
-

- {errorMessage} -

+

{errorMessage}