mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-10 01:18:55 -05:00
feat: Fix chapters (rename text/name to chapterTitle) and fetch/post the correct object
This commit is contained in:
parent
c5edfbefb6
commit
e29d364fd3
@ -15,7 +15,6 @@ const App = () => {
|
|||||||
isPlaying,
|
isPlaying,
|
||||||
setIsPlaying,
|
setIsPlaying,
|
||||||
isMuted,
|
isMuted,
|
||||||
thumbnails,
|
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
splitPoints,
|
splitPoints,
|
||||||
@ -244,7 +243,6 @@ const App = () => {
|
|||||||
<TimelineControls
|
<TimelineControls
|
||||||
currentTime={currentTime}
|
currentTime={currentTime}
|
||||||
duration={duration}
|
duration={duration}
|
||||||
thumbnails={thumbnails}
|
|
||||||
trimStart={trimStart}
|
trimStart={trimStart}
|
||||||
trimEnd={trimEnd}
|
trimEnd={trimEnd}
|
||||||
splitPoints={splitPoints}
|
splitPoints={splitPoints}
|
||||||
|
|||||||
@ -3,11 +3,9 @@ import '../styles/ClipSegments.css';
|
|||||||
|
|
||||||
export interface Segment {
|
export interface Segment {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
chapterTitle: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
thumbnail: string;
|
|
||||||
chapterTitle?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClipSegmentsProps {
|
interface ClipSegmentsProps {
|
||||||
@ -48,10 +46,6 @@ const ClipSegments = ({ segments, selectedSegmentId }: ClipSegmentsProps) => {
|
|||||||
className={`segment-item ${getSegmentColorClass(index)} ${selectedSegmentId === segment.id ? 'selected' : ''}`}
|
className={`segment-item ${getSegmentColorClass(index)} ${selectedSegmentId === segment.id ? 'selected' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="segment-content">
|
<div className="segment-content">
|
||||||
<div
|
|
||||||
className="segment-thumbnail"
|
|
||||||
style={{ backgroundImage: `url(${segment.thumbnail})` }}
|
|
||||||
></div>
|
|
||||||
<div className="segment-info">
|
<div className="segment-info">
|
||||||
<div className="segment-title">
|
<div className="segment-title">
|
||||||
{segment.chapterTitle ? (
|
{segment.chapterTitle ? (
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useRef, useEffect, useState, useCallback } from 'react';
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { formatTime, formatDetailedTime } from '../lib/timeUtils';
|
import { formatTime, formatDetailedTime } from '../lib/timeUtils';
|
||||||
import { generateThumbnail, generateSolidColor } from '../lib/videoUtils';
|
import { generateSolidColor } from '../lib/videoUtils';
|
||||||
import { Segment } from './ClipSegments';
|
import { Segment } from './ClipSegments';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { autoSaveVideo } from '../services/videoApi';
|
import { autoSaveVideo } from '../services/videoApi';
|
||||||
@ -38,7 +38,7 @@ interface TimelineControlsProps {
|
|||||||
selectedSegmentId?: number | null;
|
selectedSegmentId?: number | null;
|
||||||
onSelectedSegmentChange?: (segmentId: number | null) => void;
|
onSelectedSegmentChange?: (segmentId: number | null) => void;
|
||||||
onSegmentUpdate?: (segmentId: number, updates: Partial<Segment>) => void;
|
onSegmentUpdate?: (segmentId: number, updates: Partial<Segment>) => void;
|
||||||
onChapterSave?: (chapters: { name: string; from: string; to: string }[]) => void;
|
onChapterSave?: (chapters: { chapterTitle: string; from: string; to: string }[]) => void;
|
||||||
onTrimStartChange: (time: number) => void;
|
onTrimStartChange: (time: number) => void;
|
||||||
onTrimEndChange: (time: number) => void;
|
onTrimEndChange: (time: number) => void;
|
||||||
onZoomChange: (level: number) => void;
|
onZoomChange: (level: number) => void;
|
||||||
@ -104,7 +104,6 @@ const constrainTooltipPosition = (positionPercent: number) => {
|
|||||||
const TimelineControls = ({
|
const TimelineControls = ({
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
thumbnails,
|
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
splitPoints,
|
splitPoints,
|
||||||
@ -168,26 +167,41 @@ const TimelineControls = ({
|
|||||||
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const clipSegmentsRef = useRef(clipSegments);
|
const clipSegmentsRef = useRef(clipSegments);
|
||||||
|
|
||||||
|
// Redirect timer refs
|
||||||
|
const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const redirectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// Keep clipSegmentsRef updated
|
// Keep clipSegmentsRef updated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clipSegmentsRef.current = clipSegments;
|
clipSegmentsRef.current = clipSegments;
|
||||||
}, [clipSegments]);
|
}, [clipSegments]);
|
||||||
|
|
||||||
|
// Function to cancel redirect timers
|
||||||
|
const cancelRedirect = useCallback(() => {
|
||||||
|
if (countdownIntervalRef.current) {
|
||||||
|
clearInterval(countdownIntervalRef.current);
|
||||||
|
countdownIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
if (redirectTimeoutRef.current) {
|
||||||
|
clearTimeout(redirectTimeoutRef.current);
|
||||||
|
redirectTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
logger.debug('Redirect cancelled by user');
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Auto-save function
|
// Auto-save function
|
||||||
const performAutoSave = useCallback(async () => {
|
const performAutoSave = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsAutoSaving(true);
|
setIsAutoSaving(true);
|
||||||
|
|
||||||
// Format segments data for API request - use ref to get latest segments
|
// Format segments data for API request - use ref to get latest segments
|
||||||
const segments = clipSegmentsRef.current.map((segment) => ({
|
const chapters = clipSegmentsRef.current.map((chapter) => ({
|
||||||
startTime: formatDetailedTime(segment.startTime),
|
startTime: formatDetailedTime(chapter.startTime),
|
||||||
endTime: formatDetailedTime(segment.endTime),
|
endTime: formatDetailedTime(chapter.endTime),
|
||||||
name: segment.name,
|
chapterTitle: chapter.chapterTitle,
|
||||||
chapterTitle: segment.chapterTitle,
|
|
||||||
text: segment.chapterTitle,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
logger.debug('segments', segments);
|
logger.debug('chapters', chapters);
|
||||||
|
|
||||||
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
|
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
|
||||||
// For testing, use '1234' if no mediaId is available
|
// For testing, use '1234' if no mediaId is available
|
||||||
@ -195,20 +209,22 @@ const TimelineControls = ({
|
|||||||
|
|
||||||
logger.debug('mediaId', finalMediaId);
|
logger.debug('mediaId', finalMediaId);
|
||||||
|
|
||||||
if (!finalMediaId || segments.length === 0) {
|
if (!finalMediaId || chapters.length === 0) {
|
||||||
logger.debug('No mediaId or segments, skipping auto-save');
|
logger.debug('No mediaId or segments, skipping auto-save');
|
||||||
setIsAutoSaving(false);
|
setIsAutoSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, segments });
|
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, chapters });
|
||||||
|
|
||||||
const response = await autoSaveVideo(finalMediaId, { segments });
|
const response = await autoSaveVideo(finalMediaId, { chapters });
|
||||||
|
|
||||||
if (response.success) {
|
console.log('response autoSaveVideo edw', response);
|
||||||
|
|
||||||
|
if (response.success === true) {
|
||||||
logger.debug('Auto-save successful');
|
logger.debug('Auto-save successful');
|
||||||
// Format the timestamp for display
|
// Format the timestamp for display
|
||||||
const date = new Date(response.timestamp);
|
const date = new Date(response.updated_at || new Date().toISOString());
|
||||||
const formattedTime = date
|
const formattedTime = date
|
||||||
.toLocaleString('en-US', {
|
.toLocaleString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -224,10 +240,10 @@ const TimelineControls = ({
|
|||||||
setLastAutoSaveTime(formattedTime);
|
setLastAutoSaveTime(formattedTime);
|
||||||
logger.debug('Auto-save successful:', formattedTime);
|
logger.debug('Auto-save successful:', formattedTime);
|
||||||
} else {
|
} else {
|
||||||
logger.error('Auto-save failed:', response.error);
|
logger.error('Auto-save failed: (TimelineControls.tsx)');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Auto-save error:', error);
|
logger.error('Auto-save error: (TimelineControls.tsx)', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsAutoSaving(false);
|
setIsAutoSaving(false);
|
||||||
}
|
}
|
||||||
@ -255,6 +271,7 @@ const TimelineControls = ({
|
|||||||
|
|
||||||
// Update editing title when selected segment changes
|
// Update editing title when selected segment changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('edw selectedSegment', selectedSegment);
|
||||||
if (selectedSegment) {
|
if (selectedSegment) {
|
||||||
setEditingChapterTitle(selectedSegment.chapterTitle || '');
|
setEditingChapterTitle(selectedSegment.chapterTitle || '');
|
||||||
} else {
|
} else {
|
||||||
@ -274,7 +291,7 @@ const TimelineControls = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle save chapters
|
// Handle save chapters
|
||||||
const handleSaveChapters = () => {
|
/* const handleSaveChapters = () => {
|
||||||
if (!onChapterSave) return;
|
if (!onChapterSave) return;
|
||||||
|
|
||||||
// Convert segments to chapter format
|
// Convert segments to chapter format
|
||||||
@ -286,10 +303,10 @@ const TimelineControls = ({
|
|||||||
|
|
||||||
onChapterSave(chapters);
|
onChapterSave(chapters);
|
||||||
setChapterHasUnsavedChanges(false);
|
setChapterHasUnsavedChanges(false);
|
||||||
};
|
}; */
|
||||||
|
|
||||||
// Helper function for time adjustment buttons to maintain playback state
|
// Helper function for time adjustment buttons to maintain playback state
|
||||||
const handleTimeAdjustment = (offsetSeconds: number) => (e: React.MouseEvent) => {
|
/* const handleTimeAdjustment = (offsetSeconds: number) => (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Calculate new time based on offset (positive or negative)
|
// Calculate new time based on offset (positive or negative)
|
||||||
@ -313,7 +330,7 @@ const TimelineControls = ({
|
|||||||
videoRef.current.play();
|
videoRef.current.play();
|
||||||
setIsPlayingSegment(true);
|
setIsPlayingSegment(true);
|
||||||
}
|
}
|
||||||
};
|
}; */
|
||||||
|
|
||||||
// Enhanced helper for continuous time adjustment when button is held down
|
// Enhanced helper for continuous time adjustment when button is held down
|
||||||
const handleContinuousTimeAdjustment = (offsetSeconds: number) => {
|
const handleContinuousTimeAdjustment = (offsetSeconds: number) => {
|
||||||
@ -484,7 +501,7 @@ const TimelineControls = ({
|
|||||||
const chapters = clipSegments
|
const chapters = clipSegments
|
||||||
.filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
|
.filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
|
||||||
.map((segment) => ({
|
.map((segment) => ({
|
||||||
name: segment.chapterTitle || `Chapter ${segment.id}`,
|
chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`,
|
||||||
from: formatDetailedTime(segment.startTime),
|
from: formatDetailedTime(segment.startTime),
|
||||||
to: formatDetailedTime(segment.endTime),
|
to: formatDetailedTime(segment.endTime),
|
||||||
}));
|
}));
|
||||||
@ -969,12 +986,14 @@ const TimelineControls = ({
|
|||||||
const loadSavedSegments = () => {
|
const loadSavedSegments = () => {
|
||||||
// Get savedSegments directly from window.MEDIA_DATA
|
// Get savedSegments directly from window.MEDIA_DATA
|
||||||
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) || null;
|
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) || null;
|
||||||
|
console.log('MEDIA_DATA edw1', (window as any).MEDIA_DATA);
|
||||||
|
console.log('savedData edw1', savedData);
|
||||||
|
|
||||||
// If no saved segments, use default segments
|
// If no saved segments, use default segments
|
||||||
if (!savedData) {
|
/* if (!savedData) {
|
||||||
logger.debug('No saved segments found in MEDIA_DATA, using default segments');
|
logger.debug('No saved segments found in MEDIA_DATA, using default segments');
|
||||||
savedData = {
|
savedData = {
|
||||||
segments: [
|
chapters: [
|
||||||
{
|
{
|
||||||
startTime: '00:00:00.000',
|
startTime: '00:00:00.000',
|
||||||
endTime: '00:00:10.000',
|
endTime: '00:00:10.000',
|
||||||
@ -993,22 +1012,19 @@ const TimelineControls = ({
|
|||||||
],
|
],
|
||||||
updated_at: '2025-06-24 14:59:14',
|
updated_at: '2025-06-24 14:59:14',
|
||||||
};
|
};
|
||||||
}
|
} */
|
||||||
|
|
||||||
logger.debug('Loading saved segments:', savedData);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (savedData && savedData.segments && savedData.segments.length > 0) {
|
if (savedData && savedData.chapters && savedData.chapters.length > 0) {
|
||||||
logger.debug('Found saved segments:', savedData);
|
logger.debug('Found saved segments:', savedData);
|
||||||
|
console.log('savedData edw', savedData);
|
||||||
|
|
||||||
// Convert the saved segments to the format expected by the component
|
// Convert the saved segments to the format expected by the component
|
||||||
const convertedSegments: Segment[] = savedData.segments.map((seg: any, index: number) => ({
|
const convertedSegments: Segment[] = savedData.chapters.map((seg: any , index: number) => ({
|
||||||
id: Date.now() + index, // Generate unique IDs
|
id: Date.now() + index, // Generate unique IDs
|
||||||
name: seg.name || `Segment ${index + 1}`,
|
chapterTitle: seg.chapterTitle || `Chapter ${index + 1}`,
|
||||||
startTime: parseTimeString(seg.startTime),
|
startTime: parseTimeString(seg.startTime),
|
||||||
endTime: parseTimeString(seg.endTime),
|
endTime: parseTimeString(seg.endTime),
|
||||||
thumbnail: '',
|
|
||||||
chapterTitle: seg.chapterTitle || '', // Preserve chapter title from saved data
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Dispatch event to update segments
|
// Dispatch event to update segments
|
||||||
@ -1188,34 +1204,6 @@ const TimelineControls = ({
|
|||||||
};
|
};
|
||||||
}, [duration, trimStart, trimEnd, onTrimStartChange, onTrimEndChange]);
|
}, [duration, trimStart, trimEnd, onTrimStartChange, onTrimEndChange]);
|
||||||
|
|
||||||
// Render solid color backgrounds evenly spread across timeline
|
|
||||||
const renderThumbnails = () => {
|
|
||||||
// Create thumbnail sections even if we don't have actual thumbnail data
|
|
||||||
const numSections = thumbnails.length || 10; // Default to 10 sections if no thumbnails
|
|
||||||
|
|
||||||
return Array.from({ length: numSections }).map((_, index) => {
|
|
||||||
const segmentDuration = duration / numSections;
|
|
||||||
const segmentStartTime = index * segmentDuration;
|
|
||||||
const segmentEndTime = segmentStartTime + segmentDuration;
|
|
||||||
const midpointTime = (segmentStartTime + segmentEndTime) / 2;
|
|
||||||
|
|
||||||
// Get a solid color based on the segment position
|
|
||||||
const backgroundColor = generateSolidColor(midpointTime, duration);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="timeline-thumbnail"
|
|
||||||
style={{
|
|
||||||
width: `${100 / numSections}%`,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
// Remove background image and use solid color instead
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render split points
|
// Render split points
|
||||||
const renderSplitPoints = () => {
|
const renderSplitPoints = () => {
|
||||||
return splitPoints.map((point, index) => {
|
return splitPoints.map((point, index) => {
|
||||||
@ -1443,7 +1431,7 @@ const TimelineControls = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only process tooltip display if clicked on the timeline background or thumbnails, not on other UI elements
|
// Only process tooltip display if clicked on the timeline background or thumbnails, not on other UI elements
|
||||||
if (e.target === timelineRef.current || (e.target as HTMLElement).classList.contains('timeline-thumbnail')) {
|
if (e.target === timelineRef.current) {
|
||||||
// Check if there's a segment at the clicked position
|
// Check if there's a segment at the clicked position
|
||||||
if (segmentAtClickedTime) {
|
if (segmentAtClickedTime) {
|
||||||
setSelectedSegmentId(segmentAtClickedTime.id);
|
setSelectedSegmentId(segmentAtClickedTime.id);
|
||||||
@ -1549,15 +1537,6 @@ const TimelineControls = ({
|
|||||||
const position = Math.max(0, Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width));
|
const position = Math.max(0, Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width));
|
||||||
const newTime = position * duration;
|
const newTime = position * duration;
|
||||||
|
|
||||||
// Create a temporary segment with the current drag position to check against
|
|
||||||
const draggedSegment = {
|
|
||||||
id: segmentId,
|
|
||||||
startTime: isLeft ? newTime : originalStartTime,
|
|
||||||
endTime: isLeft ? originalEndTime : newTime,
|
|
||||||
name: '',
|
|
||||||
thumbnail: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the current marker position intersects with where the segment will be
|
// Check if the current marker position intersects with where the segment will be
|
||||||
const currentSegmentStart = isLeft ? newTime : originalStartTime;
|
const currentSegmentStart = isLeft ? newTime : originalStartTime;
|
||||||
const currentSegmentEnd = isLeft ? originalEndTime : newTime;
|
const currentSegmentEnd = isLeft ? originalEndTime : newTime;
|
||||||
@ -2121,10 +2100,9 @@ const TimelineControls = ({
|
|||||||
// Create a full video segment
|
// Create a full video segment
|
||||||
const fullVideoSegment: Segment = {
|
const fullVideoSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'Full Video',
|
chapterTitle: 'Full Video',
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: duration,
|
endTime: duration,
|
||||||
thumbnail: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create and dispatch the update event to replace all segments with the full video segment
|
// Create and dispatch the update event to replace all segments with the full video segment
|
||||||
@ -2522,15 +2500,15 @@ const TimelineControls = ({
|
|||||||
|
|
||||||
// Add a useEffect for auto-redirection
|
// Add a useEffect for auto-redirection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let countdownInterval: NodeJS.Timeout;
|
// Clear any existing timers first
|
||||||
let redirectTimeout: NodeJS.Timeout;
|
cancelRedirect();
|
||||||
|
|
||||||
if (showSuccessModal && redirectUrl) {
|
if (showSuccessModal && redirectUrl) {
|
||||||
// Start countdown timer
|
// Start countdown timer
|
||||||
let secondsLeft = 10;
|
let secondsLeft = 10;
|
||||||
|
|
||||||
// Update the countdown every second
|
// Update the countdown every second
|
||||||
countdownInterval = setInterval(() => {
|
countdownIntervalRef.current = setInterval(() => {
|
||||||
secondsLeft--;
|
secondsLeft--;
|
||||||
const countdownElement = document.querySelector('.countdown');
|
const countdownElement = document.querySelector('.countdown');
|
||||||
if (countdownElement) {
|
if (countdownElement) {
|
||||||
@ -2538,32 +2516,28 @@ const TimelineControls = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (secondsLeft <= 0) {
|
if (secondsLeft <= 0) {
|
||||||
clearInterval(countdownInterval);
|
if (countdownIntervalRef.current) {
|
||||||
|
clearInterval(countdownIntervalRef.current);
|
||||||
|
countdownIntervalRef.current = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Set redirect timeout
|
// Set redirect timeout
|
||||||
redirectTimeout = setTimeout(() => {
|
redirectTimeoutRef.current = setTimeout(() => {
|
||||||
// Redirect to the URL
|
// Redirect to the URL
|
||||||
logger.debug('Automatically redirecting to:', redirectUrl);
|
logger.debug('Automatically redirecting to:', redirectUrl);
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
}, 10000); // 10 seconds
|
}, 10000); // 10 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on unmount or when success modal closes
|
// Cleanup on unmount
|
||||||
return () => {
|
return () => {
|
||||||
if (countdownInterval) clearInterval(countdownInterval);
|
cancelRedirect();
|
||||||
if (redirectTimeout) clearTimeout(redirectTimeout);
|
|
||||||
};
|
};
|
||||||
}, [showSuccessModal, redirectUrl]);
|
}, [showSuccessModal, redirectUrl, cancelRedirect]);
|
||||||
|
|
||||||
// Effect to handle redirect after success modal is closed
|
// Note: Removed the conflicting redirect effect - redirect is now handled by cancelRedirect function
|
||||||
useEffect(() => {
|
|
||||||
if (!showSuccessModal && redirectUrl) {
|
|
||||||
logger.debug('Redirecting to:', redirectUrl);
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
}
|
|
||||||
}, [redirectUrl, saveType, showSuccessModal]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`timeline-container-card ${isPlayingSegments ? 'segments-playback-mode' : ''}`}>
|
<div className={`timeline-container-card ${isPlayingSegments ? 'segments-playback-mode' : ''}`}>
|
||||||
@ -2665,9 +2639,6 @@ const TimelineControls = ({
|
|||||||
{/* Split Points */}
|
{/* Split Points */}
|
||||||
{renderSplitPoints()}
|
{renderSplitPoints()}
|
||||||
|
|
||||||
{/* Thumbnails */}
|
|
||||||
{renderThumbnails()}
|
|
||||||
|
|
||||||
{/* Segment Tooltip */}
|
{/* Segment Tooltip */}
|
||||||
{selectedSegmentId !== null && (
|
{selectedSegmentId !== null && (
|
||||||
<div
|
<div
|
||||||
@ -3267,10 +3238,9 @@ const TimelineControls = ({
|
|||||||
// Create the new segment with a generic name
|
// Create the new segment with a generic name
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: `segment`,
|
chapterTitle: `segment`,
|
||||||
startTime: segmentStartTime,
|
startTime: segmentStartTime,
|
||||||
endTime: segmentEndTime,
|
endTime: segmentEndTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segment to existing segments
|
// Add the new segment to existing segments
|
||||||
@ -3376,10 +3346,9 @@ const TimelineControls = ({
|
|||||||
// Create a virtual "segment" for the cutaway area
|
// Create a virtual "segment" for the cutaway area
|
||||||
const cutawaySegment: Segment = {
|
const cutawaySegment: Segment = {
|
||||||
id: -999, // Use a unique negative ID to indicate a virtual segment
|
id: -999, // Use a unique negative ID to indicate a virtual segment
|
||||||
name: 'Cutaway',
|
chapterTitle: 'Cutaway',
|
||||||
startTime: startTime,
|
startTime: startTime,
|
||||||
endTime: endTime,
|
endTime: endTime,
|
||||||
thumbnail: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Seek to the start of the cutaway (true beginning of this cutaway area)
|
// Seek to the start of the cutaway (true beginning of this cutaway area)
|
||||||
@ -3616,249 +3585,6 @@ const TimelineControls = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Play/Pause button for empty space */}
|
|
||||||
{/* <button
|
|
||||||
className={`tooltip-action-btn ${isPlaying ? 'pause' : 'play'}`}
|
|
||||||
data-tooltip={isPlaying ? "Pause playback" : "Play from here until next segment"}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (videoRef.current) {
|
|
||||||
if (isPlaying) {
|
|
||||||
// If already playing, pause the video
|
|
||||||
videoRef.current.pause();
|
|
||||||
setIsPlayingSegment(false);
|
|
||||||
// Reset continuePastBoundary when stopping playback
|
|
||||||
setContinuePastBoundary(false);
|
|
||||||
logger.debug("Pause clicked in empty space - resetting continuePastBoundary flag");
|
|
||||||
} else {
|
|
||||||
// Enable continuePastBoundary flag when user explicitly clicks play
|
|
||||||
// This will allow playback to continue even if we're at segment boundary
|
|
||||||
setContinuePastBoundary(true);
|
|
||||||
logger.debug("Setting continuePastBoundary=true to allow playback through boundaries");
|
|
||||||
|
|
||||||
// Find the current time and determine cutaway boundaries
|
|
||||||
// For end, find the next segment after current position
|
|
||||||
// Make sure we look for any segment that starts after our current position,
|
|
||||||
// including the first segment if we're before it
|
|
||||||
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
||||||
const currentTime = videoRef.current.currentTime;
|
|
||||||
const nextSegment = sortedSegments.find(seg => seg.startTime > currentTime);
|
|
||||||
|
|
||||||
// Check if we're at a segment boundary that we previously stopped at
|
|
||||||
const isAtSegmentBoundary = nextSegment && Math.abs(currentTime - nextSegment.startTime) < 0.05;
|
|
||||||
|
|
||||||
if (isAtSegmentBoundary && nextSegment) {
|
|
||||||
// We're at the start of a segment - just continue into the segment rather than staying in cutaway
|
|
||||||
logger.debug(`At segment boundary: Moving into segment ${nextSegment.id}`);
|
|
||||||
|
|
||||||
// Update UI to show segment tooltip instead of empty space tooltip
|
|
||||||
setSelectedSegmentId(nextSegment.id);
|
|
||||||
setShowEmptySpaceTooltip(false);
|
|
||||||
|
|
||||||
// Set this segment as the active segment for boundary checking
|
|
||||||
setActiveSegment(nextSegment);
|
|
||||||
|
|
||||||
// Play from this segment directly
|
|
||||||
videoRef.current.play()
|
|
||||||
.then(() => {
|
|
||||||
setIsPlayingSegment(true);
|
|
||||||
logger.debug("Playing from segment start after boundary");
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error starting playback:", err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return; // Exit early as we've handled this special case
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define end boundary (either next segment start or video end)
|
|
||||||
const endTime = nextSegment ? nextSegment.startTime : duration;
|
|
||||||
|
|
||||||
// Special handling for when we're already at a segment boundary
|
|
||||||
// If we're at or extremely close to the segment boundary already,
|
|
||||||
// we need to nudge the position slightly back to allow playback
|
|
||||||
let adjustedCurrentTime = currentTime;
|
|
||||||
|
|
||||||
if (nextSegment && Math.abs(currentTime - nextSegment.startTime) < 0.05) {
|
|
||||||
logger.debug(`Already at boundary (${formatDetailedTime(currentTime)}), nudging position back slightly`);
|
|
||||||
adjustedCurrentTime = Math.max(0, currentTime - 0.1); // Move 100ms back
|
|
||||||
videoRef.current.currentTime = adjustedCurrentTime;
|
|
||||||
onSeek(adjustedCurrentTime);
|
|
||||||
logger.debug(`Position adjusted to ${formatDetailedTime(adjustedCurrentTime)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a virtual "segment" for the cutaway area
|
|
||||||
const cutawaySegment: Segment = {
|
|
||||||
id: -999, // Use a consistent negative ID for virtual segments
|
|
||||||
name: "Cutaway",
|
|
||||||
startTime: adjustedCurrentTime, // Use the potentially adjusted time
|
|
||||||
endTime: endTime,
|
|
||||||
thumbnail: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
// IMPORTANT: First reset isPlayingSegment to false to ensure clean state
|
|
||||||
setIsPlayingSegment(false);
|
|
||||||
|
|
||||||
// Then set active segment for boundary checking
|
|
||||||
// We use setTimeout to ensure this happens in the next tick
|
|
||||||
// after the isPlayingSegment value is updated
|
|
||||||
setTimeout(() => {
|
|
||||||
setActiveSegment(cutawaySegment);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Add a manual boundary check specifically for cutaway playback
|
|
||||||
// This ensures we detect when we reach the next segment's start
|
|
||||||
const checkCutawayBoundary = () => {
|
|
||||||
if (!videoRef.current) return;
|
|
||||||
|
|
||||||
// Check if we've entered a segment (i.e., reached a boundary)
|
|
||||||
const currentPosition = videoRef.current.currentTime;
|
|
||||||
const segments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
||||||
|
|
||||||
// Find the next segment we're approaching - use a wider detection range
|
|
||||||
// to catch the boundary earlier
|
|
||||||
const nextSegment = segments.find(seg => seg.startTime > currentPosition - 0.3);
|
|
||||||
|
|
||||||
// Also check if we've entered a different segment - we need to detect this too
|
|
||||||
const segmentAtCurrentTime = segments.find(
|
|
||||||
seg => currentPosition >= seg.startTime && currentPosition <= seg.endTime
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we've moved directly into a segment during playback, we need to update the active segment
|
|
||||||
if (segmentAtCurrentTime && activeSegment?.id !== segmentAtCurrentTime.id) {
|
|
||||||
logger.debug(`Entered segment ${segmentAtCurrentTime.id} during cutaway playback`);
|
|
||||||
setActiveSegment(segmentAtCurrentTime);
|
|
||||||
setSelectedSegmentId(segmentAtCurrentTime.id);
|
|
||||||
setShowEmptySpaceTooltip(false);
|
|
||||||
|
|
||||||
// Remove our boundary checker since we're now in a standard segment
|
|
||||||
videoRef.current.removeEventListener('timeupdate', checkCutawayBoundary);
|
|
||||||
|
|
||||||
// Reset continuation flags
|
|
||||||
setContinuePastBoundary(false);
|
|
||||||
sessionStorage.removeItem('continuingPastSegment');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to detect boundaries much earlier to allow for time to react
|
|
||||||
// This is a key fix - we need to detect the boundary BEFORE we reach it
|
|
||||||
// But don't stop if we're in continuePastBoundary mode
|
|
||||||
const shouldStop = nextSegment &&
|
|
||||||
(currentPosition >= nextSegment.startTime - 0.25) &&
|
|
||||||
(currentPosition <= nextSegment.startTime + 0.1) &&
|
|
||||||
!continuePastBoundary;
|
|
||||||
|
|
||||||
// Add logging to show boundary check decisions
|
|
||||||
if (nextSegment && (currentPosition >= nextSegment.startTime - 0.25) &&
|
|
||||||
(currentPosition <= nextSegment.startTime + 0.1)) {
|
|
||||||
logger.debug(`Approaching boundary at ${formatDetailedTime(nextSegment.startTime)}, continuePastBoundary=${continuePastBoundary}, willStop=${shouldStop}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've entered a segment, stop at its boundary
|
|
||||||
if (shouldStop && nextSegment) {
|
|
||||||
logger.debug(`CUTAWAY MANUAL BOUNDARY CHECK: Current position ${formatDetailedTime(currentPosition)} approaching segment at ${formatDetailedTime(nextSegment.startTime)} (distance: ${Math.abs(currentPosition - nextSegment.startTime).toFixed(3)}s) - STOPPING`);
|
|
||||||
|
|
||||||
videoRef.current.pause();
|
|
||||||
// Force exact time position with high precision
|
|
||||||
setTimeout(() => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
// First seek directly to exact start time, no offset
|
|
||||||
videoRef.current.currentTime = nextSegment.startTime;
|
|
||||||
// Update UI immediately to match video position
|
|
||||||
onSeek(nextSegment.startTime);
|
|
||||||
// Also update tooltip time displays
|
|
||||||
setDisplayTime(nextSegment.startTime);
|
|
||||||
setClickedTime(nextSegment.startTime);
|
|
||||||
|
|
||||||
// Reset continuePastBoundary when stopping at a boundary
|
|
||||||
setContinuePastBoundary(false);
|
|
||||||
|
|
||||||
// Update tooltip to show the segment at the boundary
|
|
||||||
setSelectedSegmentId(nextSegment.id);
|
|
||||||
setShowEmptySpaceTooltip(false);
|
|
||||||
setActiveSegment(nextSegment);
|
|
||||||
|
|
||||||
// Force multiple adjustments to ensure exact precision
|
|
||||||
const verifyPosition = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
// Always force the exact time in every verification
|
|
||||||
videoRef.current.currentTime = nextSegment.startTime;
|
|
||||||
|
|
||||||
// Make sure we update the UI to reflect the corrected position
|
|
||||||
onSeek(nextSegment.startTime);
|
|
||||||
|
|
||||||
// Update the displayTime and clickedTime state to match exact position
|
|
||||||
setDisplayTime(nextSegment.startTime);
|
|
||||||
setClickedTime(nextSegment.startTime);
|
|
||||||
|
|
||||||
logger.debug(`Position corrected to exact segment boundary: ${formatDetailedTime(videoRef.current.currentTime)} (target: ${formatDetailedTime(nextSegment.startTime)})`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply multiple correction attempts with increasing delays
|
|
||||||
setTimeout(verifyPosition, 10); // Immediate correction
|
|
||||||
setTimeout(verifyPosition, 20); // First correction
|
|
||||||
setTimeout(verifyPosition, 50); // Second correction
|
|
||||||
setTimeout(verifyPosition, 100); // Third correction
|
|
||||||
setTimeout(verifyPosition, 200); // Final correction
|
|
||||||
|
|
||||||
// Also add event listeners to ensure position is corrected whenever video state changes
|
|
||||||
videoRef.current.addEventListener('seeked', verifyPosition);
|
|
||||||
videoRef.current.addEventListener('canplay', verifyPosition);
|
|
||||||
videoRef.current.addEventListener('waiting', verifyPosition);
|
|
||||||
|
|
||||||
// Remove these event listeners after a short time
|
|
||||||
setTimeout(() => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.removeEventListener('seeked', verifyPosition);
|
|
||||||
videoRef.current.removeEventListener('canplay', verifyPosition);
|
|
||||||
videoRef.current.removeEventListener('waiting', verifyPosition);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
setIsPlayingSegment(false);
|
|
||||||
setActiveSegment(null);
|
|
||||||
|
|
||||||
// Remove our boundary checker
|
|
||||||
videoRef.current.removeEventListener('timeupdate', checkCutawayBoundary);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start our manual boundary checker
|
|
||||||
videoRef.current.addEventListener('timeupdate', checkCutawayBoundary);
|
|
||||||
|
|
||||||
// Start playing from current position with boundary restrictions
|
|
||||||
// Use a timeout to ensure active segment is set before playback starts
|
|
||||||
setTimeout(() => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
videoRef.current.play()
|
|
||||||
.then(() => {
|
|
||||||
setIsPlayingSegment(true);
|
|
||||||
logger.debug("Play clicked in empty space - position:",
|
|
||||||
formatDetailedTime(currentTime),
|
|
||||||
"will stop at:", formatDetailedTime(endTime),
|
|
||||||
nextSegment ? `(start of segment ${nextSegment.id})` : "(end of video)"
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error starting playback:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPlaying ? (
|
|
||||||
<img src={pauseIcon} alt="Pause" style={{width: '24px', height: '24px'}} />
|
|
||||||
) : (
|
|
||||||
<img src={playIcon} alt="Play" style={{width: '24px', height: '24px'}} />
|
|
||||||
)}
|
|
||||||
</button> */}
|
|
||||||
|
|
||||||
{/* Play/Pause button for empty space - Same as main play/pause button */}
|
{/* Play/Pause button for empty space - Same as main play/pause button */}
|
||||||
<button
|
<button
|
||||||
className={`tooltip-action-btn ${isPlaying ? 'pause' : 'play'} ${
|
className={`tooltip-action-btn ${isPlaying ? 'pause' : 'play'} ${
|
||||||
@ -3997,10 +3723,9 @@ const TimelineControls = ({
|
|||||||
// We're in a gap, create a new segment from gap start to clicked time
|
// We're in a gap, create a new segment from gap start to clicked time
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: gapStart,
|
startTime: gapStart,
|
||||||
endTime: clickedTime,
|
endTime: clickedTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segment to existing segments
|
// Add the new segment to existing segments
|
||||||
@ -4031,10 +3756,9 @@ const TimelineControls = ({
|
|||||||
// Create a new segment from start of video to clicked time
|
// Create a new segment from start of video to clicked time
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: clickedTime,
|
endTime: clickedTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segment to existing segments
|
// Add the new segment to existing segments
|
||||||
@ -4095,10 +3819,9 @@ const TimelineControls = ({
|
|||||||
// No segments exist; create a new segment from start to clicked time
|
// No segments exist; create a new segment from start to clicked time
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: clickedTime,
|
endTime: clickedTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create and dispatch the update event
|
// Create and dispatch the update event
|
||||||
@ -4214,10 +3937,9 @@ const TimelineControls = ({
|
|||||||
// We're in a gap, create a new segment from clicked time to gap end
|
// We're in a gap, create a new segment from clicked time to gap end
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: clickedTime,
|
startTime: clickedTime,
|
||||||
endTime: gapEnd,
|
endTime: gapEnd,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segment to existing segments
|
// Add the new segment to existing segments
|
||||||
@ -4248,10 +3970,9 @@ const TimelineControls = ({
|
|||||||
// Create a new segment from clicked time to first segment start
|
// Create a new segment from clicked time to first segment start
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: clickedTime,
|
startTime: clickedTime,
|
||||||
endTime: sortedByStart[0].startTime,
|
endTime: sortedByStart[0].startTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segment to existing segments
|
// Add the new segment to existing segments
|
||||||
@ -4283,10 +4004,9 @@ const TimelineControls = ({
|
|||||||
// Create a new segment from clicked time to end of video
|
// Create a new segment from clicked time to end of video
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: clickedTime,
|
startTime: clickedTime,
|
||||||
endTime: duration,
|
endTime: duration,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segment to existing segments
|
// Add the new segment to existing segments
|
||||||
@ -4347,10 +4067,9 @@ const TimelineControls = ({
|
|||||||
// No segments exist; create a new segment from clicked time to end
|
// No segments exist; create a new segment from clicked time to end
|
||||||
const newSegment: Segment = {
|
const newSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: clickedTime,
|
startTime: clickedTime,
|
||||||
endTime: duration,
|
endTime: duration,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create and dispatch the update event
|
// Create and dispatch the update event
|
||||||
@ -4760,7 +4479,10 @@ const TimelineControls = ({
|
|||||||
{/* Success Modal */}
|
{/* Success Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showSuccessModal}
|
isOpen={showSuccessModal}
|
||||||
onClose={() => setShowSuccessModal(false)}
|
onClose={() => {
|
||||||
|
cancelRedirect();
|
||||||
|
setShowSuccessModal(false);
|
||||||
|
}}
|
||||||
title="Video Edited Successfully"
|
title="Video Edited Successfully"
|
||||||
>
|
>
|
||||||
<div className="modal-success-content">
|
<div className="modal-success-content">
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { generateThumbnail } from '@/lib/videoUtils';
|
|
||||||
import { formatDetailedTime } from '@/lib/timeUtils';
|
import { formatDetailedTime } from '@/lib/timeUtils';
|
||||||
import logger from '@/lib/logger';
|
import logger from '@/lib/logger';
|
||||||
import type { Segment } from '@/components/ClipSegments';
|
import type { Segment } from '@/components/ClipSegments';
|
||||||
@ -34,7 +33,6 @@ const useVideoChapters = () => {
|
|||||||
const [isMuted, setIsMuted] = useState(false);
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
|
||||||
// Timeline state
|
// Timeline state
|
||||||
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
|
||||||
const [trimStart, setTrimStart] = useState(0);
|
const [trimStart, setTrimStart] = useState(0);
|
||||||
const [trimEnd, setTrimEnd] = useState(0);
|
const [trimEnd, setTrimEnd] = useState(0);
|
||||||
const [splitPoints, setSplitPoints] = useState<number[]>([]);
|
const [splitPoints, setSplitPoints] = useState<number[]>([]);
|
||||||
@ -105,30 +103,22 @@ const useVideoChapters = () => {
|
|||||||
const startTime = parseTimeToSeconds(chapter.startTime);
|
const startTime = parseTimeToSeconds(chapter.startTime);
|
||||||
const endTime = parseTimeToSeconds(chapter.endTime);
|
const endTime = parseTimeToSeconds(chapter.endTime);
|
||||||
|
|
||||||
// Generate thumbnail for this segment
|
|
||||||
const segmentThumbnail = await generateThumbnail(video, (startTime + endTime) / 2);
|
|
||||||
|
|
||||||
const segment: Segment = {
|
const segment: Segment = {
|
||||||
id: i + 1,
|
id: i + 1,
|
||||||
name: `segment-${i + 1}`,
|
chapterTitle: chapter.chapterTitle,
|
||||||
startTime: startTime,
|
startTime: startTime,
|
||||||
endTime: endTime,
|
endTime: endTime,
|
||||||
thumbnail: segmentThumbnail,
|
|
||||||
chapterTitle: chapter.text,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
initialSegments.push(segment);
|
initialSegments.push(segment);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create a default segment that spans the entire video (fallback)
|
|
||||||
const segmentThumbnail = await generateThumbnail(video, video.duration / 2);
|
|
||||||
|
|
||||||
const initialSegment: Segment = {
|
const initialSegment: Segment = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: video.duration,
|
endTime: video.duration,
|
||||||
thumbnail: segmentThumbnail,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
initialSegments = [initialSegment];
|
initialSegments = [initialSegment];
|
||||||
@ -145,19 +135,6 @@ const useVideoChapters = () => {
|
|||||||
setHistory([initialState]);
|
setHistory([initialState]);
|
||||||
setHistoryPosition(0);
|
setHistoryPosition(0);
|
||||||
setClipSegments(initialSegments);
|
setClipSegments(initialSegments);
|
||||||
|
|
||||||
// Generate timeline thumbnails
|
|
||||||
const count = 6;
|
|
||||||
const interval = video.duration / count;
|
|
||||||
const placeholders: string[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const time = interval * i + interval / 2;
|
|
||||||
const thumbnail = await generateThumbnail(video, time);
|
|
||||||
placeholders.push(thumbnail);
|
|
||||||
}
|
|
||||||
|
|
||||||
setThumbnails(placeholders);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeEditor();
|
initializeEditor();
|
||||||
@ -470,22 +447,18 @@ const useVideoChapters = () => {
|
|||||||
|
|
||||||
newSegments.splice(segmentIndex, 1);
|
newSegments.splice(segmentIndex, 1);
|
||||||
|
|
||||||
// Create first half of the split segment - no thumbnail needed
|
|
||||||
const firstHalf: Segment = {
|
const firstHalf: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: `${segmentToSplit.name}-A`,
|
chapterTitle: `${segmentToSplit.chapterTitle}-A`,
|
||||||
startTime: segmentToSplit.startTime,
|
startTime: segmentToSplit.startTime,
|
||||||
endTime: timeToSplit,
|
endTime: timeToSplit,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create second half of the split segment - no thumbnail needed
|
|
||||||
const secondHalf: Segment = {
|
const secondHalf: Segment = {
|
||||||
id: Date.now() + 1,
|
id: Date.now() + 1,
|
||||||
name: `${segmentToSplit.name}-B`,
|
chapterTitle: `${segmentToSplit.chapterTitle}-B`,
|
||||||
startTime: timeToSplit,
|
startTime: timeToSplit,
|
||||||
endTime: segmentToSplit.endTime,
|
endTime: segmentToSplit.endTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the new segments
|
// Add the new segments
|
||||||
@ -513,13 +486,11 @@ const useVideoChapters = () => {
|
|||||||
// If all segments are deleted, create a new full video segment
|
// If all segments are deleted, create a new full video segment
|
||||||
if (newSegments.length === 0 && videoRef.current) {
|
if (newSegments.length === 0 && videoRef.current) {
|
||||||
// Create a new default segment that spans the entire video
|
// Create a new default segment that spans the entire video
|
||||||
// No need to generate a thumbnail - we'll use dynamic colors
|
|
||||||
const defaultSegment: Segment = {
|
const defaultSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: videoRef.current.duration,
|
endTime: videoRef.current.duration,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset the trim points as well
|
// Reset the trim points as well
|
||||||
@ -576,13 +547,11 @@ const useVideoChapters = () => {
|
|||||||
const endTime = i < newSplitPoints.length ? newSplitPoints[i] : duration;
|
const endTime = i < newSplitPoints.length ? newSplitPoints[i] : duration;
|
||||||
|
|
||||||
if (startTime < endTime) {
|
if (startTime < endTime) {
|
||||||
// No need to generate thumbnails - we'll use dynamic colors
|
|
||||||
newSegments.push({
|
newSegments.push({
|
||||||
id: Date.now() + i,
|
id: Date.now() + i,
|
||||||
name: `Segment ${i + 1}`,
|
chapterTitle: `Segment ${i + 1}`,
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
});
|
});
|
||||||
|
|
||||||
startTime = endTime;
|
startTime = endTime;
|
||||||
@ -603,13 +572,11 @@ const useVideoChapters = () => {
|
|||||||
// Create a new default segment that spans the entire video
|
// Create a new default segment that spans the entire video
|
||||||
if (!videoRef.current) return;
|
if (!videoRef.current) return;
|
||||||
|
|
||||||
// No need to generate thumbnails - we'll use dynamic colors
|
|
||||||
const defaultSegment: Segment = {
|
const defaultSegment: Segment = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
name: 'segment',
|
chapterTitle: 'segment',
|
||||||
startTime: 0,
|
startTime: 0,
|
||||||
endTime: duration,
|
endTime: duration,
|
||||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setClipSegments([defaultSegment]);
|
setClipSegments([defaultSegment]);
|
||||||
@ -731,7 +698,7 @@ const useVideoChapters = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle saving chapters to database
|
// Handle saving chapters to database
|
||||||
const handleChapterSave = async (chapters: { name: string; from: string; to: string }[]) => {
|
const handleChapterSave = async (chapters: { chapterTitle: string; from: string; to: string }[]) => {
|
||||||
try {
|
try {
|
||||||
// Get media ID from window.MEDIA_DATA
|
// Get media ID from window.MEDIA_DATA
|
||||||
const mediaId = (window as any).MEDIA_DATA?.mediaId;
|
const mediaId = (window as any).MEDIA_DATA?.mediaId;
|
||||||
@ -744,7 +711,7 @@ const useVideoChapters = () => {
|
|||||||
const backendChapters = chapters.map((chapter) => ({
|
const backendChapters = chapters.map((chapter) => ({
|
||||||
startTime: chapter.from,
|
startTime: chapter.from,
|
||||||
endTime: chapter.to,
|
endTime: chapter.to,
|
||||||
text: chapter.name,
|
chapterTitle: chapter.chapterTitle,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Create the API request body
|
// Create the API request body
|
||||||
@ -931,7 +898,6 @@ const useVideoChapters = () => {
|
|||||||
setIsPlaying,
|
setIsPlaying,
|
||||||
isMuted,
|
isMuted,
|
||||||
isPlayingSegments,
|
isPlayingSegments,
|
||||||
thumbnails,
|
|
||||||
trimStart,
|
trimStart,
|
||||||
trimEnd,
|
trimEnd,
|
||||||
splitPoints,
|
splitPoints,
|
||||||
|
|||||||
@ -176,15 +176,6 @@
|
|||||||
right: -4px;
|
right: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-thumbnail {
|
|
||||||
height: 100%;
|
|
||||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.split-point {
|
.split-point {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
|
|||||||
@ -15,30 +15,3 @@ export const generateSolidColor = (time: number, duration: number): string => {
|
|||||||
|
|
||||||
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy function kept for compatibility
|
|
||||||
* Now returns a data URL for a solid color square instead of a video thumbnail
|
|
||||||
*/
|
|
||||||
export const generateThumbnail = async (videoElement: HTMLVideoElement, time: number): Promise<string> => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Create a small canvas for the solid color
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = 10; // Much smaller - we only need a color
|
|
||||||
canvas.height = 10;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
// Get the solid color based on time
|
|
||||||
const color = generateSolidColor(time, videoElement.duration);
|
|
||||||
|
|
||||||
// Fill with solid color
|
|
||||||
ctx.fillStyle = color;
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to data URL (much smaller now)
|
|
||||||
const dataUrl = canvas.toDataURL('image/png', 0.5);
|
|
||||||
resolve(dataUrl);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@ -6,25 +6,24 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|||||||
|
|
||||||
// Auto-save interface
|
// Auto-save interface
|
||||||
interface AutoSaveRequest {
|
interface AutoSaveRequest {
|
||||||
segments: {
|
chapters: {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
name?: string;
|
chapterTitle?: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoSaveResponse {
|
interface AutoSaveResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
timestamp: string;
|
|
||||||
error?: string;
|
|
||||||
status?: string;
|
status?: string;
|
||||||
media_id?: string;
|
timestamp: string;
|
||||||
segments?: {
|
chapters?: {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
name: string;
|
chapterTitle: string;
|
||||||
}[];
|
}[];
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-save API function
|
// Auto-save API function
|
||||||
@ -36,6 +35,9 @@ export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Pro
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('data edw', data);
|
||||||
|
console.log('response edw', response);
|
||||||
|
|
||||||
logger.debug('response', response);
|
logger.debug('response', response);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -54,13 +56,13 @@ export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Pro
|
|||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
error: errorData.error || 'Auto-save failed',
|
error: errorData.error || 'Auto-save failed (videoApi.ts)',
|
||||||
};
|
};
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
error: 'Auto-save failed',
|
error: 'Auto-save failed (videoApi.ts)',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,21 +70,15 @@ export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Pro
|
|||||||
|
|
||||||
// Successful response
|
// Successful response
|
||||||
const jsonResponse = await response.json();
|
const jsonResponse = await response.json();
|
||||||
|
console.log('jsonResponse edw', jsonResponse);
|
||||||
|
|
||||||
// Check if the response has the expected format
|
// Check if the response has the expected format
|
||||||
if (jsonResponse.status === 'success') {
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
timestamp: jsonResponse.updated_at || new Date().toISOString(),
|
timestamp: jsonResponse.updated_at || new Date().toISOString(),
|
||||||
...jsonResponse,
|
...jsonResponse,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
error: jsonResponse.error || 'Auto-save failed',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// For any fetch errors, return mock success response
|
// For any fetch errors, return mock success response
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
|||||||
@ -216,16 +216,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.segment-thumbnail {
|
|
||||||
width: 4rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-info {
|
.segment-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user