From 8df5ea880c41b17a45e9c296460b941aba3479df Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Mon, 28 Jul 2025 02:13:09 +0300 Subject: [PATCH] feat: Chapter editor main functionality and styling --- .../chapters-editor/client/src/App.tsx | 510 +++++++++--------- .../client/src/components/ClipSegments.tsx | 26 +- .../src/components/TimelineControls.tsx | 471 +++++----------- ...eVideoTrimmer.tsx => useVideoChapters.tsx} | 263 +++++---- .../client/src/styles/ClipSegments.css | 154 +++++- .../client/src/styles/TimelineControls.css | 171 +++++- .../vite.chapters-editor.config.ts | 2 - .../src/components/TimelineControls.tsx | 2 +- 8 files changed, 892 insertions(+), 707 deletions(-) rename frontend-tools/chapters-editor/client/src/hooks/{useVideoTrimmer.tsx => useVideoChapters.tsx} (83%) diff --git a/frontend-tools/chapters-editor/client/src/App.tsx b/frontend-tools/chapters-editor/client/src/App.tsx index a51f6379..905e697c 100644 --- a/frontend-tools/chapters-editor/client/src/App.tsx +++ b/frontend-tools/chapters-editor/client/src/App.tsx @@ -1,303 +1,277 @@ -import { useRef, useEffect, useState } from "react"; -import { formatTime, formatDetailedTime } from "./lib/timeUtils"; -import logger from "./lib/logger"; -import VideoPlayer from "@/components/VideoPlayer"; -import TimelineControls from "@/components/TimelineControls"; -import EditingTools from "@/components/EditingTools"; -import ClipSegments from "@/components/ClipSegments"; -import MobilePlayPrompt from "@/components/IOSPlayPrompt"; -import useVideoTrimmer from "@/hooks/useVideoTrimmer"; +import { formatDetailedTime } from './lib/timeUtils'; +import logger from './lib/logger'; +import VideoPlayer from '@/components/VideoPlayer'; +import TimelineControls from '@/components/TimelineControls'; +import EditingTools from '@/components/EditingTools'; +import ClipSegments from '@/components/ClipSegments'; +import MobilePlayPrompt from '@/components/IOSPlayPrompt'; +import useVideoChapters from '@/hooks/useVideoChapters'; const App = () => { - const { - videoRef, - currentTime, - duration, - isPlaying, - setIsPlaying, - isMuted, - thumbnails, - trimStart, - trimEnd, - splitPoints, - zoomLevel, - clipSegments, - hasUnsavedChanges, - historyPosition, - history, - handleTrimStartChange, - handleTrimEndChange, - handleZoomChange, - handleMobileSafeSeek, - handleSplit, - handleReset, - handleUndo, - handleRedo, - toggleMute, - handleSave, - handleSaveACopy, - handleSaveSegments, - isMobile, - videoInitialized, - setVideoInitialized, - isPlayingSegments, - handlePlaySegments - } = useVideoTrimmer(); + const { + videoRef, + currentTime, + duration, + isPlaying, + setIsPlaying, + isMuted, + thumbnails, + trimStart, + trimEnd, + splitPoints, + zoomLevel, + clipSegments, + selectedSegmentId, + hasUnsavedChanges, + historyPosition, + history, + handleTrimStartChange, + handleTrimEndChange, + handleZoomChange, + handleMobileSafeSeek, + handleSplit, + handleReset, + handleUndo, + handleRedo, + toggleMute, + handleSegmentUpdate, + handleChapterSave, + handleSelectedSegmentChange, + isMobile, + videoInitialized, + setVideoInitialized, + isPlayingSegments, + handlePlaySegments, + } = useVideoChapters(); - // Function to play from the beginning - const playFromBeginning = () => { - if (videoRef.current) { - videoRef.current.currentTime = 0; - handleMobileSafeSeek(0); - if (!isPlaying) { - handlePlay(); - } - } - }; + const handlePlay = () => { + if (!videoRef.current) return; - // Function to jump 15 seconds backward - const jumpBackward15 = () => { - const newTime = Math.max(0, currentTime - 15); - handleMobileSafeSeek(newTime); - }; + const video = videoRef.current; - // Function to jump 15 seconds forward - const jumpForward15 = () => { - const newTime = Math.min(duration, currentTime + 15); - handleMobileSafeSeek(newTime); - }; + // If already playing, just pause the video + if (isPlaying) { + video.pause(); + setIsPlaying(false); + return; + } - const handlePlay = () => { - if (!videoRef.current) return; + const currentPosition = Number(video.currentTime.toFixed(6)); - const video = videoRef.current; + // Find the next stopping point based on current position + let stopTime = duration; + let currentSegment = null; + let nextSegment = null; - // If already playing, just pause the video - if (isPlaying) { - video.pause(); - setIsPlaying(false); - return; - } + // Sort segments by start time to ensure correct order + const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const currentPosition = Number(video.currentTime.toFixed(6)); // Fix to microsecond precision + // First, check if we're inside a segment or exactly at its start/end + currentSegment = sortedSegments.find((seg) => { + const segStartTime = Number(seg.startTime.toFixed(6)); + const segEndTime = Number(seg.endTime.toFixed(6)); - // Find the next stopping point based on current position - let stopTime = duration; - let currentSegment = null; - let nextSegment = null; + // Check if we're inside the segment + if (currentPosition > segStartTime && currentPosition < segEndTime) { + return true; + } + // Check if we're exactly at the start + if (currentPosition === segStartTime) { + return true; + } + // Check if we're exactly at the end + if (currentPosition === segEndTime) { + // If we're at the end of a segment, we should look for the next one + return false; + } + return false; + }); - // Sort segments by start time to ensure correct order - const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); + // If we're not in a segment, find the next segment + if (!currentSegment) { + nextSegment = sortedSegments.find((seg) => { + const segStartTime = Number(seg.startTime.toFixed(6)); + return segStartTime > currentPosition; + }); + } - // First, check if we're inside a segment or exactly at its start/end - currentSegment = sortedSegments.find((seg) => { - const segStartTime = Number(seg.startTime.toFixed(6)); - const segEndTime = Number(seg.endTime.toFixed(6)); + // Determine where to stop based on position + if (currentSegment) { + // If we're in a segment, stop at its end + stopTime = Number(currentSegment.endTime.toFixed(6)); + } else if (nextSegment) { + // If we're in a cutaway and there's a next segment, stop at its start + stopTime = Number(nextSegment.startTime.toFixed(6)); + } - // Check if we're inside the segment - if (currentPosition > segStartTime && currentPosition < segEndTime) { - return true; - } - // Check if we're exactly at the start - if (currentPosition === segStartTime) { - return true; - } - // Check if we're exactly at the end - if (currentPosition === segEndTime) { - // If we're at the end of a segment, we should look for the next one - return false; - } - return false; - }); + // Create a boundary checker function with high precision + const checkBoundary = () => { + if (!video) return; - // If we're not in a segment, find the next segment - if (!currentSegment) { - nextSegment = sortedSegments.find((seg) => { - const segStartTime = Number(seg.startTime.toFixed(6)); - return segStartTime > currentPosition; - }); - } + const currentPosition = Number(video.currentTime.toFixed(6)); + const timeLeft = Number((stopTime - currentPosition).toFixed(6)); - // Determine where to stop based on position - if (currentSegment) { - // If we're in a segment, stop at its end - stopTime = Number(currentSegment.endTime.toFixed(6)); - } else if (nextSegment) { - // If we're in a cutaway and there's a next segment, stop at its start - stopTime = Number(nextSegment.startTime.toFixed(6)); - } + // If we've reached or passed the boundary + if (timeLeft <= 0 || currentPosition >= stopTime) { + // First pause playback + video.pause(); - // Create a boundary checker function with high precision - const checkBoundary = () => { - if (!video) return; + // Force exact position with multiple verification attempts + const setExactPosition = () => { + if (!video) return; - const currentPosition = Number(video.currentTime.toFixed(6)); - const timeLeft = Number((stopTime - currentPosition).toFixed(6)); + // Set to exact boundary time + video.currentTime = stopTime; + handleMobileSafeSeek(stopTime); - // If we've reached or passed the boundary - if (timeLeft <= 0 || currentPosition >= stopTime) { - // First pause playback - video.pause(); + const actualPosition = Number(video.currentTime.toFixed(6)); + const difference = Number(Math.abs(actualPosition - stopTime).toFixed(6)); - // Force exact position with multiple verification attempts - const setExactPosition = () => { - if (!video) return; + logger.debug('Position verification:', { + target: formatDetailedTime(stopTime), + actual: formatDetailedTime(actualPosition), + difference: difference, + }); - // Set to exact boundary time - video.currentTime = stopTime; - handleMobileSafeSeek(stopTime); + // If we're not exactly at the target position, try one more time + if (difference > 0) { + video.currentTime = stopTime; + handleMobileSafeSeek(stopTime); + } + }; - const actualPosition = Number(video.currentTime.toFixed(6)); - const difference = Number(Math.abs(actualPosition - stopTime).toFixed(6)); + // Multiple attempts to ensure precision, with increasing delays + setExactPosition(); + setTimeout(setExactPosition, 5); // Quick first retry + setTimeout(setExactPosition, 10); // Second retry + setTimeout(setExactPosition, 20); // Third retry if needed + setTimeout(setExactPosition, 50); // Final verification - logger.debug("Position verification:", { - target: formatDetailedTime(stopTime), - actual: formatDetailedTime(actualPosition), - difference: difference - }); + // Remove our boundary checker + video.removeEventListener('timeupdate', checkBoundary); + setIsPlaying(false); - // If we're not exactly at the target position, try one more time - if (difference > 0) { - video.currentTime = stopTime; - handleMobileSafeSeek(stopTime); - } + // Log the final position for debugging + logger.debug('Stopped at position:', { + target: formatDetailedTime(stopTime), + actual: formatDetailedTime(video.currentTime), + type: currentSegment ? 'segment end' : nextSegment ? 'next segment start' : 'end of video', + segment: currentSegment + ? { + id: currentSegment.id, + start: formatDetailedTime(currentSegment.startTime), + end: formatDetailedTime(currentSegment.endTime), + } + : null, + nextSegment: nextSegment + ? { + id: nextSegment.id, + start: formatDetailedTime(nextSegment.startTime), + end: formatDetailedTime(nextSegment.endTime), + } + : null, + }); + + return; + } }; - // Multiple attempts to ensure precision, with increasing delays - setExactPosition(); - setTimeout(setExactPosition, 5); // Quick first retry - setTimeout(setExactPosition, 10); // Second retry - setTimeout(setExactPosition, 20); // Third retry if needed - setTimeout(setExactPosition, 50); // Final verification + // Start our boundary checker + video.addEventListener('timeupdate', checkBoundary); - // Remove our boundary checker - video.removeEventListener("timeupdate", checkBoundary); - setIsPlaying(false); - - // Log the final position for debugging - logger.debug("Stopped at position:", { - target: formatDetailedTime(stopTime), - actual: formatDetailedTime(video.currentTime), - type: currentSegment - ? "segment end" - : nextSegment - ? "next segment start" - : "end of video", - segment: currentSegment - ? { - id: currentSegment.id, - start: formatDetailedTime(currentSegment.startTime), - end: formatDetailedTime(currentSegment.endTime) - } - : null, - nextSegment: nextSegment - ? { - id: nextSegment.id, - start: formatDetailedTime(nextSegment.startTime), - end: formatDetailedTime(nextSegment.endTime) - } - : null - }); - - return; - } + // Start playing + video + .play() + .then(() => { + setIsPlaying(true); + setVideoInitialized(true); + logger.debug('Playback started:', { + from: formatDetailedTime(currentPosition), + to: formatDetailedTime(stopTime), + currentSegment: currentSegment + ? { + id: currentSegment.id, + start: formatDetailedTime(currentSegment.startTime), + end: formatDetailedTime(currentSegment.endTime), + } + : 'None', + nextSegment: nextSegment + ? { + id: nextSegment.id, + start: formatDetailedTime(nextSegment.startTime), + end: formatDetailedTime(nextSegment.endTime), + } + : 'None', + }); + }) + .catch((err) => { + console.error('Error playing video:', err); + }); }; - // Start our boundary checker - video.addEventListener("timeupdate", checkBoundary); + return ( +
+ - // Start playing - video - .play() - .then(() => { - setIsPlaying(true); - setVideoInitialized(true); - logger.debug("Playback started:", { - from: formatDetailedTime(currentPosition), - to: formatDetailedTime(stopTime), - currentSegment: currentSegment - ? { - id: currentSegment.id, - start: formatDetailedTime(currentSegment.startTime), - end: formatDetailedTime(currentSegment.endTime) - } - : "None", - nextSegment: nextSegment - ? { - id: nextSegment.id, - start: formatDetailedTime(nextSegment.startTime), - end: formatDetailedTime(nextSegment.endTime) - } - : "None" - }); - }) - .catch((err) => { - console.error("Error playing video:", err); - }); - }; +
+ {/* Video Player */} + - return ( -
- + {/* Editing Tools */} + 0} + canRedo={historyPosition < history.length - 1} + /> -
- {/* Video Player */} - + {/* Timeline Controls */} + - {/* Editing Tools */} - 0} - canRedo={historyPosition < history.length - 1} - /> - - {/* Timeline Controls */} - - - {/* Clip Segments */} - -
-
- ); + {/* Clip Segments */} + +
+
+ ); }; export default App; diff --git a/frontend-tools/chapters-editor/client/src/components/ClipSegments.tsx b/frontend-tools/chapters-editor/client/src/components/ClipSegments.tsx index 02bb8c23..45fc87ac 100644 --- a/frontend-tools/chapters-editor/client/src/components/ClipSegments.tsx +++ b/frontend-tools/chapters-editor/client/src/components/ClipSegments.tsx @@ -7,13 +7,15 @@ export interface Segment { startTime: number; endTime: number; thumbnail: string; + chapterTitle?: string; } interface ClipSegmentsProps { segments: Segment[]; + selectedSegmentId?: number | null; } -const ClipSegments = ({ segments }: ClipSegmentsProps) => { +const ClipSegments = ({ segments, selectedSegmentId }: ClipSegmentsProps) => { // Sort segments by startTime const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime); @@ -33,19 +35,31 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => { return `segment-default-color segment-color-${(index % 8) + 1}`; }; + // Get selected segment + const selectedSegment = sortedSegments.find((seg) => seg.id === selectedSegmentId); + return (
-

Clip Segments

+

Chapters

{sortedSegments.map((segment, index) => ( -
+
-
Segment {index + 1}
+
+ {segment.chapterTitle ? ( + {segment.chapterTitle} + ) : ( + Chapter {index + 1} + )} +
{formatTime(segment.startTime)} - {formatTime(segment.endTime)}
@@ -74,7 +88,9 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => { ))} {sortedSegments.length === 0 && ( -
No segments created yet. Use the split button to create segments.
+
+ No chapters created yet. Use the split button to create chapter segments. +
)}
); diff --git a/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx b/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx index ab057cc3..a519e8ef 100644 --- a/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx +++ b/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx @@ -35,19 +35,20 @@ interface TimelineControlsProps { splitPoints: number[]; zoomLevel: number; clipSegments: Segment[]; + selectedSegmentId?: number | null; + onSelectedSegmentChange?: (segmentId: number | null) => void; + onSegmentUpdate?: (segmentId: number, updates: Partial) => void; + onChapterSave?: (chapters: { name: string; from: string; to: string }[]) => void; onTrimStartChange: (time: number) => void; onTrimEndChange: (time: number) => void; onZoomChange: (level: number) => void; onSeek: (time: number) => void; videoRef: React.RefObject; - onSave?: () => void; - onSaveACopy?: () => void; - onSaveSegments?: () => void; hasUnsavedChanges?: boolean; isIOSUninitialized?: boolean; isPlaying: boolean; setIsPlaying: (playing: boolean) => void; - onPlayPause: () => void; // Add this prop + onPlayPause: () => void; isPlayingSegments?: boolean; } @@ -109,14 +110,15 @@ const TimelineControls = ({ splitPoints, zoomLevel, clipSegments, + selectedSegmentId: externalSelectedSegmentId, + onSelectedSegmentChange, + onSegmentUpdate, + onChapterSave, onTrimStartChange, onTrimEndChange, onZoomChange, onSeek, videoRef, - onSave, - onSaveACopy, - onSaveSegments, hasUnsavedChanges = false, isIOSUninitialized = false, isPlaying, @@ -127,7 +129,17 @@ const TimelineControls = ({ const timelineRef = useRef(null); const leftHandleRef = useRef(null); const rightHandleRef = useRef(null); - const [selectedSegmentId, setSelectedSegmentId] = useState(null); + // Use external selectedSegmentId if provided, otherwise use internal state + const [internalSelectedSegmentId, setInternalSelectedSegmentId] = useState(null); + const selectedSegmentId = + externalSelectedSegmentId !== undefined ? externalSelectedSegmentId : internalSelectedSegmentId; + const setSelectedSegmentId = (segmentId: number | null) => { + if (onSelectedSegmentChange) { + onSelectedSegmentChange(segmentId); + } else { + setInternalSelectedSegmentId(segmentId); + } + }; const [showEmptySpaceTooltip, setShowEmptySpaceTooltip] = useState(false); const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }); const [clickedTime, setClickedTime] = useState(0); @@ -142,6 +154,49 @@ const TimelineControls = ({ // Reference for the scrollable container const scrollContainerRef = useRef(null); + // Chapter editor state + const [editingChapterTitle, setEditingChapterTitle] = useState(''); + const [chapterHasUnsavedChanges, setChapterHasUnsavedChanges] = useState(false); + + // Sort segments by startTime for chapter editor + const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); + const selectedSegment = sortedSegments.find((seg) => seg.id === selectedSegmentId); + + // Update editing title when selected segment changes + useEffect(() => { + if (selectedSegment) { + setEditingChapterTitle(selectedSegment.chapterTitle || ''); + } else { + setEditingChapterTitle(''); + } + }, [selectedSegmentId, selectedSegment]); + + // Handle chapter title change + const handleChapterTitleChange = (value: string) => { + setEditingChapterTitle(value); + setChapterHasUnsavedChanges(true); + + // Update the segment immediately + if (selectedSegmentId && onSegmentUpdate) { + onSegmentUpdate(selectedSegmentId, { chapterTitle: value }); + } + }; + + // Handle save chapters + const handleSaveChapters = () => { + if (!onChapterSave) return; + + // Convert segments to chapter format + const chapters = sortedSegments.map((segment, index) => ({ + name: segment.chapterTitle || `Chapter ${index + 1}`, + from: formatDetailedTime(segment.startTime), + to: formatDetailedTime(segment.endTime), + })); + + onChapterSave(chapters); + setChapterHasUnsavedChanges(false); + }; + // Helper function for time adjustment buttons to maintain playback state const handleTimeAdjustment = (offsetSeconds: number) => (e: React.MouseEvent) => { e.stopPropagation(); @@ -310,16 +365,14 @@ const TimelineControls = ({ }; // Modal states - const [showSaveModal, setShowSaveModal] = useState(false); - const [showSaveAsModal, setShowSaveAsModal] = useState(false); - const [showSaveSegmentsModal, setShowSaveSegmentsModal] = useState(false); + const [showSaveChaptersModal, setShowSaveChaptersModal] = useState(false); const [showProcessingModal, setShowProcessingModal] = useState(false); const [showSuccessModal, setShowSuccessModal] = useState(false); const [showErrorModal, setShowErrorModal] = useState(false); const [successMessage, setSuccessMessage] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [redirectUrl, setRedirectUrl] = useState(''); - const [saveType, setSaveType] = useState<'save' | 'copy' | 'segments'>('save'); + const [saveType, setSaveType] = useState<'chapters'>('chapters'); // Calculate positions as percentages const currentTimePercent = duration > 0 ? (currentTime / duration) * 100 : 0; @@ -328,218 +381,55 @@ const TimelineControls = ({ // No need for an extra effect here as we handle displayTime updates in the segment playback effect - // Save and API handlers - const handleSaveConfirm = async () => { + // Save Chapters handler + const handleSaveChaptersConfirm = async () => { // Close confirmation modal and show processing modal - setShowSaveModal(false); + setShowSaveChaptersModal(false); setShowProcessingModal(true); - setSaveType('save'); + setSaveType('chapters'); try { - // Format segments data for API request - const segments = clipSegments.map((segment) => ({ - startTime: formatDetailedTime(segment.startTime), - endTime: formatDetailedTime(segment.endTime), - })); + // Format chapters data for API request + const chapters = clipSegments + .filter((segment) => segment.chapterTitle && segment.chapterTitle.trim()) + .map((segment) => ({ + name: segment.chapterTitle || `Chapter ${segment.id}`, + from: formatDetailedTime(segment.startTime), + to: 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; + if (chapters.length === 0) { + setErrorMessage('No chapters with titles found'); + setShowErrorModal(true); + setShowProcessingModal(false); + return; + } - // Log the request details for debugging - logger.debug('Save request:', { - mediaId, - segments, - saveAsCopy: false, - redirectURL, - }); + // Call the onChapterSave function if provided + if (onChapterSave) { + await onChapterSave(chapters); + setShowProcessingModal(false); + setSuccessMessage('Chapters saved successfully!'); - const response = await trimVideo(mediaId, { - segments, - saveAsCopy: false, - }); + // Set redirect URL to media page + const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null; + if (mediaId) { + setRedirectUrl(`/view?m=${mediaId}`); + } - // Log the response for debugging - logger.debug('Save response:', response); - - // Hide processing modal - setShowProcessingModal(false); - - // Check if response indicates success (200 OK) - if (response.status === 200) { - // For "Save", use the redirectURL from the window or response - const finalRedirectUrl = redirectURL || response.url_redirect; - logger.debug('Using redirect URL:', finalRedirectUrl); - - setRedirectUrl(finalRedirectUrl); - setSuccessMessage('Video saved successfully!'); - - // Show success modal setShowSuccessModal(true); - } else if (response.status === 400) { - // Set error message from response and show error modal - const errorMsg = response.error || 'An error occurred during processing'; - logger.debug('Save error (400):', errorMsg); - setErrorMessage(errorMsg); - setShowErrorModal(true); } else { - // Handle other status codes as needed - logger.debug('Save error (unknown status):', response); - setErrorMessage('An unexpected error occurred'); + setErrorMessage('Chapter save function not available'); setShowErrorModal(true); + setShowProcessingModal(false); } } catch (error) { - logger.error('Error processing video:', error); + logger.error('Error saving chapters:', error); setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : 'An error occurred during processing'; - logger.debug('Save error (exception):', errorMsg); - setErrorMessage(errorMsg); - setShowErrorModal(true); - } - }; - - const handleSaveAsCopyConfirm = async () => { - // Close confirmation modal and show processing modal - setShowSaveAsModal(false); - setShowProcessingModal(true); - setSaveType('copy'); - - try { - // Format segments data for API request - 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; - - // Log the request details for debugging - logger.debug('Save as copy request:', { - mediaId, - segments, - saveAsCopy: true, - redirectUserMediaURL, - }); - - const response = await trimVideo(mediaId, { - segments, - saveAsCopy: true, - }); - - // Log the response for debugging - logger.debug('Save as copy response:', response); - - // Hide processing modal - setShowProcessingModal(false); - - // Check if response indicates success (200 OK) - if (response.status === 200) { - // For "Save As Copy", use the redirectUserMediaURL from the window - const finalRedirectUrl = redirectUserMediaURL || response.url_redirect; - logger.debug('Using redirect user media URL:', finalRedirectUrl); - - setRedirectUrl(finalRedirectUrl); - setSuccessMessage('Video saved as a new copy!'); - - // Show success modal - setShowSuccessModal(true); - } else if (response.status === 400) { - // Set error message from response and show error modal - const errorMsg = response.error || 'An error occurred during processing'; - logger.debug('Save as copy error (400):', errorMsg); - setErrorMessage(errorMsg); - setShowErrorModal(true); - } else { - // Handle other status codes as needed - logger.debug('Save as copy error (unknown status):', response); - setErrorMessage('An unexpected error occurred'); - setShowErrorModal(true); - } - } catch (error) { - logger.error('Error processing video:', error); - setShowProcessingModal(false); - - // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : 'An error occurred during processing'; - logger.debug('Save as copy error (exception):', errorMsg); - setErrorMessage(errorMsg); - setShowErrorModal(true); - } - }; - - const handleSaveSegmentsConfirm = async () => { - // Close confirmation modal and show processing modal - setShowSaveSegmentsModal(false); - setShowProcessingModal(true); - setSaveType('segments'); - - try { - // Format segments data for API request, with each segment saved as a separate file - 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; - - // Log the request details for debugging - logger.debug('Save segments request:', { - mediaId, - segments, - saveAsCopy: true, - saveIndividualSegments: true, - redirectUserMediaURL, - }); - - const response = await trimVideo(mediaId, { - segments, - saveAsCopy: true, - saveIndividualSegments: true, - }); - - // Log the response for debugging - logger.debug('Save segments response:', response); - - // Hide processing modal - setShowProcessingModal(false); - - // Check if response indicates success (200 OK) - if (response.status === 200) { - // For "Save Segments", use the redirectUserMediaURL from the window - const finalRedirectUrl = redirectUserMediaURL || response.url_redirect; - logger.debug('Using redirect user media URL for segments:', finalRedirectUrl); - - setRedirectUrl(finalRedirectUrl); - setSuccessMessage(`${segments.length} segments saved successfully!`); - - // Show success modal - setShowSuccessModal(true); - } else if (response.status === 400) { - // Set error message from response and show error modal - const errorMsg = response.error || 'An error occurred during processing'; - logger.debug('Save segments error (400):', errorMsg); - setErrorMessage(errorMsg); - setShowErrorModal(true); - } else { - // Handle other status codes as needed - logger.debug('Save segments error (unknown status):', response); - setErrorMessage('An unexpected error occurred'); - setShowErrorModal(true); - } - } catch (error) { - // Handle errors - logger.error('Error processing video segments:', error); - setShowProcessingModal(false); - - // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : 'An error occurred during processing'; - logger.debug('Save segments error (exception):', errorMsg); + const errorMsg = error instanceof Error ? error.message : 'An error occurred while saving chapters'; + logger.debug('Save chapters error (exception):', errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); } @@ -2407,9 +2297,6 @@ const TimelineControls = ({ // Set redirect timeout redirectTimeout = setTimeout(() => { - // Reset unsaved changes flag before navigating away - if (onSave) onSave(); - // Redirect to the URL logger.debug('Automatically redirecting to:', redirectUrl); window.location.href = redirectUrl; @@ -2421,7 +2308,15 @@ const TimelineControls = ({ if (countdownInterval) clearInterval(countdownInterval); if (redirectTimeout) clearTimeout(redirectTimeout); }; - }, [showSuccessModal, redirectUrl, onSave]); + }, [showSuccessModal, redirectUrl]); + + // Effect to handle redirect after success modal is closed + useEffect(() => { + if (!showSuccessModal && redirectUrl) { + logger.debug('Redirecting to:', redirectUrl); + window.location.href = redirectUrl; + } + }, [redirectUrl, saveType, showSuccessModal]); return (
@@ -2543,6 +2438,23 @@ const TimelineControls = ({ } }} > + {/* Chapter Editor for this segment */} + {selectedSegmentId && ( +
+