From 03872d0b256a4b256a7ecc16aa9c433d04018644 Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sun, 19 Oct 2025 12:40:12 +0300 Subject: [PATCH] Support empty chapters state in editor Allows users to clear all chapters, sending an empty array to the backend. Removes default segment creation when no chapters exist, updates UI and modal messaging for empty state, and ensures backend receives empty chapters when appropriate. --- .../src/components/TimelineControls.tsx | 85 +++++++------------ .../client/src/hooks/useVideoChapters.tsx | 70 ++++++--------- 2 files changed, 56 insertions(+), 99 deletions(-) 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,