mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-05 23:18:53 -05:00
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:
parent
c071524cb9
commit
03872d0b25
@ -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);
|
||||||
|
|
||||||
|
if (chapters.length === 0) {
|
||||||
|
setSuccessMessage('All chapters cleared successfully!');
|
||||||
|
} else {
|
||||||
setSuccessMessage('Chapters saved successfully!');
|
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);
|
|
||||||
|
|
||||||
// Calculate tooltip position at current time
|
logger.debug('All segments deleted - entering empty state');
|
||||||
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>
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user