mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 23:48:54 -05:00
feat: Auto save functionality in both editors (video-editor, chapters-editor)
This commit is contained in:
parent
32af52ccda
commit
f472f94095
@ -1,5 +1,6 @@
|
|||||||
import '../styles/EditingTools.css';
|
import '../styles/EditingTools.css';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import logger from '@/lib/logger';
|
||||||
|
|
||||||
interface EditingToolsProps {
|
interface EditingToolsProps {
|
||||||
onSplit: () => void;
|
onSplit: () => void;
|
||||||
@ -42,7 +43,7 @@ const EditingTools = ({
|
|||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
// Ensure lastSeekedPosition is used when play is clicked
|
// Ensure lastSeekedPosition is used when play is clicked
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
console.log('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
|
logger.debug('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the original handler
|
// Call the original handler
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useRef, useEffect, useState } 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 { generateThumbnail, generateSolidColor } from '../lib/videoUtils';
|
||||||
import { Segment } from './ClipSegments';
|
import { Segment } from './ClipSegments';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { trimVideo } from '../services/videoApi';
|
import { autoSaveVideo } from '../services/videoApi';
|
||||||
import logger from '../lib/logger';
|
import logger from '../lib/logger';
|
||||||
import '../styles/TimelineControls.css';
|
import '../styles/TimelineControls.css';
|
||||||
import '../styles/TwoRowTooltip.css';
|
import '../styles/TwoRowTooltip.css';
|
||||||
@ -162,6 +162,96 @@ const TimelineControls = ({
|
|||||||
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||||
const selectedSegment = sortedSegments.find((seg) => seg.id === selectedSegmentId);
|
const selectedSegment = sortedSegments.find((seg) => seg.id === selectedSegmentId);
|
||||||
|
|
||||||
|
// Auto-save related state
|
||||||
|
const [lastAutoSaveTime, setLastAutoSaveTime] = useState<string>('');
|
||||||
|
const [isAutoSaving, setIsAutoSaving] = useState(false);
|
||||||
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const clipSegmentsRef = useRef(clipSegments);
|
||||||
|
|
||||||
|
// Keep clipSegmentsRef updated
|
||||||
|
useEffect(() => {
|
||||||
|
clipSegmentsRef.current = clipSegments;
|
||||||
|
}, [clipSegments]);
|
||||||
|
|
||||||
|
// Auto-save function
|
||||||
|
const performAutoSave = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsAutoSaving(true);
|
||||||
|
|
||||||
|
// Format segments data for API request - use ref to get latest segments
|
||||||
|
const segments = clipSegmentsRef.current.map((segment) => ({
|
||||||
|
startTime: formatDetailedTime(segment.startTime),
|
||||||
|
endTime: formatDetailedTime(segment.endTime),
|
||||||
|
name: segment.name,
|
||||||
|
chapterTitle: segment.chapterTitle,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.debug('segments', segments);
|
||||||
|
|
||||||
|
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
|
||||||
|
// For testing, use '1234' if no mediaId is available
|
||||||
|
const finalMediaId = mediaId || '1234';
|
||||||
|
|
||||||
|
logger.debug('mediaId', finalMediaId);
|
||||||
|
|
||||||
|
if (!finalMediaId || segments.length === 0) {
|
||||||
|
logger.debug('No mediaId or segments, skipping auto-save');
|
||||||
|
setIsAutoSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, segments });
|
||||||
|
|
||||||
|
const response = await autoSaveVideo(finalMediaId, { segments });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
logger.debug('Auto-save successful');
|
||||||
|
// Format the timestamp for display
|
||||||
|
const date = new Date(response.timestamp);
|
||||||
|
const formattedTime = date
|
||||||
|
.toLocaleString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
|
||||||
|
setLastAutoSaveTime(formattedTime);
|
||||||
|
logger.debug('Auto-save successful:', formattedTime);
|
||||||
|
} else {
|
||||||
|
logger.error('Auto-save failed:', response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Auto-save error:', error);
|
||||||
|
} finally {
|
||||||
|
setIsAutoSaving(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Schedule auto-save with debounce
|
||||||
|
const scheduleAutoSave = useCallback(() => {
|
||||||
|
// Clear any existing timer
|
||||||
|
if (autoSaveTimerRef.current) {
|
||||||
|
clearTimeout(autoSaveTimerRef.current);
|
||||||
|
logger.debug('Cleared existing auto-save timer');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Scheduling new auto-save in 1 second...');
|
||||||
|
|
||||||
|
// Schedule new auto-save after 1 second of inactivity
|
||||||
|
const timerId = setTimeout(() => {
|
||||||
|
logger.debug('Auto-save timer fired! Calling performAutoSave...');
|
||||||
|
performAutoSave();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
autoSaveTimerRef.current = timerId;
|
||||||
|
logger.debug('Timer ID set:', timerId);
|
||||||
|
}, [performAutoSave]);
|
||||||
|
|
||||||
// Update editing title when selected segment changes
|
// Update editing title when selected segment changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedSegment) {
|
if (selectedSegment) {
|
||||||
@ -816,6 +906,162 @@ const TimelineControls = ({
|
|||||||
};
|
};
|
||||||
}, [isZoomDropdownOpen]);
|
}, [isZoomDropdownOpen]);
|
||||||
|
|
||||||
|
// Listen for segment updates and trigger auto-save
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSegmentUpdate = (event: CustomEvent) => {
|
||||||
|
const { recordHistory, fromAutoSave } = event.detail;
|
||||||
|
logger.debug('handleSegmentUpdate called, recordHistory:', recordHistory, 'fromAutoSave:', fromAutoSave);
|
||||||
|
// Only auto-save when history is recorded and not loading from auto-save
|
||||||
|
if (recordHistory && !fromAutoSave) {
|
||||||
|
logger.debug('Calling scheduleAutoSave from handleSegmentUpdate');
|
||||||
|
scheduleAutoSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSegmentDragEnd = () => {
|
||||||
|
// Trigger auto-save when drag operations end
|
||||||
|
scheduleAutoSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrimUpdate = (event: CustomEvent) => {
|
||||||
|
const { recordHistory } = event.detail;
|
||||||
|
// Only auto-save when history is recorded (i.e., after trim operations complete)
|
||||||
|
if (recordHistory) {
|
||||||
|
scheduleAutoSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('update-segments', handleSegmentUpdate as EventListener);
|
||||||
|
document.addEventListener('segment-drag-end', handleSegmentDragEnd);
|
||||||
|
document.addEventListener('update-trim', handleTrimUpdate as EventListener);
|
||||||
|
document.addEventListener('delete-segment', scheduleAutoSave);
|
||||||
|
document.addEventListener('split-segment', scheduleAutoSave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
logger.debug('Cleaning up auto-save event listeners...');
|
||||||
|
document.removeEventListener('update-segments', handleSegmentUpdate as EventListener);
|
||||||
|
document.removeEventListener('segment-drag-end', handleSegmentDragEnd);
|
||||||
|
document.removeEventListener('update-trim', handleTrimUpdate as EventListener);
|
||||||
|
document.removeEventListener('delete-segment', scheduleAutoSave);
|
||||||
|
document.removeEventListener('split-segment', scheduleAutoSave);
|
||||||
|
|
||||||
|
// Clear any pending auto-save timer
|
||||||
|
if (autoSaveTimerRef.current) {
|
||||||
|
logger.debug('Clearing auto-save timer in cleanup:', autoSaveTimerRef.current);
|
||||||
|
clearTimeout(autoSaveTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [scheduleAutoSave]);
|
||||||
|
|
||||||
|
// Perform initial auto-save when component mounts with segments
|
||||||
|
useEffect(() => {
|
||||||
|
if (clipSegments.length > 0 && !lastAutoSaveTime) {
|
||||||
|
// Perform initial auto-save after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
performAutoSave();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [lastAutoSaveTime, performAutoSave]);
|
||||||
|
|
||||||
|
// Load saved segments from MEDIA_DATA on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSavedSegments = () => {
|
||||||
|
// Get savedSegments directly from window.MEDIA_DATA
|
||||||
|
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.savedSegments) || null;
|
||||||
|
|
||||||
|
// If no saved segments, use default segments
|
||||||
|
if (!savedData) {
|
||||||
|
logger.debug('No saved segments found in MEDIA_DATA, using default segments');
|
||||||
|
savedData = {
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
startTime: '00:00:00.000',
|
||||||
|
endTime: '00:00:10.000',
|
||||||
|
chapterTitle: 'Chapter 1 (from saved data)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:12.000',
|
||||||
|
endTime: '00:00:17.000',
|
||||||
|
chapterTitle: 'Chapter 2 (from saved data)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:20.000',
|
||||||
|
endTime: '00:00:30.000',
|
||||||
|
chapterTitle: 'Chapter 3 (from saved data)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
updated_at: '2025-06-24 14:59:14',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Loading saved segments:', savedData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (savedData && savedData.segments && savedData.segments.length > 0) {
|
||||||
|
logger.debug('Found saved segments:', savedData);
|
||||||
|
|
||||||
|
// Convert the saved segments to the format expected by the component
|
||||||
|
const convertedSegments: Segment[] = savedData.segments.map((seg: any, index: number) => ({
|
||||||
|
id: Date.now() + index, // Generate unique IDs
|
||||||
|
name: seg.name || `Segment ${index + 1}`,
|
||||||
|
startTime: parseTimeString(seg.startTime),
|
||||||
|
endTime: parseTimeString(seg.endTime),
|
||||||
|
thumbnail: '',
|
||||||
|
chapterTitle: seg.chapterTitle || '', // Preserve chapter title from saved data
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Dispatch event to update segments
|
||||||
|
const updateEvent = new CustomEvent('update-segments', {
|
||||||
|
detail: {
|
||||||
|
segments: convertedSegments,
|
||||||
|
recordHistory: false, // Don't record loading saved segments in history
|
||||||
|
fromAutoSave: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
document.dispatchEvent(updateEvent);
|
||||||
|
|
||||||
|
// Update the last auto-save time
|
||||||
|
if (savedData.updated_at) {
|
||||||
|
const date = new Date(savedData.updated_at);
|
||||||
|
const formattedTime = date
|
||||||
|
.toLocaleString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
setLastAutoSaveTime(formattedTime);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug('No saved segments found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading saved segments:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to parse time string "HH:MM:SS.mmm" to seconds
|
||||||
|
const parseTimeString = (timeStr: string): number => {
|
||||||
|
const parts = timeStr.split(':');
|
||||||
|
if (parts.length !== 3) return 0;
|
||||||
|
|
||||||
|
const hours = parseInt(parts[0]) || 0;
|
||||||
|
const minutes = parseInt(parts[1]) || 0;
|
||||||
|
const secondsParts = parts[2].split('.');
|
||||||
|
const seconds = parseInt(secondsParts[0]) || 0;
|
||||||
|
const milliseconds = parseInt(secondsParts[1]) || 0;
|
||||||
|
|
||||||
|
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load saved segments after a short delay to ensure component is ready
|
||||||
|
setTimeout(loadSavedSegments, 100);
|
||||||
|
}, []); // Run only once on mount
|
||||||
|
|
||||||
// Global click handler to close tooltips when clicking outside
|
// Global click handler to close tooltips when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Remove the global click handler that closes tooltips
|
// Remove the global click handler that closes tooltips
|
||||||
@ -2446,6 +2692,8 @@ const TimelineControls = ({
|
|||||||
placeholder="Chapter Title"
|
placeholder="Chapter Title"
|
||||||
value={editingChapterTitle}
|
value={editingChapterTitle}
|
||||||
onChange={(e) => handleChapterTitleChange(e.target.value)}
|
onChange={(e) => handleChapterTitleChange(e.target.value)}
|
||||||
|
onBlur={performAutoSave}
|
||||||
|
onMouseLeave={performAutoSave}
|
||||||
rows={2}
|
rows={2}
|
||||||
maxLength={200}
|
maxLength={200}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@ -4424,6 +4672,41 @@ const TimelineControls = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Auto saved time */}
|
||||||
|
<div
|
||||||
|
className="auto-saved-time"
|
||||||
|
style={{
|
||||||
|
color: isAutoSaving ? '#1976d2' : 'gray',
|
||||||
|
fontSize: '12px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isAutoSaving ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="auto-save-spinner"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid #f3f3f3',
|
||||||
|
borderTop: '2px solid #1976d2',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
Auto saving...
|
||||||
|
</>
|
||||||
|
) : lastAutoSaveTime ? (
|
||||||
|
`Auto saved: ${lastAutoSaveTime}`
|
||||||
|
) : (
|
||||||
|
'Not saved yet'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Save Chapters Button */}
|
{/* Save Chapters Button */}
|
||||||
<div className="save-buttons-row">
|
<div className="save-buttons-row">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -92,8 +92,10 @@ const useVideoChapters = () => {
|
|||||||
let initialSegments: Segment[] = [];
|
let initialSegments: Segment[] = [];
|
||||||
|
|
||||||
// Check if we have existing chapters from the backend
|
// Check if we have existing chapters from the backend
|
||||||
const existingChapters = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) || [
|
const existingChapters =
|
||||||
{
|
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.chapters) ||
|
||||||
|
[
|
||||||
|
/* {
|
||||||
name: 'Chapter 1',
|
name: 'Chapter 1',
|
||||||
from: '00:00:00',
|
from: '00:00:00',
|
||||||
to: '00:00:03',
|
to: '00:00:03',
|
||||||
@ -117,8 +119,8 @@ const useVideoChapters = () => {
|
|||||||
name: 'Chapter 5',
|
name: 'Chapter 5',
|
||||||
from: '00:00:21',
|
from: '00:00:21',
|
||||||
to: '00:00:24',
|
to: '00:00:24',
|
||||||
},
|
}, */
|
||||||
];
|
];
|
||||||
|
|
||||||
if (existingChapters.length > 0) {
|
if (existingChapters.length > 0) {
|
||||||
// Create segments from existing chapters
|
// Create segments from existing chapters
|
||||||
|
|||||||
@ -1,115 +1,95 @@
|
|||||||
// API service for video trimming operations
|
// API service for video trimming operations
|
||||||
|
import logger from '../lib/logger';
|
||||||
|
|
||||||
interface TrimVideoRequest {
|
// Helper function to simulate delay
|
||||||
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
// Auto-save interface
|
||||||
|
interface AutoSaveRequest {
|
||||||
segments: {
|
segments: {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
}[];
|
}[];
|
||||||
saveAsCopy?: boolean;
|
|
||||||
saveIndividualSegments?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TrimVideoResponse {
|
interface AutoSaveResponse {
|
||||||
msg: string;
|
success: boolean;
|
||||||
url_redirect: string;
|
timestamp: string;
|
||||||
status?: number; // HTTP status code for success/error
|
error?: string;
|
||||||
error?: string; // Error message if status is not 200
|
status?: string;
|
||||||
|
media_id?: string;
|
||||||
|
segments?: {
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to simulate delay
|
// Auto-save API function
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Promise<AutoSaveResponse> => {
|
||||||
|
|
||||||
// For now, we'll use a mock API that returns a promise
|
|
||||||
// This can be replaced with actual API calls later
|
|
||||||
export const trimVideo = async (mediaId: string, data: TrimVideoRequest): Promise<TrimVideoResponse> => {
|
|
||||||
try {
|
try {
|
||||||
// Attempt the real API call
|
const response = await fetch(`/api/v1/media/${mediaId}/save_chapters`, {
|
||||||
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
|
// TODO: ask backend to add save_chapters endpoint
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.debug('response', response);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// For error responses, return with error status and message
|
// For error responses, return with error status
|
||||||
if (response.status === 400) {
|
if (response.status === 404) {
|
||||||
// Handle 400 Bad Request - return with error details
|
// If endpoint not ready (404), return mock success response
|
||||||
try {
|
const timestamp = new Date().toISOString();
|
||||||
// Try to get error details from response
|
return {
|
||||||
const errorData = await response.json();
|
success: true,
|
||||||
return {
|
timestamp: timestamp,
|
||||||
status: 400,
|
};
|
||||||
error: errorData.error || 'An error occurred during processing',
|
} else {
|
||||||
msg: 'Video Processing Error',
|
|
||||||
url_redirect: '',
|
|
||||||
};
|
|
||||||
} catch (parseError) {
|
|
||||||
// If can't parse response JSON, return generic error
|
|
||||||
return {
|
|
||||||
status: 400,
|
|
||||||
error: 'An error occurred during video processing',
|
|
||||||
msg: 'Video Processing Error',
|
|
||||||
url_redirect: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (response.status !== 404) {
|
|
||||||
// Handle other error responses
|
// Handle other error responses
|
||||||
try {
|
try {
|
||||||
// Try to get error details from response
|
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
success: false,
|
||||||
error: errorData.error || 'An error occurred during processing',
|
timestamp: new Date().toISOString(),
|
||||||
msg: 'Video Processing Error',
|
error: errorData.error || 'Auto-save failed',
|
||||||
url_redirect: '',
|
|
||||||
};
|
};
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
// If can't parse response JSON, return generic error
|
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
success: false,
|
||||||
error: 'An error occurred during video processing',
|
timestamp: new Date().toISOString(),
|
||||||
msg: 'Video Processing Error',
|
error: 'Auto-save failed',
|
||||||
url_redirect: '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// If endpoint not ready (404), return mock success response
|
|
||||||
await delay(1500); // Simulate 1.5 second server delay
|
|
||||||
return {
|
|
||||||
status: 200, // Mock success status
|
|
||||||
msg: 'Video Processed Successfully', // Updated per requirements
|
|
||||||
url_redirect: `./view?m=${mediaId}`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successful response
|
// Successful response
|
||||||
const jsonResponse = await response.json();
|
const jsonResponse = await response.json();
|
||||||
return {
|
|
||||||
status: 200,
|
// Check if the response has the expected format
|
||||||
msg: 'Video Processed Successfully', // Ensure the success message is correct
|
if (jsonResponse.status === 'success') {
|
||||||
url_redirect: jsonResponse.url_redirect || `./view?m=${mediaId}`,
|
return {
|
||||||
...jsonResponse,
|
success: true,
|
||||||
};
|
timestamp: jsonResponse.updated_at || new Date().toISOString(),
|
||||||
|
...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 with delay
|
// For any fetch errors, return mock success response
|
||||||
await delay(1500); // Simulate 1.5 second server delay
|
const timestamp = new Date().toISOString();
|
||||||
return {
|
return {
|
||||||
status: 200, // Mock success status
|
success: true,
|
||||||
msg: 'Video Processed Successfully', // Consistent with requirements
|
timestamp: timestamp,
|
||||||
url_redirect: `./view?m=${mediaId}`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mock implementation that simulates network latency
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve({
|
|
||||||
msg: "Video is processing for trim",
|
|
||||||
url_redirect: `./view?m=${mediaId}`
|
|
||||||
});
|
|
||||||
}, 1500); // Simulate 1.5 second server delay
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -723,6 +723,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-save-spinner {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,6 @@
|
|||||||
import '../styles/EditingTools.css';
|
import '../styles/EditingTools.css';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import logger from '@/lib/logger';
|
||||||
|
|
||||||
interface EditingToolsProps {
|
interface EditingToolsProps {
|
||||||
onSplit: () => void;
|
onSplit: () => void;
|
||||||
@ -42,7 +43,7 @@ const EditingTools = ({
|
|||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
// Ensure lastSeekedPosition is used when play is clicked
|
// Ensure lastSeekedPosition is used when play is clicked
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
console.log('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
|
logger.debug('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the original handler
|
// Call the original handler
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useRef, useEffect, useState } 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 { generateThumbnail, generateSolidColor } from '../lib/videoUtils';
|
||||||
import { Segment } from './ClipSegments';
|
import { Segment } from './ClipSegments';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { trimVideo } from '../services/videoApi';
|
import { trimVideo, autoSaveVideo, fetchAutoSavedSegments } from '../services/videoApi';
|
||||||
import logger from '../lib/logger';
|
import logger from '../lib/logger';
|
||||||
import '../styles/TimelineControls.css';
|
import '../styles/TimelineControls.css';
|
||||||
import '../styles/TwoRowTooltip.css';
|
import '../styles/TwoRowTooltip.css';
|
||||||
@ -47,7 +47,7 @@ interface TimelineControlsProps {
|
|||||||
isIOSUninitialized?: boolean;
|
isIOSUninitialized?: boolean;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
setIsPlaying: (playing: boolean) => void;
|
setIsPlaying: (playing: boolean) => void;
|
||||||
onPlayPause: () => void;
|
onPlayPause: () => void; // Add this prop
|
||||||
isPlayingSegments?: boolean;
|
isPlayingSegments?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +142,95 @@ const TimelineControls = ({
|
|||||||
// Reference for the scrollable container
|
// Reference for the scrollable container
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Auto-save related state
|
||||||
|
const [lastAutoSaveTime, setLastAutoSaveTime] = useState<string>('');
|
||||||
|
const [isAutoSaving, setIsAutoSaving] = useState(false);
|
||||||
|
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const clipSegmentsRef = useRef(clipSegments);
|
||||||
|
|
||||||
|
// Keep clipSegmentsRef updated
|
||||||
|
useEffect(() => {
|
||||||
|
clipSegmentsRef.current = clipSegments;
|
||||||
|
}, [clipSegments]);
|
||||||
|
|
||||||
|
// Auto-save function
|
||||||
|
const performAutoSave = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsAutoSaving(true);
|
||||||
|
|
||||||
|
// Format segments data for API request - use ref to get latest segments
|
||||||
|
const segments = clipSegmentsRef.current.map((segment) => ({
|
||||||
|
startTime: formatDetailedTime(segment.startTime),
|
||||||
|
endTime: formatDetailedTime(segment.endTime),
|
||||||
|
name: segment.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.debug('segments', segments);
|
||||||
|
|
||||||
|
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
|
||||||
|
// For testing, use '1234' if no mediaId is available
|
||||||
|
const finalMediaId = mediaId || '1234';
|
||||||
|
|
||||||
|
logger.debug('mediaId', finalMediaId);
|
||||||
|
|
||||||
|
if (!finalMediaId || segments.length === 0) {
|
||||||
|
logger.debug('No mediaId or segments, skipping auto-save');
|
||||||
|
setIsAutoSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, segments });
|
||||||
|
|
||||||
|
const response = await autoSaveVideo(finalMediaId, { segments });
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
logger.debug('Auto-save successful');
|
||||||
|
// Format the timestamp for display
|
||||||
|
const date = new Date(response.timestamp);
|
||||||
|
const formattedTime = date
|
||||||
|
.toLocaleString('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
|
||||||
|
setLastAutoSaveTime(formattedTime);
|
||||||
|
logger.debug('Auto-save successful:', formattedTime);
|
||||||
|
} else {
|
||||||
|
logger.error('Auto-save failed:', response.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Auto-save error:', error);
|
||||||
|
} finally {
|
||||||
|
setIsAutoSaving(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Schedule auto-save with debounce
|
||||||
|
const scheduleAutoSave = useCallback(() => {
|
||||||
|
// Clear any existing timer
|
||||||
|
if (autoSaveTimerRef.current) {
|
||||||
|
clearTimeout(autoSaveTimerRef.current);
|
||||||
|
logger.debug('Cleared existing auto-save timer');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Scheduling new auto-save in 1 second...');
|
||||||
|
|
||||||
|
// Schedule new auto-save after 1 second of inactivity
|
||||||
|
const timerId = setTimeout(() => {
|
||||||
|
logger.debug('Auto-save timer fired! Calling performAutoSave...');
|
||||||
|
performAutoSave();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
autoSaveTimerRef.current = timerId;
|
||||||
|
logger.debug('Timer ID set:', timerId);
|
||||||
|
}, [performAutoSave]);
|
||||||
|
|
||||||
// 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();
|
||||||
@ -926,6 +1015,177 @@ const TimelineControls = ({
|
|||||||
};
|
};
|
||||||
}, [isZoomDropdownOpen]);
|
}, [isZoomDropdownOpen]);
|
||||||
|
|
||||||
|
// Listen for segment updates and trigger auto-save
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSegmentUpdate = (event: CustomEvent) => {
|
||||||
|
const { recordHistory, fromAutoSave } = event.detail;
|
||||||
|
logger.debug('handleSegmentUpdate called, recordHistory:', recordHistory, 'fromAutoSave:', fromAutoSave);
|
||||||
|
// Only auto-save when history is recorded and not loading from auto-save
|
||||||
|
if (recordHistory && !fromAutoSave) {
|
||||||
|
logger.debug('Calling scheduleAutoSave from handleSegmentUpdate');
|
||||||
|
scheduleAutoSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSegmentDragEnd = () => {
|
||||||
|
// Trigger auto-save when drag operations end
|
||||||
|
scheduleAutoSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrimUpdate = (event: CustomEvent) => {
|
||||||
|
const { recordHistory } = event.detail;
|
||||||
|
// Only auto-save when history is recorded (i.e., after trim operations complete)
|
||||||
|
if (recordHistory) {
|
||||||
|
scheduleAutoSave();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('update-segments', handleSegmentUpdate as EventListener);
|
||||||
|
document.addEventListener('segment-drag-end', handleSegmentDragEnd);
|
||||||
|
document.addEventListener('update-trim', handleTrimUpdate as EventListener);
|
||||||
|
document.addEventListener('delete-segment', scheduleAutoSave);
|
||||||
|
document.addEventListener('split-segment', scheduleAutoSave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
logger.debug('Cleaning up auto-save event listeners...');
|
||||||
|
document.removeEventListener('update-segments', handleSegmentUpdate as EventListener);
|
||||||
|
document.removeEventListener('segment-drag-end', handleSegmentDragEnd);
|
||||||
|
document.removeEventListener('update-trim', handleTrimUpdate as EventListener);
|
||||||
|
document.removeEventListener('delete-segment', scheduleAutoSave);
|
||||||
|
document.removeEventListener('split-segment', scheduleAutoSave);
|
||||||
|
|
||||||
|
// Clear any pending auto-save timer
|
||||||
|
if (autoSaveTimerRef.current) {
|
||||||
|
logger.debug('Clearing auto-save timer in cleanup:', autoSaveTimerRef.current);
|
||||||
|
clearTimeout(autoSaveTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [scheduleAutoSave]);
|
||||||
|
|
||||||
|
// Perform initial auto-save when component mounts with segments
|
||||||
|
useEffect(() => {
|
||||||
|
if (clipSegments.length > 0 && !lastAutoSaveTime) {
|
||||||
|
// Perform initial auto-save after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
performAutoSave();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, [lastAutoSaveTime, performAutoSave]);
|
||||||
|
|
||||||
|
// Load saved segments from MEDIA_DATA on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSavedSegments = () => {
|
||||||
|
// Get savedSegments directly from window.MEDIA_DATA
|
||||||
|
let savedData = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.savedSegments) || null;
|
||||||
|
|
||||||
|
// If no saved segments, use default segments
|
||||||
|
if (!savedData) {
|
||||||
|
logger.debug('No saved segments found in MEDIA_DATA, using default segments');
|
||||||
|
savedData = {
|
||||||
|
segments: [
|
||||||
|
{
|
||||||
|
startTime: '00:00:01.130',
|
||||||
|
endTime: '00:00:05.442',
|
||||||
|
name: 'segment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:06.152',
|
||||||
|
endTime: '00:00:10.518',
|
||||||
|
name: 'segment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:11.518',
|
||||||
|
endTime: '00:00:15.121',
|
||||||
|
name: 'segment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:16.757',
|
||||||
|
endTime: '00:00:20.769',
|
||||||
|
name: 'segment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:21.158',
|
||||||
|
endTime: '00:00:25.870',
|
||||||
|
name: 'segment',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startTime: '00:00:26.430',
|
||||||
|
endTime: '00:00:29.798',
|
||||||
|
name: 'segment',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
updated_at: '2025-06-24 14:59:14',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Loading saved segments:', savedData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (savedData && savedData.segments && savedData.segments.length > 0) {
|
||||||
|
logger.debug('Found saved segments:', savedData);
|
||||||
|
|
||||||
|
// Convert the saved segments to the format expected by the component
|
||||||
|
const convertedSegments: Segment[] = savedData.segments.map((seg: any, index: number) => ({
|
||||||
|
id: Date.now() + index, // Generate unique IDs
|
||||||
|
name: seg.name || `Segment ${index + 1}`,
|
||||||
|
startTime: parseTimeString(seg.startTime),
|
||||||
|
endTime: parseTimeString(seg.endTime),
|
||||||
|
thumbnail: '',
|
||||||
|
}));
|
||||||
|
console.log('convertedSegments', convertedSegments);
|
||||||
|
|
||||||
|
// Dispatch event to update segments
|
||||||
|
const updateEvent = new CustomEvent('update-segments', {
|
||||||
|
detail: {
|
||||||
|
segments: convertedSegments,
|
||||||
|
recordHistory: false, // Don't record loading saved segments in history
|
||||||
|
fromAutoSave: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
document.dispatchEvent(updateEvent);
|
||||||
|
|
||||||
|
// Update the last auto-save time
|
||||||
|
if (savedData.updated_at) {
|
||||||
|
const date = new Date(savedData.updated_at);
|
||||||
|
const formattedTime = date
|
||||||
|
.toLocaleString('en-GB', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
})
|
||||||
|
.replace(',', '');
|
||||||
|
setLastAutoSaveTime(formattedTime);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug('No saved segments found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading saved segments:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to parse time string "HH:MM:SS.mmm" to seconds
|
||||||
|
const parseTimeString = (timeStr: string): number => {
|
||||||
|
const parts = timeStr.split(':');
|
||||||
|
if (parts.length !== 3) return 0;
|
||||||
|
|
||||||
|
const hours = parseInt(parts[0]) || 0;
|
||||||
|
const minutes = parseInt(parts[1]) || 0;
|
||||||
|
const secondsParts = parts[2].split('.');
|
||||||
|
const seconds = parseInt(secondsParts[0]) || 0;
|
||||||
|
const milliseconds = parseInt(secondsParts[1]) || 0;
|
||||||
|
|
||||||
|
return hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load saved segments after a short delay to ensure component is ready
|
||||||
|
setTimeout(loadSavedSegments, 100);
|
||||||
|
}, []); // Run only once on mount
|
||||||
|
|
||||||
// Global click handler to close tooltips when clicking outside
|
// Global click handler to close tooltips when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Remove the global click handler that closes tooltips
|
// Remove the global click handler that closes tooltips
|
||||||
@ -3124,6 +3384,7 @@ const TimelineControls = ({
|
|||||||
action: 'create_segment',
|
action: 'create_segment',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
logger.debug('Dispatching update-segments event for new segment creation');
|
||||||
document.dispatchEvent(updateEvent);
|
document.dispatchEvent(updateEvent);
|
||||||
|
|
||||||
// Close empty space tooltip
|
// Close empty space tooltip
|
||||||
@ -4512,6 +4773,41 @@ const TimelineControls = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Auto saved time */}
|
||||||
|
<div
|
||||||
|
className="auto-saved-time"
|
||||||
|
style={{
|
||||||
|
color: isAutoSaving ? '#1976d2' : 'gray',
|
||||||
|
fontSize: '12px',
|
||||||
|
marginLeft: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '5px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isAutoSaving ? (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="auto-save-spinner"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
border: '2px solid #f3f3f3',
|
||||||
|
borderTop: '2px solid #1976d2',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite',
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
Auto saving...
|
||||||
|
</>
|
||||||
|
) : lastAutoSaveTime ? (
|
||||||
|
`Auto saved: ${lastAutoSaveTime}`
|
||||||
|
) : (
|
||||||
|
'Not saved yet'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Save Buttons Row */}
|
{/* Save Buttons Row */}
|
||||||
<div className="save-buttons-row">
|
<div className="save-buttons-row">
|
||||||
{onSave && (
|
{onSave && (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// API service for video trimming operations
|
// API service for video trimming operations
|
||||||
|
import logger from '../lib/logger';
|
||||||
interface TrimVideoRequest {
|
interface TrimVideoRequest {
|
||||||
segments: {
|
segments: {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
@ -20,8 +20,95 @@ interface TrimVideoResponse {
|
|||||||
// Helper function to simulate delay
|
// Helper function to simulate delay
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
// For now, we'll use a mock API that returns a promise
|
// Auto-save interface
|
||||||
// This can be replaced with actual API calls later
|
interface AutoSaveRequest {
|
||||||
|
segments: {
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
name?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoSaveResponse {
|
||||||
|
success: boolean;
|
||||||
|
timestamp: string;
|
||||||
|
error?: string;
|
||||||
|
status?: string;
|
||||||
|
media_id?: string;
|
||||||
|
segments?: {
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save API function
|
||||||
|
export const autoSaveVideo = async (mediaId: string, data: AutoSaveRequest): Promise<AutoSaveResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/v1/media/${mediaId}/save_trim`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('response', response);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// For error responses, return with error status
|
||||||
|
if (response.status === 404) {
|
||||||
|
// If endpoint not ready (404), return mock success response
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Handle other error responses
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
error: errorData.error || 'Auto-save failed',
|
||||||
|
};
|
||||||
|
} catch (parseError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
error: 'Auto-save failed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Successful response
|
||||||
|
const jsonResponse = await response.json();
|
||||||
|
|
||||||
|
// Check if the response has the expected format
|
||||||
|
if (jsonResponse.status === 'success') {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
timestamp: jsonResponse.updated_at || new Date().toISOString(),
|
||||||
|
...jsonResponse,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
error: jsonResponse.error || 'Auto-save failed',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// For any fetch errors, return mock success response
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
timestamp: timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const trimVideo = async (mediaId: string, data: TrimVideoRequest): Promise<TrimVideoResponse> => {
|
export const trimVideo = async (mediaId: string, data: TrimVideoRequest): Promise<TrimVideoResponse> => {
|
||||||
try {
|
try {
|
||||||
// Attempt the real API call
|
// Attempt the real API call
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user