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),
}));
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);
setActiveSegment(null);
// 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),
});
}
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 = ({
<button
onClick={() => setShowSaveChaptersModal(true)}
className="save-chapters-button"
data-tooltip="Save chapters"
disabled={clipSegments.filter((s) => s.chapterTitle && s.chapterTitle.trim()).length === 0}
data-tooltip={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>
</div>
@ -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'}
</button>
</>
}
>
<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
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.`;
}
})()}
</p>
</Modal>

View File

@ -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');
const defaultSegment: Segment = {
id: 1,
chapterTitle: '',
startTime: 0,
endTime: video.duration,
};
logger.debug('Safari: Successfully initialized metadata with empty state');
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,