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.
This commit is contained in:
Yiannis Christodoulou 2025-10-19 12:40:12 +03:00
parent c071524cb9
commit 03872d0b25
2 changed files with 56 additions and 99 deletions

View File

@ -508,18 +508,17 @@ const TimelineControls = ({
to: formatDetailedTime(segment.endTime), to: formatDetailedTime(segment.endTime),
})); }));
if (chapters.length === 0) { // Allow saving even when no chapters exist (will send empty array)
setErrorMessage('No chapters with titles found');
setShowErrorModal(true);
setShowProcessingModal(false);
return;
}
// Call the onChapterSave function if provided // Call the onChapterSave function if provided
if (onChapterSave) { if (onChapterSave) {
await onChapterSave(chapters); await onChapterSave(chapters);
setShowProcessingModal(false); 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 // Set redirect URL to media page
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null; 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 // 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) { if (remainingSegments.length === 0) {
// Create a full video segment // Allow empty state - clear all UI state
const fullVideoSegment: Segment = { setSelectedSegmentId(null);
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);
setShowEmptySpaceTooltip(false); setShowEmptySpaceTooltip(false);
setClickedTime(currentTime); setActiveSegment(null);
setDisplayTime(currentTime);
setActiveSegment(fullVideoSegment); logger.debug('All segments deleted - entering empty state');
// 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),
});
}
} else if (selectedSegmentId === segmentId) { } else if (selectedSegmentId === segmentId) {
// Handle normal segment deletion // Handle normal segment deletion
const deletedSegment = clipSegments.find((seg) => seg.id === segmentId); const deletedSegment = clipSegments.find((seg) => seg.id === segmentId);
@ -3984,10 +3947,13 @@ const TimelineControls = ({
<button <button
onClick={() => setShowSaveChaptersModal(true)} onClick={() => setShowSaveChaptersModal(true)}
className="save-chapters-button" className="save-chapters-button"
data-tooltip="Save chapters" data-tooltip={clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
disabled={clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0} ? "Clear all chapters"
: "Save chapters"}
> >
Save Chapters {clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? 'Clear Chapters'
: 'Save Chapters'}
</button> </button>
</div> </div>
@ -4008,15 +3974,22 @@ const TimelineControls = ({
className="modal-button modal-button-primary" className="modal-button modal-button-primary"
onClick={handleSaveChaptersConfirm} onClick={handleSaveChaptersConfirm}
> >
Save Chapters {clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0
? 'Clear Chapters'
: 'Save Chapters'}
</button> </button>
</> </>
} }
> >
<p className="modal-message"> <p className="modal-message">
Are you sure you want to save the chapters? This will save{' '} {(() => {
{clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length} chapters to the const chaptersWithTitles = clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length;
database. 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.`;
}
})()}
</p> </p>
</Modal> </Modal>

View File

@ -147,15 +147,8 @@ const useVideoChapters = () => {
initialSegments.push(segment); initialSegments.push(segment);
} }
} else { } else {
// Start with empty state - no default segment
const initialSegment: Segment = { initialSegments = [];
id: 1,
chapterTitle: '',
startTime: 0,
endTime: video.duration,
};
initialSegments = [initialSegment];
} }
// Initialize history state with the segments // Initialize history state with the segments
@ -274,24 +267,17 @@ const useVideoChapters = () => {
// Check if we now have duration and initialize if needed // Check if we now have duration and initialize if needed
if (video.duration > 0 && clipSegments.length === 0) { 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); setDuration(video.duration);
setTrimEnd(video.duration); setTrimEnd(video.duration);
setClipSegments([defaultSegment]); setClipSegments([]);
const initialState: EditorState = { const initialState: EditorState = {
trimStart: 0, trimStart: 0,
trimEnd: video.duration, trimEnd: video.duration,
splitPoints: [], splitPoints: [],
clipSegments: [defaultSegment], clipSegments: [],
}; };
setHistory([initialState]); setHistory([initialState]);
@ -680,21 +666,13 @@ const useVideoChapters = () => {
const newSegments = clipSegments.filter((segment) => segment.id !== segmentId); const newSegments = clipSegments.filter((segment) => segment.id !== segmentId);
if (newSegments.length !== clipSegments.length) { if (newSegments.length !== clipSegments.length) {
// If all segments are deleted, create a new full video segment if (newSegments.length === 0) {
if (newSegments.length === 0 && videoRef.current) { // Allow empty state - no segments
// Create a new default segment that spans the entire video setClipSegments([]);
const defaultSegment: Segment = {
id: Date.now(),
chapterTitle: 'Chapter 1',
startTime: 0,
endTime: videoRef.current.duration,
};
// Reset the trim points as well // Reset the trim points as well
setTrimStart(0); setTrimStart(0);
setTrimEnd(videoRef.current.duration); setTrimEnd(videoRef.current?.duration || 0);
setSplitPoints([]); setSplitPoints([]);
setClipSegments([defaultSegment]);
} else { } else {
// Renumber remaining segments to ensure proper chronological naming // Renumber remaining segments to ensure proper chronological naming
const renumberedSegments = renumberAllSegments(newSegments); const renumberedSegments = renumberAllSegments(newSegments);
@ -767,17 +745,8 @@ const useVideoChapters = () => {
setTrimEnd(duration); setTrimEnd(duration);
setSplitPoints([]); setSplitPoints([]);
// Create a new default segment that spans the entire video // Reset to empty state - no default segment
if (!videoRef.current) return; setClipSegments([]);
const defaultSegment: Segment = {
id: Date.now(),
chapterTitle: 'Chapter 1',
startTime: 0,
endTime: duration,
};
setClipSegments([defaultSegment]);
saveState('reset_all'); saveState('reset_all');
}; };
@ -918,7 +887,7 @@ const useVideoChapters = () => {
} }
// Convert chapters to backend expected format and sort by start time // Convert chapters to backend expected format and sort by start time
const backendChapters = chapters let backendChapters = chapters
.map((chapter) => ({ .map((chapter) => ({
startTime: chapter.from, startTime: chapter.from,
endTime: chapter.to, endTime: chapter.to,
@ -931,6 +900,21 @@ const useVideoChapters = () => {
return aStartSeconds - bStartSeconds; 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 // Create the API request body
const requestData = { const requestData = {
chapters: backendChapters, chapters: backendChapters,