diff --git a/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx b/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx index 655ee51a..7ae9bde2 100644 --- a/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx +++ b/frontend-tools/chapters-editor/client/src/components/TimelineControls.tsx @@ -508,18 +508,17 @@ const TimelineControls = ({ to: formatDetailedTime(segment.endTime), })); - if (chapters.length === 0) { - setErrorMessage('No chapters with titles found'); - setShowErrorModal(true); - setShowProcessingModal(false); - return; - } - + // Allow saving even when no chapters exist (will send empty array) // Call the onChapterSave function if provided if (onChapterSave) { await onChapterSave(chapters); setShowProcessingModal(false); - setSuccessMessage('Chapters saved successfully!'); + + if (chapters.length === 0) { + setSuccessMessage('All chapters cleared successfully!'); + } else { + setSuccessMessage('Chapters saved successfully!'); + } // Set redirect URL to media page const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null; @@ -1975,48 +1974,12 @@ const TimelineControls = ({ // Check if this was the last segment before deletion const remainingSegments = clipSegments.filter((seg) => seg.id !== segmentId); if (remainingSegments.length === 0) { - // Create a full video segment - const fullVideoSegment: Segment = { - id: Date.now(), - chapterTitle: 'Full Video', - startTime: 0, - endTime: duration, - }; - - // Create and dispatch the update event to replace all segments with the full video segment - const updateEvent = new CustomEvent('update-segments', { - detail: { - segments: [fullVideoSegment], - recordHistory: true, - action: 'create_full_video_segment', - }, - }); - document.dispatchEvent(updateEvent); - - // Update UI to show the segment tooltip - setSelectedSegmentId(fullVideoSegment.id); + // Allow empty state - clear all UI state + setSelectedSegmentId(null); setShowEmptySpaceTooltip(false); - setClickedTime(currentTime); - setDisplayTime(currentTime); - setActiveSegment(fullVideoSegment); - - // Calculate tooltip position at current time - if (timelineRef.current) { - const rect = timelineRef.current.getBoundingClientRect(); - const posPercent = (currentTime / duration) * 100; - const xPosition = rect.left + rect.width * (posPercent / 100); - - setTooltipPosition({ - x: xPosition, - y: rect.top - 10, - }); - - logger.debug('Created full video segment:', { - id: fullVideoSegment.id, - duration: formatDetailedTime(duration), - currentPosition: formatDetailedTime(currentTime), - }); - } + setActiveSegment(null); + + logger.debug('All segments deleted - entering empty state'); } else if (selectedSegmentId === segmentId) { // Handle normal segment deletion const deletedSegment = clipSegments.find((seg) => seg.id === segmentId); @@ -3984,10 +3947,13 @@ const TimelineControls = ({ @@ -4008,15 +3974,22 @@ const TimelineControls = ({ className="modal-button modal-button-primary" onClick={handleSaveChaptersConfirm} > - Save Chapters + {clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0 + ? 'Clear Chapters' + : 'Save Chapters'} } >

- Are you sure you want to save the chapters? This will save{' '} - {clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length} chapters to the - database. + {(() => { + const chaptersWithTitles = clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length; + if (chaptersWithTitles === 0) { + return "Are you sure you want to clear all chapters? This will remove all existing chapters from the database."; + } else { + return `Are you sure you want to save the chapters? This will save ${chaptersWithTitles} chapters to the database.`; + } + })()}

diff --git a/frontend-tools/chapters-editor/client/src/hooks/useVideoChapters.tsx b/frontend-tools/chapters-editor/client/src/hooks/useVideoChapters.tsx index f1dadebd..63befaa3 100644 --- a/frontend-tools/chapters-editor/client/src/hooks/useVideoChapters.tsx +++ b/frontend-tools/chapters-editor/client/src/hooks/useVideoChapters.tsx @@ -147,15 +147,8 @@ const useVideoChapters = () => { initialSegments.push(segment); } } else { - - const initialSegment: Segment = { - id: 1, - chapterTitle: '', - startTime: 0, - endTime: video.duration, - }; - - initialSegments = [initialSegment]; + // Start with empty state - no default segment + initialSegments = []; } // Initialize history state with the segments @@ -274,24 +267,17 @@ const useVideoChapters = () => { // Check if we now have duration and initialize if needed if (video.duration > 0 && clipSegments.length === 0) { - logger.debug('Safari: Successfully initialized metadata, creating default segment'); + logger.debug('Safari: Successfully initialized metadata with empty state'); - const defaultSegment: Segment = { - id: 1, - chapterTitle: '', - startTime: 0, - endTime: video.duration, - }; - setDuration(video.duration); setTrimEnd(video.duration); - setClipSegments([defaultSegment]); + setClipSegments([]); const initialState: EditorState = { trimStart: 0, trimEnd: video.duration, splitPoints: [], - clipSegments: [defaultSegment], + clipSegments: [], }; setHistory([initialState]); @@ -680,21 +666,13 @@ const useVideoChapters = () => { const newSegments = clipSegments.filter((segment) => segment.id !== segmentId); if (newSegments.length !== clipSegments.length) { - // If all segments are deleted, create a new full video segment - if (newSegments.length === 0 && videoRef.current) { - // Create a new default segment that spans the entire video - const defaultSegment: Segment = { - id: Date.now(), - chapterTitle: 'Chapter 1', - startTime: 0, - endTime: videoRef.current.duration, - }; - + if (newSegments.length === 0) { + // Allow empty state - no segments + setClipSegments([]); // Reset the trim points as well setTrimStart(0); - setTrimEnd(videoRef.current.duration); + setTrimEnd(videoRef.current?.duration || 0); setSplitPoints([]); - setClipSegments([defaultSegment]); } else { // Renumber remaining segments to ensure proper chronological naming const renumberedSegments = renumberAllSegments(newSegments); @@ -767,17 +745,8 @@ const useVideoChapters = () => { setTrimEnd(duration); setSplitPoints([]); - // Create a new default segment that spans the entire video - if (!videoRef.current) return; - - const defaultSegment: Segment = { - id: Date.now(), - chapterTitle: 'Chapter 1', - startTime: 0, - endTime: duration, - }; - - setClipSegments([defaultSegment]); + // Reset to empty state - no default segment + setClipSegments([]); saveState('reset_all'); }; @@ -918,7 +887,7 @@ const useVideoChapters = () => { } // Convert chapters to backend expected format and sort by start time - const backendChapters = chapters + let backendChapters = chapters .map((chapter) => ({ startTime: chapter.from, endTime: chapter.to, @@ -931,6 +900,21 @@ const useVideoChapters = () => { return aStartSeconds - bStartSeconds; }); + // If there's only one chapter that spans the full video duration, send empty array + if (backendChapters.length === 1) { + const singleChapter = backendChapters[0]; + const startSeconds = parseTimeToSeconds(singleChapter.startTime); + const endSeconds = parseTimeToSeconds(singleChapter.endTime); + + // Check if this single chapter spans the entire video (within 0.1 second tolerance) + const isFullVideoChapter = startSeconds <= 0.1 && Math.abs(endSeconds - duration) <= 0.1; + + if (isFullVideoChapter) { + logger.debug('Manual save: Single chapter spans full video - sending empty array'); + backendChapters = []; + } + } + // Create the API request body const requestData = { chapters: backendChapters,