chore: video-editor update with new prettier settings

This commit is contained in:
Yiannis Christodoulou 2025-07-27 21:25:44 +03:00
parent eaf87e20d8
commit 074638e237
25 changed files with 12707 additions and 10118 deletions

View File

@ -1,5 +1,5 @@
import { formatTime, formatLongTime } from "@/lib/timeUtils"; import { formatTime, formatLongTime } from '@/lib/timeUtils';
import "../styles/ClipSegments.css"; import '../styles/ClipSegments.css';
export interface Segment { export interface Segment {
id: number; id: number;
@ -20,8 +20,8 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
// Handle delete segment click // Handle delete segment click
const handleDeleteSegment = (segmentId: number) => { const handleDeleteSegment = (segmentId: number) => {
// Create and dispatch the delete event // Create and dispatch the delete event
const deleteEvent = new CustomEvent("delete-segment", { const deleteEvent = new CustomEvent('delete-segment', {
detail: { segmentId } detail: { segmentId },
}); });
document.dispatchEvent(deleteEvent); document.dispatchEvent(deleteEvent);
}; };
@ -74,9 +74,7 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
))} ))}
{sortedSegments.length === 0 && ( {sortedSegments.length === 0 && (
<div className="empty-message"> <div className="empty-message">No segments created yet. Use the split button to create segments.</div>
No segments created yet. Use the split button to create segments.
</div>
)} )}
</div> </div>
); );

View File

@ -1,5 +1,5 @@
import "../styles/EditingTools.css"; import '../styles/EditingTools.css';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
interface EditingToolsProps { interface EditingToolsProps {
onSplit: () => void; onSplit: () => void;
@ -24,7 +24,7 @@ const EditingTools = ({
canUndo, canUndo,
canRedo, canRedo,
isPlaying = false, isPlaying = false,
isPlayingSegments = false isPlayingSegments = false,
}: EditingToolsProps) => { }: EditingToolsProps) => {
const [isSmallScreen, setIsSmallScreen] = useState(false); const [isSmallScreen, setIsSmallScreen] = useState(false);
@ -34,15 +34,15 @@ const EditingTools = ({
}; };
checkScreenSize(); checkScreenSize();
window.addEventListener("resize", checkScreenSize); window.addEventListener('resize', checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize); return () => window.removeEventListener('resize', checkScreenSize);
}, []); }, []);
// Handle play button click with iOS fix // Handle play button click with iOS fix
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); console.log('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
} }
// Call the original handler // Call the original handler
@ -59,9 +59,9 @@ const EditingTools = ({
className={`button segments-button`} className={`button segments-button`}
onClick={onPlaySegments} onClick={onPlaySegments}
data-tooltip={ data-tooltip={
isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow" isPlayingSegments ? 'Stop segments playback' : 'Play segments in one continuous flow'
} }
style={{ fontSize: "0.875rem" }} style={{ fontSize: '0.875rem' }}
> >
{isPlayingSegments ? ( {isPlayingSegments ? (
<> <>
@ -133,10 +133,10 @@ const EditingTools = ({
{/* Standard Play button (only shown when not in segments playback on small screens) */} {/* Standard Play button (only shown when not in segments playback on small screens) */}
{(!isPlayingSegments || !isSmallScreen) && ( {(!isPlayingSegments || !isSmallScreen) && (
<button <button
className={`button play-button ${isPlayingSegments ? "greyed-out" : ""}`} className={`button play-button ${isPlayingSegments ? 'greyed-out' : ''}`}
onClick={handlePlay} onClick={handlePlay}
data-tooltip={isPlaying ? "Pause video" : "Play full video"} data-tooltip={isPlaying ? 'Pause video' : 'Play full video'}
style={{ fontSize: "0.875rem" }} style={{ fontSize: '0.875rem' }}
disabled={isPlayingSegments} disabled={isPlayingSegments}
> >
{isPlaying && !isPlayingSegments ? ( {isPlaying && !isPlayingSegments ? (
@ -208,7 +208,7 @@ const EditingTools = ({
<button <button
className="button" className="button"
aria-label="Undo" aria-label="Undo"
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Undo last action"} data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Undo last action'}
disabled={!canUndo || isPlayingSegments} disabled={!canUndo || isPlayingSegments}
onClick={onUndo} onClick={onUndo}
> >
@ -229,7 +229,7 @@ const EditingTools = ({
<button <button
className="button" className="button"
aria-label="Redo" aria-label="Redo"
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Redo last undone action"} data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Redo last undone action'}
disabled={!canRedo || isPlayingSegments} disabled={!canRedo || isPlayingSegments}
onClick={onRedo} onClick={onRedo}
> >
@ -251,7 +251,7 @@ const EditingTools = ({
<button <button
className="button" className="button"
onClick={onReset} onClick={onReset}
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Reset to full video"} data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Reset to full video'}
disabled={isPlayingSegments} disabled={isPlayingSegments}
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from 'react';
import "../styles/IOSPlayPrompt.css"; import '../styles/IOSPlayPrompt.css';
interface MobilePlayPromptProps { interface MobilePlayPromptProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
@ -33,9 +33,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
setIsVisible(false); setIsVisible(false);
}; };
video.addEventListener("play", handlePlay); video.addEventListener('play', handlePlay);
return () => { return () => {
video.removeEventListener("play", handlePlay); video.removeEventListener('play', handlePlay);
}; };
}, [videoRef]); }, [videoRef]);

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from 'react';
import { formatTime } from "@/lib/timeUtils"; import { formatTime } from '@/lib/timeUtils';
import "../styles/IOSVideoPlayer.css"; import '../styles/IOSVideoPlayer.css';
interface IOSVideoPlayerProps { interface IOSVideoPlayerProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
@ -9,7 +9,7 @@ interface IOSVideoPlayerProps {
} }
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => { const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>(""); const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null); const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
// Refs for hold-to-continue functionality // Refs for hold-to-continue functionality
@ -26,14 +26,14 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
// Get the video source URL from the main player // Get the video source URL from the main player
useEffect(() => { useEffect(() => {
if (videoRef.current && videoRef.current.querySelector("source")) { if (videoRef.current && videoRef.current.querySelector('source')) {
const source = videoRef.current.querySelector("source") as HTMLSourceElement; const source = videoRef.current.querySelector('source') as HTMLSourceElement;
if (source && source.src) { if (source && source.src) {
setVideoUrl(source.src); setVideoUrl(source.src);
} }
} else { } else {
// Fallback to sample video if needed // Fallback to sample video if needed
setVideoUrl("/videos/sample-video-10m.mp4"); setVideoUrl('/videos/sample-video.mp4');
} }
}, [videoRef]); }, [videoRef]);

View File

@ -1,5 +1,5 @@
import React, { useEffect } from "react"; import React, { useEffect } from 'react';
import "../styles/Modal.css"; import '../styles/Modal.css';
interface ModalProps { interface ModalProps {
isOpen: boolean; isOpen: boolean;
@ -13,21 +13,21 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions
// Close modal when Escape key is pressed // Close modal when Escape key is pressed
useEffect(() => { useEffect(() => {
const handleEscapeKey = (event: KeyboardEvent) => { const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === "Escape" && isOpen) { if (event.key === 'Escape' && isOpen) {
onClose(); onClose();
} }
}; };
document.addEventListener("keydown", handleEscapeKey); document.addEventListener('keydown', handleEscapeKey);
// Disable body scrolling when modal is open // Disable body scrolling when modal is open
if (isOpen) { if (isOpen) {
document.body.style.overflow = "hidden"; document.body.style.overflow = 'hidden';
} }
return () => { return () => {
document.removeEventListener("keydown", handleEscapeKey); document.removeEventListener('keydown', handleEscapeKey);
document.body.style.overflow = ""; document.body.style.overflow = '';
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);

View File

@ -1,7 +1,7 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from 'react';
import { formatTime, formatDetailedTime } from "@/lib/timeUtils"; import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
import logger from "../lib/logger"; import logger from '../lib/logger';
import "../styles/VideoPlayer.css"; import '../styles/VideoPlayer.css';
interface VideoPlayerProps { interface VideoPlayerProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
@ -22,7 +22,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
isMuted = false, isMuted = false,
onPlayPause, onPlayPause,
onSeek, onSeek,
onToggleMute onToggleMute,
}) => { }) => {
const progressRef = useRef<HTMLDivElement>(null); const progressRef = useRef<HTMLDivElement>(null);
const [isIOS, setIsIOS] = useState(false); const [isIOS, setIsIOS] = useState(false);
@ -30,12 +30,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [lastPosition, setLastPosition] = useState<number | null>(null); const [lastPosition, setLastPosition] = useState<number | null>(null);
const [isDraggingProgress, setIsDraggingProgress] = useState(false); const [isDraggingProgress, setIsDraggingProgress] = useState(false);
const isDraggingProgressRef = useRef(false); const isDraggingProgressRef = useRef(false);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 }); const [tooltipPosition, setTooltipPosition] = useState({
x: 0,
});
const [tooltipTime, setTooltipTime] = useState(0); const [tooltipTime, setTooltipTime] = useState(0);
const sampleVideoUrl = const sampleVideoUrl =
(typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) || (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
"/videos/sample-video-10m.mp4";
// Detect iOS device // Detect iOS device
useEffect(() => { useEffect(() => {
@ -47,8 +48,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
setIsIOS(checkIOS()); setIsIOS(checkIOS());
// Check if video was previously initialized // Check if video was previously initialized
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
const wasInitialized = localStorage.getItem("video_initialized") === "true"; const wasInitialized = localStorage.getItem('video_initialized') === 'true';
setHasInitialized(wasInitialized); setHasInitialized(wasInitialized);
} }
}, []); }, []);
@ -57,8 +58,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
useEffect(() => { useEffect(() => {
if (isPlaying && !hasInitialized) { if (isPlaying && !hasInitialized) {
setHasInitialized(true); setHasInitialized(true);
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
localStorage.setItem("video_initialized", "true"); localStorage.setItem('video_initialized', 'true');
} }
} }
}, [isPlaying, hasInitialized]); }, [isPlaying, hasInitialized]);
@ -70,15 +71,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
// These attributes need to be set directly on the DOM element // These attributes need to be set directly on the DOM element
// for iOS Safari to respect inline playback // for iOS Safari to respect inline playback
video.setAttribute("playsinline", "true"); video.setAttribute('playsinline', 'true');
video.setAttribute("webkit-playsinline", "true"); video.setAttribute('webkit-playsinline', 'true');
video.setAttribute("x-webkit-airplay", "allow"); video.setAttribute('x-webkit-airplay', 'allow');
// Store the last known good position for iOS // Store the last known good position for iOS
const handleTimeUpdate = () => { const handleTimeUpdate = () => {
if (!isDraggingProgressRef.current) { if (!isDraggingProgressRef.current) {
setLastPosition(video.currentTime); setLastPosition(video.currentTime);
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
window.lastSeekedPosition = video.currentTime; window.lastSeekedPosition = video.currentTime;
} }
} }
@ -86,25 +87,25 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
// Handle iOS-specific play/pause state // Handle iOS-specific play/pause state
const handlePlay = () => { const handlePlay = () => {
logger.debug("Video play event fired"); logger.debug('Video play event fired');
if (isIOS) { if (isIOS) {
setHasInitialized(true); setHasInitialized(true);
localStorage.setItem("video_initialized", "true"); localStorage.setItem('video_initialized', 'true');
} }
}; };
const handlePause = () => { const handlePause = () => {
logger.debug("Video pause event fired"); logger.debug('Video pause event fired');
}; };
video.addEventListener("timeupdate", handleTimeUpdate); video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener("play", handlePlay); video.addEventListener('play', handlePlay);
video.addEventListener("pause", handlePause); video.addEventListener('pause', handlePause);
return () => { return () => {
video.removeEventListener("timeupdate", handleTimeUpdate); video.removeEventListener('timeupdate', handleTimeUpdate);
video.removeEventListener("play", handlePlay); video.removeEventListener('play', handlePlay);
video.removeEventListener("pause", handlePause); video.removeEventListener('pause', handlePause);
}; };
}, [videoRef, isIOS, isDraggingProgressRef]); }, [videoRef, isIOS, isDraggingProgressRef]);
@ -150,12 +151,12 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleMouseUp = () => { const handleMouseUp = () => {
setIsDraggingProgress(false); setIsDraggingProgress(false);
isDraggingProgressRef.current = false; isDraggingProgressRef.current = false;
document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
document.addEventListener("mousemove", handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
document.addEventListener("mouseup", handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
}; };
// Handle progress dragging for both mouse and touch events // Handle progress dragging for both mouse and touch events
@ -167,14 +168,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const seekTime = duration * clickPosition; const seekTime = duration * clickPosition;
// Update tooltip position and time // Update tooltip position and time
setTooltipPosition({ x: e.clientX }); setTooltipPosition({
x: e.clientX,
});
setTooltipTime(seekTime); setTooltipTime(seekTime);
// Store position locally for iOS Safari - critical for timeline seeking // Store position locally for iOS Safari - critical for timeline seeking
setLastPosition(seekTime); setLastPosition(seekTime);
// Also store globally for integration with other components // Also store globally for integration with other components
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
(window as any).lastSeekedPosition = seekTime; (window as any).lastSeekedPosition = seekTime;
} }
@ -202,14 +205,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleTouchEnd = () => { const handleTouchEnd = () => {
setIsDraggingProgress(false); setIsDraggingProgress(false);
isDraggingProgressRef.current = false; isDraggingProgressRef.current = false;
document.removeEventListener("touchmove", handleTouchMove); document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener("touchend", handleTouchEnd); document.removeEventListener('touchend', handleTouchEnd);
document.removeEventListener("touchcancel", handleTouchEnd); document.removeEventListener('touchcancel', handleTouchEnd);
}; };
document.addEventListener("touchmove", handleTouchMove, { passive: false }); document.addEventListener('touchmove', handleTouchMove, {
document.addEventListener("touchend", handleTouchEnd); passive: false,
document.addEventListener("touchcancel", handleTouchEnd); });
document.addEventListener('touchend', handleTouchEnd);
document.addEventListener('touchcancel', handleTouchEnd);
}; };
// Handle touch dragging on progress bar // Handle touch dragging on progress bar
@ -217,7 +222,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
if (!progressRef.current) return; if (!progressRef.current) return;
// Get the touch coordinates // Get the touch coordinates
const touch = "touches" in e ? e.touches[0] : null; const touch = 'touches' in e ? e.touches[0] : null;
if (!touch) return; if (!touch) return;
e.preventDefault(); // Prevent scrolling while dragging e.preventDefault(); // Prevent scrolling while dragging
@ -227,14 +232,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const seekTime = duration * touchPosition; const seekTime = duration * touchPosition;
// Update tooltip position and time // Update tooltip position and time
setTooltipPosition({ x: touch.clientX }); setTooltipPosition({
x: touch.clientX,
});
setTooltipTime(seekTime); setTooltipTime(seekTime);
// Store position for iOS Safari // Store position for iOS Safari
setLastPosition(seekTime); setLastPosition(seekTime);
// Also store globally for integration with other components // Also store globally for integration with other components
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
(window as any).lastSeekedPosition = seekTime; (window as any).lastSeekedPosition = seekTime;
} }
@ -255,7 +262,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
setLastPosition(seekTime); setLastPosition(seekTime);
// Also store globally for integration with other components // Also store globally for integration with other components
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
(window as any).lastSeekedPosition = seekTime; (window as any).lastSeekedPosition = seekTime;
} }
@ -283,7 +290,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
if (video.paused) { if (video.paused) {
// For iOS Safari: Before playing, explicitly seek to the remembered position // For iOS Safari: Before playing, explicitly seek to the remembered position
if (isIOS && lastPosition !== null && lastPosition > 0) { if (isIOS && lastPosition !== null && lastPosition > 0) {
logger.debug("iOS: Explicitly setting position before play:", lastPosition); logger.debug('iOS: Explicitly setting position before play:', lastPosition);
// First, seek to the position // First, seek to the position
video.currentTime = lastPosition; video.currentTime = lastPosition;
@ -296,13 +303,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
.play() .play()
.then(() => { .then(() => {
logger.debug( logger.debug(
"iOS: Play started successfully at position:", 'iOS: Play started successfully at position:',
videoRef.current?.currentTime videoRef.current?.currentTime
); );
onPlayPause(); // Update parent state after successful play onPlayPause(); // Update parent state after successful play
}) })
.catch((err) => { .catch((err) => {
console.error("iOS: Error playing video:", err); console.error('iOS: Error playing video:', err);
}); });
} }
}, 50); }, 50);
@ -311,11 +318,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
video video
.play() .play()
.then(() => { .then(() => {
logger.debug("Normal: Play started successfully"); logger.debug('Normal: Play started successfully');
onPlayPause(); // Update parent state after successful play onPlayPause(); // Update parent state after successful play
}) })
.catch((err) => { .catch((err) => {
console.error("Error playing video:", err); console.error('Error playing video:', err);
}); });
} }
} else { } else {
@ -350,7 +357,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
)} )}
{/* Play/Pause Indicator (shows based on current state) */} {/* Play/Pause Indicator (shows based on current state) */}
<div className={`play-pause-indicator ${isPlaying ? "pause-icon" : "play-icon"}`}></div> <div className={`play-pause-indicator ${isPlaying ? 'pause-icon' : 'play-icon'}`}></div>
{/* Video Controls Overlay */} {/* Video Controls Overlay */}
<div className="video-controls"> <div className="video-controls">
@ -363,13 +370,23 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
{/* Progress Bar with enhanced dragging */} {/* Progress Bar with enhanced dragging */}
<div <div
ref={progressRef} ref={progressRef}
className={`video-progress ${isDraggingProgress ? "dragging" : ""}`} className={`video-progress ${isDraggingProgress ? 'dragging' : ''}`}
onClick={handleProgressClick} onClick={handleProgressClick}
onMouseDown={handleProgressDragStart} onMouseDown={handleProgressDragStart}
onTouchStart={handleProgressTouchStart} onTouchStart={handleProgressTouchStart}
> >
<div className="video-progress-fill" style={{ width: `${progressPercentage}%` }}></div> <div
<div className="video-scrubber" style={{ left: `${progressPercentage}%` }}></div> className="video-progress-fill"
style={{
width: `${progressPercentage}%`,
}}
></div>
<div
className="video-scrubber"
style={{
left: `${progressPercentage}%`,
}}
></div>
{/* Floating time tooltip when dragging */} {/* Floating time tooltip when dragging */}
{isDraggingProgress && ( {isDraggingProgress && (
@ -377,7 +394,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
className="video-time-tooltip" className="video-time-tooltip"
style={{ style={{
left: `${tooltipPosition.x}px`, left: `${tooltipPosition.x}px`,
transform: "translateX(-50%)" transform: 'translateX(-50%)',
}} }}
> >
{formatDetailedTime(tooltipTime)} {formatDetailedTime(tooltipTime)}
@ -391,9 +408,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
{onToggleMute && ( {onToggleMute && (
<button <button
className="mute-button" className="mute-button"
aria-label={isMuted ? "Unmute" : "Mute"} aria-label={isMuted ? 'Unmute' : 'Mute'}
onClick={onToggleMute} onClick={onToggleMute}
data-tooltip={isMuted ? "Unmute" : "Mute"} data-tooltip={isMuted ? 'Unmute' : 'Mute'}
> >
{isMuted ? ( {isMuted ? (
<svg <svg

View File

@ -1,8 +1,8 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from 'react';
import { generateThumbnail } from "@/lib/videoUtils"; 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';
// Represents a state of the editor for undo/redo // Represents a state of the editor for undo/redo
interface EditorState { interface EditorState {
@ -46,21 +46,18 @@ const useVideoTrimmer = () => {
useEffect(() => { useEffect(() => {
if (history.length > 0) { if (history.length > 0) {
// For debugging - moved to console.debug // For debugging - moved to console.debug
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug( console.debug(`History state updated: ${history.length} entries, position: ${historyPosition}`);
`History state updated: ${history.length} entries, position: ${historyPosition}`
);
// Log actions in history to help debug undo/redo // Log actions in history to help debug undo/redo
const actions = history.map( const actions = history.map(
(state, idx) => (state, idx) => `${idx}: ${state.action || 'unknown'} (segments: ${state.clipSegments.length})`
`${idx}: ${state.action || "unknown"} (segments: ${state.clipSegments.length})`
); );
console.debug("History actions:", actions); console.debug('History actions:', actions);
} }
// If there's at least one history entry and it wasn't a save operation, mark as having unsaved changes // If there's at least one history entry and it wasn't a save operation, mark as having unsaved changes
const lastAction = history[historyPosition]?.action || ""; const lastAction = history[historyPosition]?.action || '';
if (lastAction !== "save" && lastAction !== "save_copy" && lastAction !== "save_segments") { if (lastAction !== 'save' && lastAction !== 'save_copy' && lastAction !== 'save_segments') {
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
} }
} }
@ -72,7 +69,7 @@ const useVideoTrimmer = () => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => { const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasUnsavedChanges) { if (hasUnsavedChanges) {
// Standard way of showing a confirmation dialog before leaving // Standard way of showing a confirmation dialog before leaving
const message = "Your edits will get lost if you leave the page. Do you want to continue?"; const message = 'Your edits will get lost if you leave the page. Do you want to continue?';
e.preventDefault(); e.preventDefault();
e.returnValue = message; // Chrome requires returnValue to be set e.returnValue = message; // Chrome requires returnValue to be set
return message; // For other browsers return message; // For other browsers
@ -80,11 +77,11 @@ const useVideoTrimmer = () => {
}; };
// Add event listener // Add event listener
window.addEventListener("beforeunload", handleBeforeUnload); window.addEventListener('beforeunload', handleBeforeUnload);
// Clean up // Clean up
return () => { return () => {
window.removeEventListener("beforeunload", handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload);
}; };
}, [hasUnsavedChanges]); }, [hasUnsavedChanges]);
@ -105,10 +102,10 @@ const useVideoTrimmer = () => {
// Create an initial segment that spans the entire video // Create an initial segment that spans the entire video
const initialSegment: Segment = { const initialSegment: Segment = {
id: 1, id: 1,
name: "segment", name: 'segment',
startTime: 0, startTime: 0,
endTime: video.duration, endTime: video.duration,
thumbnail: segmentThumbnail thumbnail: segmentThumbnail,
}; };
// Initialize history state with the full-length segment // Initialize history state with the full-length segment
@ -116,7 +113,7 @@ const useVideoTrimmer = () => {
trimStart: 0, trimStart: 0,
trimEnd: video.duration, trimEnd: video.duration,
splitPoints: [], splitPoints: [],
clipSegments: [initialSegment] clipSegments: [initialSegment],
}; };
setHistory([initialState]); setHistory([initialState]);
@ -159,19 +156,19 @@ const useVideoTrimmer = () => {
}; };
// Add event listeners // Add event listeners
video.addEventListener("loadedmetadata", handleLoadedMetadata); video.addEventListener('loadedmetadata', handleLoadedMetadata);
video.addEventListener("timeupdate", handleTimeUpdate); video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener("play", handlePlay); video.addEventListener('play', handlePlay);
video.addEventListener("pause", handlePause); video.addEventListener('pause', handlePause);
video.addEventListener("ended", handleEnded); video.addEventListener('ended', handleEnded);
return () => { return () => {
// Remove event listeners // Remove event listeners
video.removeEventListener("loadedmetadata", handleLoadedMetadata); video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener("timeupdate", handleTimeUpdate); video.removeEventListener('timeupdate', handleTimeUpdate);
video.removeEventListener("play", handlePlay); video.removeEventListener('play', handlePlay);
video.removeEventListener("pause", handlePause); video.removeEventListener('pause', handlePause);
video.removeEventListener("ended", handleEnded); video.removeEventListener('ended', handleEnded);
}; };
}, []); }, []);
@ -184,7 +181,7 @@ const useVideoTrimmer = () => {
video.pause(); video.pause();
} else { } else {
// iOS Safari fix: Use the last seeked position if available // iOS Safari fix: Use the last seeked position if available
if (!isPlaying && typeof window !== "undefined" && window.lastSeekedPosition > 0) { if (!isPlaying && typeof window !== 'undefined' && window.lastSeekedPosition > 0) {
// Only apply this if the video is not at the same position already // Only apply this if the video is not at the same position already
// This avoids unnecessary seeking which might cause playback issues // This avoids unnecessary seeking which might cause playback issues
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) { if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
@ -201,12 +198,12 @@ const useVideoTrimmer = () => {
.then(() => { .then(() => {
// Play started successfully // Play started successfully
// Reset the last seeked position after successfully starting playback // Reset the last seeked position after successfully starting playback
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
window.lastSeekedPosition = 0; window.lastSeekedPosition = 0;
} }
}) })
.catch((err) => { .catch((err) => {
console.error("Error starting playback:", err); console.error('Error starting playback:', err);
setIsPlaying(false); // Reset state if play failed setIsPlaying(false); // Reset state if play failed
}); });
} }
@ -226,7 +223,7 @@ const useVideoTrimmer = () => {
// Store the position in a global state accessible to iOS Safari // Store the position in a global state accessible to iOS Safari
// This ensures when play is pressed later, it remembers the position // This ensures when play is pressed later, it remembers the position
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
window.lastSeekedPosition = time; window.lastSeekedPosition = time;
} }
@ -239,7 +236,7 @@ const useVideoTrimmer = () => {
setIsPlaying(true); // Update state to reflect we're playing setIsPlaying(true); // Update state to reflect we're playing
}) })
.catch((err) => { .catch((err) => {
console.error("Error resuming playback:", err); console.error('Error resuming playback:', err);
setIsPlaying(false); setIsPlaying(false);
}); });
} }
@ -254,7 +251,7 @@ const useVideoTrimmer = () => {
trimEnd, trimEnd,
splitPoints: [...splitPoints], splitPoints: [...splitPoints],
clipSegments: JSON.parse(JSON.stringify(clipSegments)), // Deep clone to avoid reference issues clipSegments: JSON.parse(JSON.stringify(clipSegments)), // Deep clone to avoid reference issues
action: action || "manual_save" // Track the action that triggered this save action: action || 'manual_save', // Track the action that triggered this save
}; };
// Check if state is significantly different from last saved state // Check if state is significantly different from last saved state
@ -339,16 +336,16 @@ const useVideoTrimmer = () => {
if (recordHistory) { if (recordHistory) {
// Use a small timeout to ensure the state is updated // Use a small timeout to ensure the state is updated
setTimeout(() => { setTimeout(() => {
saveState(action || (isStart ? "adjust_trim_start" : "adjust_trim_end")); saveState(action || (isStart ? 'adjust_trim_start' : 'adjust_trim_end'));
}, 10); }, 10);
} }
} }
}; };
document.addEventListener("update-trim", handleTrimUpdate as EventListener); document.addEventListener('update-trim', handleTrimUpdate as EventListener);
return () => { return () => {
document.removeEventListener("update-trim", handleTrimUpdate as EventListener); document.removeEventListener('update-trim', handleTrimUpdate as EventListener);
}; };
}, []); }, []);
@ -360,11 +357,11 @@ const useVideoTrimmer = () => {
// Default to true to ensure all segment changes are recorded // Default to true to ensure all segment changes are recorded
const isSignificantChange = e.detail.recordHistory !== false; const isSignificantChange = e.detail.recordHistory !== false;
// Get the action type if provided // Get the action type if provided
const actionType = e.detail.action || "update_segments"; const actionType = e.detail.action || 'update_segments';
// Log the update details // Log the update details
logger.debug( logger.debug(
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}` `Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? 'true' : 'false'}`
); );
// Update segment state immediately for UI feedback // Update segment state immediately for UI feedback
@ -384,7 +381,7 @@ const useVideoTrimmer = () => {
trimEnd, trimEnd,
splitPoints: [...splitPoints], splitPoints: [...splitPoints],
clipSegments: segmentsClone, clipSegments: segmentsClone,
action: actionType // Store the action type in the state action: actionType, // Store the action type in the state
}; };
// Get the current history position to ensure we're using the latest value // Get the current history position to ensure we're using the latest value
@ -405,16 +402,12 @@ const useVideoTrimmer = () => {
// Ensure the historyPosition is updated to the correct position // Ensure the historyPosition is updated to the correct position
setHistoryPosition((prev) => { setHistoryPosition((prev) => {
const newPosition = prev + 1; const newPosition = prev + 1;
logger.debug( logger.debug(`Saved state with action: ${actionType} to history position ${newPosition}`);
`Saved state with action: ${actionType} to history position ${newPosition}`
);
return newPosition; return newPosition;
}); });
}, 20); // Slightly increased delay to ensure state updates are complete }, 20); // Slightly increased delay to ensure state updates are complete
} else { } else {
logger.debug( logger.debug(`Skipped saving state to history for action: ${actionType} (recordHistory=false)`);
`Skipped saving state to history for action: ${actionType} (recordHistory=false)`
);
} }
} }
}; };
@ -423,8 +416,8 @@ const useVideoTrimmer = () => {
const customEvent = e as CustomEvent; const customEvent = e as CustomEvent;
if ( if (
customEvent.detail && customEvent.detail &&
typeof customEvent.detail.time === "number" && typeof customEvent.detail.time === 'number' &&
typeof customEvent.detail.segmentId === "number" typeof customEvent.detail.segmentId === 'number'
) { ) {
// Get the time and segment ID from the event // Get the time and segment ID from the event
const timeToSplit = customEvent.detail.time; const timeToSplit = customEvent.detail.time;
@ -457,7 +450,7 @@ const useVideoTrimmer = () => {
name: `${segmentToSplit.name}-A`, name: `${segmentToSplit.name}-A`,
startTime: segmentToSplit.startTime, startTime: segmentToSplit.startTime,
endTime: timeToSplit, endTime: timeToSplit,
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Create second half of the split segment - no thumbnail needed // Create second half of the split segment - no thumbnail needed
@ -466,7 +459,7 @@ const useVideoTrimmer = () => {
name: `${segmentToSplit.name}-B`, name: `${segmentToSplit.name}-B`,
startTime: timeToSplit, startTime: timeToSplit,
endTime: segmentToSplit.endTime, endTime: segmentToSplit.endTime,
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Add the new segments // Add the new segments
@ -477,14 +470,14 @@ const useVideoTrimmer = () => {
// Update state // Update state
setClipSegments(newSegments); setClipSegments(newSegments);
saveState("split_segment"); saveState('split_segment');
} }
}; };
// Handle delete segment event // Handle delete segment event
const handleDeleteSegment = async (e: Event) => { const handleDeleteSegment = async (e: Event) => {
const customEvent = e as CustomEvent; const customEvent = e as CustomEvent;
if (customEvent.detail && typeof customEvent.detail.segmentId === "number") { if (customEvent.detail && typeof customEvent.detail.segmentId === 'number') {
const segmentId = customEvent.detail.segmentId; const segmentId = customEvent.detail.segmentId;
// Find and remove the segment // Find and remove the segment
@ -497,10 +490,10 @@ const useVideoTrimmer = () => {
// No need to generate a thumbnail - we'll use dynamic colors // 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", name: 'segment',
startTime: 0, startTime: 0,
endTime: videoRef.current.duration, endTime: videoRef.current.duration,
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
// Reset the trim points as well // Reset the trim points as well
@ -512,32 +505,32 @@ const useVideoTrimmer = () => {
// Just update the segments normally // Just update the segments normally
setClipSegments(newSegments); setClipSegments(newSegments);
} }
saveState("delete_segment"); saveState('delete_segment');
} }
} }
}; };
document.addEventListener("update-segments", handleUpdateSegments as EventListener); document.addEventListener('update-segments', handleUpdateSegments as EventListener);
document.addEventListener("split-segment", handleSplitSegment as EventListener); document.addEventListener('split-segment', handleSplitSegment as EventListener);
document.addEventListener("delete-segment", handleDeleteSegment as EventListener); document.addEventListener('delete-segment', handleDeleteSegment as EventListener);
return () => { return () => {
document.removeEventListener("update-segments", handleUpdateSegments as EventListener); document.removeEventListener('update-segments', handleUpdateSegments as EventListener);
document.removeEventListener("split-segment", handleSplitSegment as EventListener); document.removeEventListener('split-segment', handleSplitSegment as EventListener);
document.removeEventListener("delete-segment", handleDeleteSegment as EventListener); document.removeEventListener('delete-segment', handleDeleteSegment as EventListener);
}; };
}, [clipSegments, duration]); }, [clipSegments, duration]);
// Handle trim start change // Handle trim start change
const handleTrimStartChange = (time: number) => { const handleTrimStartChange = (time: number) => {
setTrimStart(time); setTrimStart(time);
saveState("adjust_trim_start"); saveState('adjust_trim_start');
}; };
// Handle trim end change // Handle trim end change
const handleTrimEndChange = (time: number) => { const handleTrimEndChange = (time: number) => {
setTrimEnd(time); setTrimEnd(time);
saveState("adjust_trim_end"); saveState('adjust_trim_end');
}; };
// Handle split at current position // Handle split at current position
@ -563,7 +556,7 @@ const useVideoTrimmer = () => {
name: `Segment ${i + 1}`, name: `Segment ${i + 1}`,
startTime, startTime,
endTime, endTime,
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}); });
startTime = endTime; startTime = endTime;
@ -571,7 +564,7 @@ const useVideoTrimmer = () => {
} }
setClipSegments(newSegments); setClipSegments(newSegments);
saveState("create_split_points"); saveState('create_split_points');
} }
}; };
@ -587,14 +580,14 @@ const useVideoTrimmer = () => {
// No need to generate thumbnails - we'll use dynamic colors // No need to generate thumbnails - we'll use dynamic colors
const defaultSegment: Segment = { const defaultSegment: Segment = {
id: Date.now(), id: Date.now(),
name: "segment", name: 'segment',
startTime: 0, startTime: 0,
endTime: duration, endTime: duration,
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
}; };
setClipSegments([defaultSegment]); setClipSegments([defaultSegment]);
saveState("reset_all"); saveState('reset_all');
}; };
// Handle undo // Handle undo
@ -607,7 +600,7 @@ const useVideoTrimmer = () => {
// Log segment details to help debug // Log segment details to help debug
logger.debug( logger.debug(
"Segment details after undo:", 'Segment details after undo:',
previousState.clipSegments.map( previousState.clipSegments.map(
(seg) => (seg) =>
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}` `ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
@ -621,7 +614,7 @@ const useVideoTrimmer = () => {
setClipSegments(JSON.parse(JSON.stringify(previousState.clipSegments))); setClipSegments(JSON.parse(JSON.stringify(previousState.clipSegments)));
setHistoryPosition(historyPosition - 1); setHistoryPosition(historyPosition - 1);
} else { } else {
logger.debug("Cannot undo: at earliest history position"); logger.debug('Cannot undo: at earliest history position');
} }
}; };
@ -635,7 +628,7 @@ const useVideoTrimmer = () => {
// Log segment details to help debug // Log segment details to help debug
logger.debug( logger.debug(
"Segment details after redo:", 'Segment details after redo:',
nextState.clipSegments.map( nextState.clipSegments.map(
(seg) => (seg) =>
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}` `ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
@ -649,7 +642,7 @@ const useVideoTrimmer = () => {
setClipSegments(JSON.parse(JSON.stringify(nextState.clipSegments))); setClipSegments(JSON.parse(JSON.stringify(nextState.clipSegments)));
setHistoryPosition(historyPosition + 1); setHistoryPosition(historyPosition + 1);
} else { } else {
logger.debug("Cannot redo: at latest history position"); logger.debug('Cannot redo: at latest history position');
} }
}; };
@ -669,10 +662,10 @@ const useVideoTrimmer = () => {
setIsPlaying(false); setIsPlaying(false);
} else { } else {
// iOS Safari fix: Check for lastSeekedPosition // iOS Safari fix: Check for lastSeekedPosition
if (typeof window !== "undefined" && window.lastSeekedPosition > 0) { if (typeof window !== 'undefined' && window.lastSeekedPosition > 0) {
// Only seek if the position is significantly different // Only seek if the position is significantly different
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) { if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
console.log("handlePlay: Using lastSeekedPosition", window.lastSeekedPosition); console.log('handlePlay: Using lastSeekedPosition', window.lastSeekedPosition);
video.currentTime = window.lastSeekedPosition; video.currentTime = window.lastSeekedPosition;
} }
} }
@ -683,12 +676,12 @@ const useVideoTrimmer = () => {
.then(() => { .then(() => {
setIsPlaying(true); setIsPlaying(true);
// Reset lastSeekedPosition after successful play // Reset lastSeekedPosition after successful play
if (typeof window !== "undefined") { if (typeof window !== 'undefined') {
window.lastSeekedPosition = 0; window.lastSeekedPosition = 0;
} }
}) })
.catch((err) => { .catch((err) => {
console.error("Error playing video:", err); console.error('Error playing video:', err);
setIsPlaying(false); // Reset state if play failed setIsPlaying(false); // Reset state if play failed
}); });
} }
@ -710,28 +703,28 @@ const useVideoTrimmer = () => {
// Create the JSON data for saving // Create the JSON data for saving
const saveData = { const saveData = {
type: "save", type: 'save',
segments: sortedSegments.map((segment) => ({ segments: sortedSegments.map((segment) => ({
startTime: formatDetailedTime(segment.startTime), startTime: formatDetailedTime(segment.startTime),
endTime: formatDetailedTime(segment.endTime) endTime: formatDetailedTime(segment.endTime),
})) })),
}; };
// Display JSON in alert (for demonstration purposes) // Display JSON in alert (for demonstration purposes)
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug("Saving data:", saveData); console.debug('Saving data:', saveData);
} }
// Mark as saved - no unsaved changes // Mark as saved - no unsaved changes
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
// Debug message // Debug message
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug("Changes saved - reset unsaved changes flag"); console.debug('Changes saved - reset unsaved changes flag');
} }
// Save to history with special "save" action to mark saved state // Save to history with special "save" action to mark saved state
saveState("save"); saveState('save');
// In a real implementation, this would make a POST request to save the data // In a real implementation, this would make a POST request to save the data
// logger.debug("Save data:", saveData); // logger.debug("Save data:", saveData);
@ -744,28 +737,28 @@ const useVideoTrimmer = () => {
// Create the JSON data for saving as a copy // Create the JSON data for saving as a copy
const saveData = { const saveData = {
type: "save_as_a_copy", type: 'save_as_a_copy',
segments: sortedSegments.map((segment) => ({ segments: sortedSegments.map((segment) => ({
startTime: formatDetailedTime(segment.startTime), startTime: formatDetailedTime(segment.startTime),
endTime: formatDetailedTime(segment.endTime) endTime: formatDetailedTime(segment.endTime),
})) })),
}; };
// Display JSON in alert (for demonstration purposes) // Display JSON in alert (for demonstration purposes)
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug("Saving data as copy:", saveData); console.debug('Saving data as copy:', saveData);
} }
// Mark as saved - no unsaved changes // Mark as saved - no unsaved changes
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
// Debug message // Debug message
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug("Changes saved as copy - reset unsaved changes flag"); console.debug('Changes saved as copy - reset unsaved changes flag');
} }
// Save to history with special "save_copy" action to mark saved state // Save to history with special "save_copy" action to mark saved state
saveState("save_copy"); saveState('save_copy');
}; };
// Handle save segments individually action // Handle save segments individually action
@ -775,27 +768,27 @@ const useVideoTrimmer = () => {
// Create the JSON data for saving individual segments // Create the JSON data for saving individual segments
const saveData = { const saveData = {
type: "save_segments", type: 'save_segments',
segments: sortedSegments.map((segment) => ({ segments: sortedSegments.map((segment) => ({
name: segment.name, name: segment.name,
startTime: formatDetailedTime(segment.startTime), startTime: formatDetailedTime(segment.startTime),
endTime: formatDetailedTime(segment.endTime) endTime: formatDetailedTime(segment.endTime),
})) })),
}; };
// Display JSON in alert (for demonstration purposes) // Display JSON in alert (for demonstration purposes)
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug("Saving data as segments:", saveData); console.debug('Saving data as segments:', saveData);
} }
// Mark as saved - no unsaved changes // Mark as saved - no unsaved changes
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
// Debug message // Debug message
logger.debug("All segments saved individually - reset unsaved changes flag"); logger.debug('All segments saved individually - reset unsaved changes flag');
// Save to history with special "save_segments" action to mark saved state // Save to history with special "save_segments" action to mark saved state
saveState("save_segments"); saveState('save_segments');
}; };
// Handle seeking with mobile check // Handle seeking with mobile check
@ -808,10 +801,8 @@ const useVideoTrimmer = () => {
// Check if device is mobile // Check if device is mobile
const isMobile = const isMobile =
typeof window !== "undefined" && typeof window !== 'undefined' &&
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent);
navigator.userAgent
);
// Add videoInitialized state // Add videoInitialized state
const [videoInitialized, setVideoInitialized] = useState(false); const [videoInitialized, setVideoInitialized] = useState(false);
@ -845,9 +836,9 @@ const useVideoTrimmer = () => {
// If video is somehow paused, ensure it keeps playing // If video is somehow paused, ensure it keeps playing
if (video.paused) { if (video.paused) {
logger.debug("Ensuring playback continues to next segment"); logger.debug('Ensuring playback continues to next segment');
video.play().catch((err) => { video.play().catch((err) => {
console.error("Error continuing segment playback:", err); console.error('Error continuing segment playback:', err);
}); });
} }
} else { } else {
@ -855,12 +846,12 @@ const useVideoTrimmer = () => {
video.pause(); video.pause();
setIsPlayingSegments(false); setIsPlayingSegments(false);
setCurrentSegmentIndex(0); setCurrentSegmentIndex(0);
video.removeEventListener("timeupdate", handleSegmentsPlayback); video.removeEventListener('timeupdate', handleSegmentsPlayback);
} }
} }
}; };
video.addEventListener("timeupdate", handleSegmentsPlayback); video.addEventListener('timeupdate', handleSegmentsPlayback);
// Start playing if not already playing // Start playing if not already playing
if (video.paused && orderedSegments.length > 0) { if (video.paused && orderedSegments.length > 0) {
@ -869,7 +860,7 @@ const useVideoTrimmer = () => {
} }
return () => { return () => {
video.removeEventListener("timeupdate", handleSegmentsPlayback); video.removeEventListener('timeupdate', handleSegmentsPlayback);
}; };
}, [isPlayingSegments, currentSegmentIndex, clipSegments]); }, [isPlayingSegments, currentSegmentIndex, clipSegments]);
@ -878,20 +869,15 @@ const useVideoTrimmer = () => {
const handleSegmentIndexUpdate = (event: CustomEvent) => { const handleSegmentIndexUpdate = (event: CustomEvent) => {
const { segmentIndex } = event.detail; const { segmentIndex } = event.detail;
if (isPlayingSegments && segmentIndex !== currentSegmentIndex) { if (isPlayingSegments && segmentIndex !== currentSegmentIndex) {
logger.debug( logger.debug(`Updating current segment index from ${currentSegmentIndex} to ${segmentIndex}`);
`Updating current segment index from ${currentSegmentIndex} to ${segmentIndex}`
);
setCurrentSegmentIndex(segmentIndex); setCurrentSegmentIndex(segmentIndex);
} }
}; };
document.addEventListener("update-segment-index", handleSegmentIndexUpdate as EventListener); document.addEventListener('update-segment-index', handleSegmentIndexUpdate as EventListener);
return () => { return () => {
document.removeEventListener( document.removeEventListener('update-segment-index', handleSegmentIndexUpdate as EventListener);
"update-segment-index",
handleSegmentIndexUpdate as EventListener
);
}; };
}, [isPlayingSegments, currentSegmentIndex]); }, [isPlayingSegments, currentSegmentIndex]);
@ -920,11 +906,11 @@ const useVideoTrimmer = () => {
// Start playback with proper error handling // Start playback with proper error handling
video.play().catch((err) => { video.play().catch((err) => {
console.error("Error starting segments playback:", err); console.error('Error starting segments playback:', err);
setIsPlayingSegments(false); setIsPlayingSegments(false);
}); });
logger.debug("Starting playback of all segments continuously"); logger.debug('Starting playback of all segments continuously');
} }
}; };
@ -960,7 +946,7 @@ const useVideoTrimmer = () => {
handleSaveSegments, handleSaveSegments,
isMobile, isMobile,
videoInitialized, videoInitialized,
setVideoInitialized setVideoInitialized,
}; };
}; };

View File

@ -7,7 +7,7 @@ const logger = {
* Logs debug messages only in development environment * Logs debug messages only in development environment
*/ */
debug: (...args: any[]) => { debug: (...args: any[]) => {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === 'development') {
console.debug(...args); console.debug(...args);
} }
}, },
@ -25,7 +25,7 @@ const logger = {
/** /**
* Always logs info messages * Always logs info messages
*/ */
info: (...args: any[]) => console.info(...args) info: (...args: any[]) => console.info(...args),
}; };
export default logger; export default logger;

View File

@ -1,4 +1,4 @@
import { QueryClient, QueryFunction } from "@tanstack/react-query"; import { QueryClient, QueryFunction } from '@tanstack/react-query';
async function throwIfResNotOk(res: Response) { async function throwIfResNotOk(res: Response) {
if (!res.ok) { if (!res.ok) {
@ -7,31 +7,27 @@ async function throwIfResNotOk(res: Response) {
} }
} }
export async function apiRequest( export async function apiRequest(method: string, url: string, data?: unknown | undefined): Promise<Response> {
method: string,
url: string,
data?: unknown | undefined
): Promise<Response> {
const res = await fetch(url, { const res = await fetch(url, {
method, method,
headers: data ? { "Content-Type": "application/json" } : {}, headers: data ? { 'Content-Type': 'application/json' } : {},
body: data ? JSON.stringify(data) : undefined, body: data ? JSON.stringify(data) : undefined,
credentials: "include" credentials: 'include',
}); });
await throwIfResNotOk(res); await throwIfResNotOk(res);
return res; return res;
} }
type UnauthorizedBehavior = "returnNull" | "throw"; type UnauthorizedBehavior = 'returnNull' | 'throw';
export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> = export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> =
({ on401: unauthorizedBehavior }) => ({ on401: unauthorizedBehavior }) =>
async ({ queryKey }) => { async ({ queryKey }) => {
const res = await fetch(queryKey[0] as string, { const res = await fetch(queryKey[0] as string, {
credentials: "include" credentials: 'include',
}); });
if (unauthorizedBehavior === "returnNull" && res.status === 401) { if (unauthorizedBehavior === 'returnNull' && res.status === 401) {
return null; return null;
} }
@ -42,14 +38,14 @@ export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryF
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
queryFn: getQueryFn({ on401: "throw" }), queryFn: getQueryFn({ on401: 'throw' }),
refetchInterval: false, refetchInterval: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
staleTime: Infinity, staleTime: Infinity,
retry: false retry: false,
}, },
mutations: { mutations: {
retry: false retry: false,
} },
} },
}); });

View File

@ -2,17 +2,17 @@
* Format seconds to HH:MM:SS.mmm format with millisecond precision * Format seconds to HH:MM:SS.mmm format with millisecond precision
*/ */
export const formatDetailedTime = (seconds: number): string => { export const formatDetailedTime = (seconds: number): string => {
if (isNaN(seconds)) return "00:00:00.000"; if (isNaN(seconds)) return '00:00:00.000';
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60); const remainingSeconds = Math.floor(seconds % 60);
const milliseconds = Math.round((seconds % 1) * 1000); const milliseconds = Math.round((seconds % 1) * 1000);
const formattedHours = String(hours).padStart(2, "0"); const formattedHours = String(hours).padStart(2, '0');
const formattedMinutes = String(minutes).padStart(2, "0"); const formattedMinutes = String(minutes).padStart(2, '0');
const formattedSeconds = String(remainingSeconds).padStart(2, "0"); const formattedSeconds = String(remainingSeconds).padStart(2, '0');
const formattedMilliseconds = String(milliseconds).padStart(3, "0"); const formattedMilliseconds = String(milliseconds).padStart(3, '0');
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`;
}; };

View File

@ -1,5 +1,5 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));

View File

@ -20,17 +20,14 @@ export const generateSolidColor = (time: number, duration: number): string => {
* Legacy function kept for compatibility * Legacy function kept for compatibility
* Now returns a data URL for a solid color square instead of a video thumbnail * Now returns a data URL for a solid color square instead of a video thumbnail
*/ */
export const generateThumbnail = async ( export const generateThumbnail = async (videoElement: HTMLVideoElement, time: number): Promise<string> => {
videoElement: HTMLVideoElement,
time: number
): Promise<string> => {
return new Promise((resolve) => { return new Promise((resolve) => {
// Create a small canvas for the solid color // Create a small canvas for the solid color
const canvas = document.createElement("canvas"); const canvas = document.createElement('canvas');
canvas.width = 10; // Much smaller - we only need a color canvas.width = 10; // Much smaller - we only need a color
canvas.height = 10; canvas.height = 10;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext('2d');
if (ctx) { if (ctx) {
// Get the solid color based on time // Get the solid color based on time
const color = generateSolidColor(time, videoElement.duration); const color = generateSolidColor(time, videoElement.duration);
@ -41,7 +38,7 @@ export const generateThumbnail = async (
} }
// Convert to data URL (much smaller now) // Convert to data URL (much smaller now)
const dataUrl = canvas.toDataURL("image/png", 0.5); const dataUrl = canvas.toDataURL('image/png', 0.5);
resolve(dataUrl); resolve(dataUrl);
}); });
}; };

View File

@ -22,16 +22,13 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// For now, we'll use a mock API that returns a promise // For now, we'll use a mock API that returns a promise
// This can be replaced with actual API calls later // This can be replaced with actual API calls later
export const trimVideo = async ( export const trimVideo = async (mediaId: string, data: TrimVideoRequest): Promise<TrimVideoResponse> => {
mediaId: string,
data: TrimVideoRequest
): Promise<TrimVideoResponse> => {
try { try {
// Attempt the real API call // Attempt the real API call
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, { const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
method: "POST", method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data),
}); });
if (!response.ok) { if (!response.ok) {
@ -43,17 +40,17 @@ export const trimVideo = async (
const errorData = await response.json(); const errorData = await response.json();
return { return {
status: 400, status: 400,
error: errorData.error || "An error occurred during processing", error: errorData.error || 'An error occurred during processing',
msg: "Video Processing Error", msg: 'Video Processing Error',
url_redirect: "" url_redirect: '',
}; };
} catch (parseError) { } catch (parseError) {
// If can't parse response JSON, return generic error // If can't parse response JSON, return generic error
return { return {
status: 400, status: 400,
error: "An error occurred during video processing", error: 'An error occurred during video processing',
msg: "Video Processing Error", msg: 'Video Processing Error',
url_redirect: "" url_redirect: '',
}; };
} }
} else if (response.status !== 404) { } else if (response.status !== 404) {
@ -63,17 +60,17 @@ export const trimVideo = async (
const errorData = await response.json(); const errorData = await response.json();
return { return {
status: response.status, status: response.status,
error: errorData.error || "An error occurred during processing", error: errorData.error || 'An error occurred during processing',
msg: "Video Processing Error", msg: 'Video Processing Error',
url_redirect: "" url_redirect: '',
}; };
} catch (parseError) { } catch (parseError) {
// If can't parse response JSON, return generic error // If can't parse response JSON, return generic error
return { return {
status: response.status, status: response.status,
error: "An error occurred during video processing", error: 'An error occurred during video processing',
msg: "Video Processing Error", msg: 'Video Processing Error',
url_redirect: "" url_redirect: '',
}; };
} }
} else { } else {
@ -81,8 +78,8 @@ export const trimVideo = async (
await delay(1500); // Simulate 1.5 second server delay await delay(1500); // Simulate 1.5 second server delay
return { return {
status: 200, // Mock success status status: 200, // Mock success status
msg: "Video Processed Successfully", // Updated per requirements msg: 'Video Processed Successfully', // Updated per requirements
url_redirect: `./view?m=${mediaId}` url_redirect: `./view?m=${mediaId}`,
}; };
} }
} }
@ -91,17 +88,17 @@ export const trimVideo = async (
const jsonResponse = await response.json(); const jsonResponse = await response.json();
return { return {
status: 200, status: 200,
msg: "Video Processed Successfully", // Ensure the success message is correct msg: 'Video Processed Successfully', // Ensure the success message is correct
url_redirect: jsonResponse.url_redirect || `./view?m=${mediaId}`, url_redirect: jsonResponse.url_redirect || `./view?m=${mediaId}`,
...jsonResponse ...jsonResponse,
}; };
} catch (error) { } catch (error) {
// For any fetch errors, return mock success response with delay // For any fetch errors, return mock success response with delay
await delay(1500); // Simulate 1.5 second server delay await delay(1500); // Simulate 1.5 second server delay
return { return {
status: 200, // Mock success status status: 200, // Mock success status
msg: "Video Processed Successfully", // Consistent with requirements msg: 'Video Processed Successfully', // Consistent with requirements
url_redirect: `./view?m=${mediaId}` url_redirect: `./view?m=${mediaId}`,
}; };
} }

View File

@ -3788,9 +3788,9 @@
} }
}, },
"node_modules/aproba": { "node_modules/aproba": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
"license": "ISC", "license": "ISC",
"optional": true "optional": true
}, },
@ -12590,9 +12590,9 @@
} }
}, },
"node_modules/nan": { "node_modules/nan": {
"version": "2.22.2", "version": "2.23.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },

File diff suppressed because it is too large Load Diff