style: Format entire codebase (video-editor) with Prettier

This commit is contained in:
Yiannis Christodoulou 2025-06-24 17:07:28 +03:00
parent e101a48c48
commit add6f6a704
25 changed files with 1076 additions and 961 deletions

View File

@ -41,7 +41,7 @@ const App = () => {
videoInitialized, videoInitialized,
setVideoInitialized, setVideoInitialized,
isPlayingSegments, isPlayingSegments,
handlePlaySegments, handlePlaySegments
} = useVideoTrimmer(); } = useVideoTrimmer();
// Function to play from the beginning // Function to play from the beginning
@ -90,7 +90,7 @@ const App = () => {
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
// First, check if we're inside a segment or exactly at its start/end // First, check if we're inside a segment or exactly at its start/end
currentSegment = sortedSegments.find(seg => { currentSegment = sortedSegments.find((seg) => {
const segStartTime = Number(seg.startTime.toFixed(6)); const segStartTime = Number(seg.startTime.toFixed(6));
const segEndTime = Number(seg.endTime.toFixed(6)); const segEndTime = Number(seg.endTime.toFixed(6));
@ -112,7 +112,7 @@ const App = () => {
// If we're not in a segment, find the next segment // If we're not in a segment, find the next segment
if (!currentSegment) { if (!currentSegment) {
nextSegment = sortedSegments.find(seg => { nextSegment = sortedSegments.find((seg) => {
const segStartTime = Number(seg.startTime.toFixed(6)); const segStartTime = Number(seg.startTime.toFixed(6));
return segStartTime > currentPosition; return segStartTime > currentPosition;
}); });
@ -165,30 +165,38 @@ const App = () => {
// Multiple attempts to ensure precision, with increasing delays // Multiple attempts to ensure precision, with increasing delays
setExactPosition(); setExactPosition();
setTimeout(setExactPosition, 5); // Quick first retry setTimeout(setExactPosition, 5); // Quick first retry
setTimeout(setExactPosition, 10); // Second retry setTimeout(setExactPosition, 10); // Second retry
setTimeout(setExactPosition, 20); // Third retry if needed setTimeout(setExactPosition, 20); // Third retry if needed
setTimeout(setExactPosition, 50); // Final verification setTimeout(setExactPosition, 50); // Final verification
// Remove our boundary checker // Remove our boundary checker
video.removeEventListener('timeupdate', checkBoundary); video.removeEventListener("timeupdate", checkBoundary);
setIsPlaying(false); setIsPlaying(false);
// Log the final position for debugging // Log the final position for debugging
logger.debug("Stopped at position:", { logger.debug("Stopped at position:", {
target: formatDetailedTime(stopTime), target: formatDetailedTime(stopTime),
actual: formatDetailedTime(video.currentTime), actual: formatDetailedTime(video.currentTime),
type: currentSegment ? "segment end" : (nextSegment ? "next segment start" : "end of video"), type: currentSegment
segment: currentSegment ? { ? "segment end"
id: currentSegment.id, : nextSegment
start: formatDetailedTime(currentSegment.startTime), ? "next segment start"
end: formatDetailedTime(currentSegment.endTime) : "end of video",
} : null, segment: currentSegment
nextSegment: nextSegment ? { ? {
id: nextSegment.id, id: currentSegment.id,
start: formatDetailedTime(nextSegment.startTime), start: formatDetailedTime(currentSegment.startTime),
end: formatDetailedTime(nextSegment.endTime) end: formatDetailedTime(currentSegment.endTime)
} : null }
: null,
nextSegment: nextSegment
? {
id: nextSegment.id,
start: formatDetailedTime(nextSegment.startTime),
end: formatDetailedTime(nextSegment.endTime)
}
: null
}); });
return; return;
@ -196,39 +204,41 @@ const App = () => {
}; };
// Start our boundary checker // Start our boundary checker
video.addEventListener('timeupdate', checkBoundary); video.addEventListener("timeupdate", checkBoundary);
// Start playing // Start playing
video.play() video
.play()
.then(() => { .then(() => {
setIsPlaying(true); setIsPlaying(true);
setVideoInitialized(true); setVideoInitialized(true);
logger.debug("Playback started:", { logger.debug("Playback started:", {
from: formatDetailedTime(currentPosition), from: formatDetailedTime(currentPosition),
to: formatDetailedTime(stopTime), to: formatDetailedTime(stopTime),
currentSegment: currentSegment ? { currentSegment: currentSegment
id: currentSegment.id, ? {
start: formatDetailedTime(currentSegment.startTime), id: currentSegment.id,
end: formatDetailedTime(currentSegment.endTime) start: formatDetailedTime(currentSegment.startTime),
} : 'None', end: formatDetailedTime(currentSegment.endTime)
nextSegment: nextSegment ? { }
id: nextSegment.id, : "None",
start: formatDetailedTime(nextSegment.startTime), nextSegment: nextSegment
end: formatDetailedTime(nextSegment.endTime) ? {
} : 'None' id: nextSegment.id,
start: formatDetailedTime(nextSegment.startTime),
end: formatDetailedTime(nextSegment.endTime)
}
: "None"
}); });
}) })
.catch(err => { .catch((err) => {
console.error("Error playing video:", err); console.error("Error playing video:", err);
}); });
}; };
return ( return (
<div className="bg-background min-h-screen"> <div className="bg-background min-h-screen">
<MobilePlayPrompt <MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />
videoRef={videoRef}
onPlay={handlePlay}
/>
<div className="container mx-auto px-4 py-6 max-w-6xl"> <div className="container mx-auto px-4 py-6 max-w-6xl">
{/* Video Player */} {/* Video Player */}

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,7 +20,7 @@ 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);
@ -38,19 +38,14 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
<h3 className="clip-segments-title">Clip Segments</h3> <h3 className="clip-segments-title">Clip Segments</h3>
{sortedSegments.map((segment, index) => ( {sortedSegments.map((segment, index) => (
<div <div key={segment.id} className={`segment-item ${getSegmentColorClass(index)}`}>
key={segment.id}
className={`segment-item ${getSegmentColorClass(index)}`}
>
<div className="segment-content"> <div className="segment-content">
<div <div
className="segment-thumbnail" className="segment-thumbnail"
style={{ backgroundImage: `url(${segment.thumbnail})` }} style={{ backgroundImage: `url(${segment.thumbnail})` }}
></div> ></div>
<div className="segment-info"> <div className="segment-info">
<div className="segment-title"> <div className="segment-title">Segment {index + 1}</div>
Segment {index + 1}
</div>
<div className="segment-time"> <div className="segment-time">
{formatTime(segment.startTime)} - {formatTime(segment.endTime)} {formatTime(segment.startTime)} - {formatTime(segment.endTime)}
</div> </div>
@ -67,7 +62,11 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
onClick={() => handleDeleteSegment(segment.id)} onClick={() => handleDeleteSegment(segment.id)}
> >
<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">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg> </svg>
</button> </button>
</div> </div>

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>;
@ -13,7 +13,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
useEffect(() => { useEffect(() => {
const checkIsMobile = () => { const checkIsMobile = () => {
// More comprehensive check for mobile/tablet devices // More comprehensive check for mobile/tablet devices
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent); return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(
navigator.userAgent
);
}; };
// Always show for mobile devices on each visit // Always show for mobile devices on each visit
@ -31,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]);
@ -63,10 +65,7 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
</ol> </ol>
</div> */} </div> */}
<button <button className="mobile-play-button" onClick={handlePlayClick}>
className="mobile-play-button"
onClick={handlePlayClick}
>
Click to start editing... Click to start editing...
</button> </button>
</div> </div>

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>;
@ -8,11 +8,7 @@ interface IOSVideoPlayerProps {
duration: number; duration: number;
} }
const IOSVideoPlayer = ({ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
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);
@ -30,8 +26,8 @@ const IOSVideoPlayer = ({
// 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);
} }
@ -115,12 +111,14 @@ const IOSVideoPlayer = ({
<div className="ios-video-player-container"> <div className="ios-video-player-container">
{/* Current Time / Duration Display */} {/* Current Time / Duration Display */}
<div className="ios-time-display mb-2"> <div className="ios-time-display mb-2">
<span className="text-sm">{formatTime(currentTime)} / {formatTime(duration)}</span> <span className="text-sm">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div> </div>
{/* iOS-optimized Video Element with Native Controls */} {/* iOS-optimized Video Element with Native Controls */}
<video <video
ref={ref => setIosVideoRef(ref)} ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md" className="w-full rounded-md"
src={videoUrl} src={videoUrl}
controls controls

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;
@ -9,31 +9,25 @@ interface ModalProps {
actions?: React.ReactNode; actions?: React.ReactNode;
} }
const Modal: React.FC<ModalProps> = ({ const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions }) => {
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]);
@ -48,14 +42,10 @@ const Modal: React.FC<ModalProps> = ({
return ( return (
<div className="modal-overlay" onClick={handleClickOutside}> <div className="modal-overlay" onClick={handleClickOutside}>
<div className="modal-container" onClick={e => e.stopPropagation()}> <div className="modal-container" onClick={(e) => e.stopPropagation()}>
<div className="modal-header"> <div className="modal-header">
<h2 className="modal-title">{title}</h2> <h2 className="modal-title">{title}</h2>
<button <button className="modal-close-button" onClick={onClose} aria-label="Close modal">
className="modal-close-button"
onClick={onClose}
aria-label="Close modal"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
@ -73,15 +63,9 @@ const Modal: React.FC<ModalProps> = ({
</button> </button>
</div> </div>
<div className="modal-content"> <div className="modal-content">{children}</div>
{children}
</div>
{actions && ( {actions && <div className="modal-actions">{actions}</div>}
<div className="modal-actions">
{actions}
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -2878,8 +2878,8 @@ const TimelineControls = ({
isPlayingSegments isPlayingSegments
? "Disabled during preview" ? "Disabled during preview"
: isPlaying : isPlaying
? "Pause playback" ? "Pause playback"
: "Play from current position" : "Play from current position"
} }
style={{ style={{
userSelect: "none", userSelect: "none",
@ -3142,8 +3142,8 @@ const TimelineControls = ({
isPlayingSegments isPlayingSegments
? "Disabled during preview" ? "Disabled during preview"
: availableSegmentDuration < 0.5 : availableSegmentDuration < 0.5
? "Not enough space for new segment" ? "Not enough space for new segment"
: "Create new segment" : "Create new segment"
} }
disabled={availableSegmentDuration < 0.5 || isPlayingSegments} disabled={availableSegmentDuration < 0.5 || isPlayingSegments}
onClick={async (e) => { onClick={async (e) => {
@ -3735,8 +3735,8 @@ const TimelineControls = ({
isPlayingSegments isPlayingSegments
? "Disabled during preview" ? "Disabled during preview"
: isPlaying : isPlaying
? "Pause playback" ? "Pause playback"
: "Play from here until next segment" : "Play from here until next segment"
} }
style={{ style={{
userSelect: "none", userSelect: "none",

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>;
@ -33,8 +33,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 }); const [tooltipPosition, setTooltipPosition] = useState({ x: 0 });
const [tooltipTime, setTooltipTime] = useState(0); const [tooltipTime, setTooltipTime] = useState(0);
const sampleVideoUrl = typeof window !== 'undefined' && const sampleVideoUrl =
(window as any).MEDIA_DATA?.videoUrl || (typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) ||
"/videos/sample-video-10m.mp4"; "/videos/sample-video-10m.mp4";
// Detect iOS device // Detect iOS device
@ -47,8 +47,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 +57,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 +70,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 +86,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 +150,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
@ -174,7 +174,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;
} }
@ -202,14 +202,14 @@ 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, { passive: false });
document.addEventListener('touchend', handleTouchEnd); document.addEventListener("touchend", handleTouchEnd);
document.addEventListener('touchcancel', handleTouchEnd); document.addEventListener("touchcancel", handleTouchEnd);
}; };
// Handle touch dragging on progress bar // Handle touch dragging on progress bar
@ -217,7 +217,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
@ -234,7 +234,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;
} }
@ -255,7 +255,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;
} }
@ -292,24 +292,29 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
setTimeout(() => { setTimeout(() => {
if (videoRef.current) { if (videoRef.current) {
// Try to play with proper promise handling // Try to play with proper promise handling
videoRef.current.play() videoRef.current
.play()
.then(() => { .then(() => {
logger.debug("iOS: Play started successfully at position:", videoRef.current?.currentTime); logger.debug(
"iOS: Play started successfully at position:",
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);
} else { } else {
// Normal play (non-iOS or no remembered position) // Normal play (non-iOS or no remembered position)
video.play() video
.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);
}); });
} }
@ -340,14 +345,12 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */} {/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}
{isIOS && !hasInitialized && !isPlaying && ( {isIOS && !hasInitialized && !isPlaying && (
<div className="ios-first-play-indicator"> <div className="ios-first-play-indicator">
<div className="ios-play-message"> <div className="ios-play-message">Tap Play to initialize video controls</div>
Tap Play to initialize video controls
</div>
</div> </div>
)} )}
{/* 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">
@ -360,26 +363,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 <div className="video-progress-fill" style={{ width: `${progressPercentage}%` }}></div>
className="video-progress-fill" <div className="video-scrubber" style={{ left: `${progressPercentage}%` }}></div>
style={{ width: `${progressPercentage}%` }}
></div>
<div
className="video-scrubber"
style={{ left: `${progressPercentage}%` }}
></div>
{/* Floating time tooltip when dragging */} {/* Floating time tooltip when dragging */}
{isDraggingProgress && ( {isDraggingProgress && (
<div className="video-time-tooltip" style={{ <div
left: `${tooltipPosition.x}px`, className="video-time-tooltip"
transform: 'translateX(-50%)' style={{
}}> left: `${tooltipPosition.x}px`,
transform: "translateX(-50%)"
}}
>
{formatDetailedTime(tooltipTime)} {formatDetailedTime(tooltipTime)}
</div> </div>
)} )}
@ -396,7 +396,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
data-tooltip={isMuted ? "Unmute" : "Mute"} data-tooltip={isMuted ? "Unmute" : "Mute"}
> >
{isMuted ? ( {isMuted ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="1" y1="1" x2="23" y2="23"></line> <line x1="1" y1="1" x2="23" y2="23"></line>
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path> <path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path> <path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
@ -404,7 +412,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<line x1="8" y1="23" x2="16" y2="23"></line> <line x1="8" y1="23" x2="16" y2="23"></line>
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg> </svg>
@ -420,7 +436,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
data-tooltip="Toggle fullscreen" data-tooltip="Toggle fullscreen"
> >
<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">
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg> </svg>
</button> </button>
</div> </div>

View File

@ -46,18 +46,21 @@ 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(`History state updated: ${history.length} entries, position: ${historyPosition}`); console.debug(
// Log actions in history to help debug undo/redo `History state updated: ${history.length} entries, position: ${historyPosition}`
const actions = history.map((state, idx) =>
`${idx}: ${state.action || 'unknown'} (segments: ${state.clipSegments.length})`
); );
console.debug('History actions:', actions); // Log actions in history to help debug undo/redo
const actions = history.map(
(state, idx) =>
`${idx}: ${state.action || "unknown"} (segments: ${state.clipSegments.length})`
);
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);
} }
} }
@ -69,7 +72,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
@ -77,11 +80,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]);
@ -156,19 +159,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);
}; };
}, []); }, []);
@ -181,7 +184,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) {
@ -193,15 +196,16 @@ const useVideoTrimmer = () => {
video.currentTime = trimStart; video.currentTime = trimStart;
} }
video.play() video
.play()
.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
}); });
@ -222,18 +226,19 @@ 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;
} }
// Resume playback if it was playing before // Resume playback if it was playing before
if (wasPlaying) { if (wasPlaying) {
// Play immediately without delay // Play immediately without delay
video.play() video
.play()
.then(() => { .then(() => {
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);
}); });
@ -249,7 +254,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
@ -269,8 +274,10 @@ const useVideoTrimmer = () => {
if (!oldSeg || !newSeg) return true; if (!oldSeg || !newSeg) return true;
// Check if any time values changed by more than 0.001 seconds (1ms) // Check if any time values changed by more than 0.001 seconds (1ms)
if (Math.abs(oldSeg.startTime - newSeg.startTime) > 0.001 || if (
Math.abs(oldSeg.endTime - newSeg.endTime) > 0.001) { Math.abs(oldSeg.startTime - newSeg.startTime) > 0.001 ||
Math.abs(oldSeg.endTime - newSeg.endTime) > 0.001
) {
return true; return true;
} }
} }
@ -278,7 +285,8 @@ const useVideoTrimmer = () => {
return false; // No significant changes found return false; // No significant changes found
}; };
const isSignificantChange = !lastState || const isSignificantChange =
!lastState ||
lastState.trimStart !== newState.trimStart || lastState.trimStart !== newState.trimStart ||
lastState.trimEnd !== newState.trimEnd || lastState.trimEnd !== newState.trimEnd ||
lastState.splitPoints.length !== newState.splitPoints.length || lastState.splitPoints.length !== newState.splitPoints.length ||
@ -293,7 +301,7 @@ const useVideoTrimmer = () => {
const currentPosition = historyPosition; const currentPosition = historyPosition;
// Use functional updates to ensure we're working with the latest state // Use functional updates to ensure we're working with the latest state
setHistory(prevHistory => { setHistory((prevHistory) => {
// If we're not at the end of history, truncate // If we're not at the end of history, truncate
if (currentPosition < prevHistory.length - 1) { if (currentPosition < prevHistory.length - 1) {
const newHistory = prevHistory.slice(0, currentPosition + 1); const newHistory = prevHistory.slice(0, currentPosition + 1);
@ -305,7 +313,7 @@ const useVideoTrimmer = () => {
}); });
// Update position using functional update // Update position using functional update
setHistoryPosition(prev => { setHistoryPosition((prev) => {
const newPosition = prev + 1; const newPosition = prev + 1;
// "Saved state to history position", newPosition) // "Saved state to history position", newPosition)
return newPosition; return newPosition;
@ -331,16 +339,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);
}; };
}, []); }, []);
@ -352,10 +360,12 @@ 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(`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}`); logger.debug(
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}`
);
// Update segment state immediately for UI feedback // Update segment state immediately for UI feedback
setClipSegments(e.detail.segments); setClipSegments(e.detail.segments);
@ -381,7 +391,7 @@ const useVideoTrimmer = () => {
const currentHistoryPosition = historyPosition; const currentHistoryPosition = historyPosition;
// Update history with the functional pattern to avoid stale closure issues // Update history with the functional pattern to avoid stale closure issues
setHistory(prevHistory => { setHistory((prevHistory) => {
// If we're not at the end of the history, truncate // If we're not at the end of the history, truncate
if (currentHistoryPosition < prevHistory.length - 1) { if (currentHistoryPosition < prevHistory.length - 1) {
const newHistory = prevHistory.slice(0, currentHistoryPosition + 1); const newHistory = prevHistory.slice(0, currentHistoryPosition + 1);
@ -393,24 +403,29 @@ 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(`Saved state with action: ${actionType} to history position ${newPosition}`); logger.debug(
`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(`Skipped saving state to history for action: ${actionType} (recordHistory=false)`); logger.debug(
`Skipped saving state to history for action: ${actionType} (recordHistory=false)`
);
} }
} }
}; };
const handleSplitSegment = async (e: Event) => { const handleSplitSegment = async (e: Event) => {
const customEvent = e as CustomEvent; const customEvent = e as CustomEvent;
if (customEvent.detail && if (
typeof customEvent.detail.time === 'number' && customEvent.detail &&
typeof customEvent.detail.segmentId === 'number') { typeof customEvent.detail.time === "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;
const segmentId = customEvent.detail.segmentId; const segmentId = customEvent.detail.segmentId;
@ -419,7 +434,7 @@ const useVideoTrimmer = () => {
seekVideo(timeToSplit); seekVideo(timeToSplit);
// Find the segment to split // Find the segment to split
const segmentToSplit = clipSegments.find(seg => seg.id === segmentId); const segmentToSplit = clipSegments.find((seg) => seg.id === segmentId);
if (!segmentToSplit) return; if (!segmentToSplit) return;
// Make sure the split point is within the segment // Make sure the split point is within the segment
@ -431,7 +446,7 @@ const useVideoTrimmer = () => {
const newSegments = [...clipSegments]; const newSegments = [...clipSegments];
// Remove the original segment // Remove the original segment
const segmentIndex = newSegments.findIndex(seg => seg.id === segmentId); const segmentIndex = newSegments.findIndex((seg) => seg.id === segmentId);
if (segmentIndex === -1) return; if (segmentIndex === -1) return;
newSegments.splice(segmentIndex, 1); newSegments.splice(segmentIndex, 1);
@ -442,7 +457,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
@ -451,7 +466,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
@ -462,18 +477,18 @@ 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
const newSegments = clipSegments.filter(segment => segment.id !== segmentId); const newSegments = clipSegments.filter((segment) => segment.id !== segmentId);
if (newSegments.length !== clipSegments.length) { if (newSegments.length !== clipSegments.length) {
// If all segments are deleted, create a new full video segment // If all segments are deleted, create a new full video segment
@ -485,7 +500,7 @@ const useVideoTrimmer = () => {
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
@ -497,32 +512,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
@ -548,7 +563,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;
@ -556,7 +571,7 @@ const useVideoTrimmer = () => {
} }
setClipSegments(newSegments); setClipSegments(newSegments);
saveState('create_split_points'); saveState("create_split_points");
} }
}; };
@ -575,23 +590,29 @@ const useVideoTrimmer = () => {
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
const handleUndo = () => { const handleUndo = () => {
if (historyPosition > 0) { if (historyPosition > 0) {
const previousState = history[historyPosition - 1]; const previousState = history[historyPosition - 1];
logger.debug(`** UNDO ** to position ${historyPosition - 1}, action: ${previousState.action}, segments: ${previousState.clipSegments.length}`); logger.debug(
`** UNDO ** to position ${historyPosition - 1}, action: ${previousState.action}, segments: ${previousState.clipSegments.length}`
);
// Log segment details to help debug // Log segment details to help debug
logger.debug("Segment details after undo:", previousState.clipSegments.map(seg => logger.debug(
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}` "Segment details after undo:",
)); previousState.clipSegments.map(
(seg) =>
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
)
);
// Apply the previous state with deep cloning to avoid reference issues // Apply the previous state with deep cloning to avoid reference issues
setTrimStart(previousState.trimStart); setTrimStart(previousState.trimStart);
@ -608,12 +629,18 @@ const useVideoTrimmer = () => {
const handleRedo = () => { const handleRedo = () => {
if (historyPosition < history.length - 1) { if (historyPosition < history.length - 1) {
const nextState = history[historyPosition + 1]; const nextState = history[historyPosition + 1];
logger.debug(`** REDO ** to position ${historyPosition + 1}, action: ${nextState.action}, segments: ${nextState.clipSegments.length}`); logger.debug(
`** REDO ** to position ${historyPosition + 1}, action: ${nextState.action}, segments: ${nextState.clipSegments.length}`
);
// Log segment details to help debug // Log segment details to help debug
logger.debug("Segment details after redo:", nextState.clipSegments.map(seg => logger.debug(
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}` "Segment details after redo:",
)); nextState.clipSegments.map(
(seg) =>
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
)
);
// Apply the next state with deep cloning to avoid reference issues // Apply the next state with deep cloning to avoid reference issues
setTrimStart(nextState.trimStart); setTrimStart(nextState.trimStart);
@ -642,7 +669,7 @@ 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);
@ -651,15 +678,16 @@ const useVideoTrimmer = () => {
} }
// Play the video from current position with proper promise handling // Play the video from current position with proper promise handling
video.play() video
.play()
.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
}); });
@ -683,14 +711,14 @@ 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);
} }
@ -698,12 +726,12 @@ const useVideoTrimmer = () => {
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);
@ -717,14 +745,14 @@ 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);
} }
@ -732,12 +760,12 @@ const useVideoTrimmer = () => {
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
@ -748,7 +776,7 @@ 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)
@ -756,7 +784,7 @@ const useVideoTrimmer = () => {
}; };
// 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);
} }
@ -767,7 +795,7 @@ const useVideoTrimmer = () => {
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
@ -779,7 +807,11 @@ const useVideoTrimmer = () => {
}; };
// Check if device is mobile // Check if device is mobile
const isMobile = typeof window !== 'undefined' && /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent); const isMobile =
typeof window !== "undefined" &&
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(
navigator.userAgent
);
// Add videoInitialized state // Add videoInitialized state
const [videoInitialized, setVideoInitialized] = useState(false); const [videoInitialized, setVideoInitialized] = useState(false);
@ -814,7 +846,7 @@ 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);
}); });
} }
@ -823,12 +855,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) {
@ -837,7 +869,7 @@ const useVideoTrimmer = () => {
} }
return () => { return () => {
video.removeEventListener('timeupdate', handleSegmentsPlayback); video.removeEventListener("timeupdate", handleSegmentsPlayback);
}; };
}, [isPlayingSegments, currentSegmentIndex, clipSegments]); }, [isPlayingSegments, currentSegmentIndex, clipSegments]);
@ -846,15 +878,20 @@ 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(`Updating current segment index from ${currentSegmentIndex} to ${segmentIndex}`); logger.debug(
`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('update-segment-index', handleSegmentIndexUpdate as EventListener); document.removeEventListener(
"update-segment-index",
handleSegmentIndexUpdate as EventListener
);
}; };
}, [isPlayingSegments, currentSegmentIndex]); }, [isPlayingSegments, currentSegmentIndex]);
@ -882,7 +919,7 @@ const useVideoTrimmer = () => {
video.currentTime = orderedSegments[0].startTime; video.currentTime = orderedSegments[0].startTime;
// 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);
}); });
@ -923,7 +960,7 @@ const useVideoTrimmer = () => {
handleSaveSegments, handleSaveSegments,
isMobile, isMobile,
videoInitialized, videoInitialized,
setVideoInitialized, setVideoInitialized
}; };
}; };

View File

@ -125,13 +125,13 @@
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
background-color: #EEE; /* Very light gray background */ background-color: #eee; /* Very light gray background */
position: relative; position: relative;
} }
.timeline-container { .timeline-container {
position: relative; position: relative;
background-color: #EEE; /* Very light gray background */ background-color: #eee; /* Very light gray background */
height: 6rem; height: 6rem;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
@ -208,17 +208,27 @@
overflow: hidden; overflow: hidden;
cursor: grab; cursor: grab;
user-select: none; user-select: none;
transition: box-shadow 0.2s, transform 0.1s; transition:
box-shadow 0.2s,
transform 0.1s;
/* Original z-index for stacking order based on segment ID */ /* Original z-index for stacking order based on segment ID */
z-index: 15; z-index: 15;
} }
/* No background colors for segments, just borders with 2-color scheme */ /* No background colors for segments, just borders with 2-color scheme */
.clip-segment:nth-child(odd), .segment-color-1, .segment-color-3, .segment-color-5, .segment-color-7 { .clip-segment:nth-child(odd),
.segment-color-1,
.segment-color-3,
.segment-color-5,
.segment-color-7 {
background-color: transparent; background-color: transparent;
border: 2px solid rgba(0, 123, 255, 0.9); /* Blue border */ border: 2px solid rgba(0, 123, 255, 0.9); /* Blue border */
} }
.clip-segment:nth-child(even), .segment-color-2, .segment-color-4, .segment-color-6, .segment-color-8 { .clip-segment:nth-child(even),
.segment-color-2,
.segment-color-4,
.segment-color-6,
.segment-color-8 {
background-color: transparent; background-color: transparent;
border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */ border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */
} }
@ -315,7 +325,7 @@
input[type="range"] { input[type="range"] {
-webkit-appearance: none; -webkit-appearance: none;
height: 6px; height: 6px;
background: #E0E0E0; background: #e0e0e0;
border-radius: 3px; border-radius: 3px;
} }
@ -350,12 +360,14 @@ input[type="range"]::-webkit-slider-thumb {
z-index: 1000; z-index: 1000;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]::after { [data-tooltip]::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
@ -366,7 +378,9 @@ input[type="range"]::-webkit-slider-thumb {
margin-bottom: 0px; margin-bottom: 0px;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }
@ -464,7 +478,7 @@ button[disabled][data-tooltip]::after {
} }
.segment-tooltip::after { .segment-tooltip::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -6px; bottom: -6px;
left: 50%; left: 50%;
@ -539,7 +553,7 @@ button[disabled][data-tooltip]::after {
} }
.empty-space-tooltip::after { .empty-space-tooltip::after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -8px; bottom: -8px;
left: 50%; left: 50%;
@ -617,7 +631,9 @@ button[disabled][data-tooltip]::after {
} }
/* Save buttons styling */ /* Save buttons styling */
.save-button, .save-copy-button, .save-segments-button { .save-button,
.save-copy-button,
.save-segments-button {
background-color: rgba(0, 123, 255, 0.8); background-color: rgba(0, 123, 255, 0.8);
color: white; color: white;
border: none; border: none;
@ -628,7 +644,8 @@ button[disabled][data-tooltip]::after {
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.save-button:hover, .save-copy-button:hover { .save-button:hover,
.save-copy-button:hover {
background-color: rgba(0, 123, 255, 1); background-color: rgba(0, 123, 255, 1);
} }
@ -735,7 +752,8 @@ button[disabled][data-tooltip]::after {
font-size: 1.1rem; font-size: 1.1rem;
} }
.current-time, .duration-time { .current-time,
.duration-time {
white-space: nowrap; white-space: nowrap;
} }
@ -770,7 +788,8 @@ button[disabled][data-tooltip]::after {
gap: 8px; gap: 8px;
} }
.save-button, .save-copy-button { .save-button,
.save-copy-button {
margin-top: 8px; margin-top: 8px;
width: 100%; width: 100%;
} }

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);
} }
}, },

View File

@ -10,13 +10,13 @@ async function throwIfResNotOk(res: Response) {
export async function apiRequest( export async function apiRequest(
method: string, method: string,
url: string, url: string,
data?: unknown | undefined, data?: unknown | undefined
): Promise<Response> { ): 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);
@ -24,13 +24,11 @@ export async function apiRequest(
} }
type UnauthorizedBehavior = "returnNull" | "throw"; type UnauthorizedBehavior = "returnNull" | "throw";
export const getQueryFn: <T>(options: { export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> =
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) {
@ -48,10 +46,10 @@ export const queryClient = new QueryClient({
refetchInterval: false, refetchInterval: false,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
staleTime: Infinity, staleTime: Infinity,
retry: false, retry: false
}, },
mutations: { mutations: {
retry: false, retry: false
}, }
}, }
}); });

View File

@ -1,6 +1,6 @@
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

@ -2,10 +2,7 @@
* Generate a solid color background for a segment * Generate a solid color background for a segment
* Returns a CSS color based on the segment position * Returns a CSS color based on the segment position
*/ */
export const generateSolidColor = ( export const generateSolidColor = (time: number, duration: number): string => {
time: number,
duration: number
): string => {
// Use the time position to create different colors // Use the time position to create different colors
// This gives each segment a different color without needing an image // This gives each segment a different color without needing an image
const position = Math.min(Math.max(time / (duration || 1), 0), 1); const position = Math.min(Math.max(time / (duration || 1), 0), 1);
@ -29,11 +26,11 @@ export const generateThumbnail = async (
): Promise<string> => { ): 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);
@ -44,7 +41,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

@ -2,7 +2,7 @@ import { createRoot } from "react-dom/client";
import App from "./App"; import App from "./App";
import "./index.css"; import "./index.css";
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
window.MEDIA_DATA = { window.MEDIA_DATA = {
videoUrl: "", videoUrl: "",
mediaId: "" mediaId: ""
@ -30,8 +30,8 @@ const mountComponents = () => {
} }
}; };
if (document.readyState === 'loading') { if (document.readyState === "loading") {
document.addEventListener('DOMContentLoaded', mountComponents); document.addEventListener("DOMContentLoaded", mountComponents);
} else { } else {
mountComponents(); mountComponents();
} }

View File

@ -13,12 +13,12 @@ interface TrimVideoRequest {
interface TrimVideoResponse { interface TrimVideoResponse {
msg: string; msg: string;
url_redirect: string; url_redirect: string;
status?: number; // HTTP status code for success/error status?: number; // HTTP status code for success/error
error?: string; // Error message if status is not 200 error?: string; // Error message if status is not 200
} }
// 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 // 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
@ -29,8 +29,8 @@ export const trimVideo = async (
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)
}); });

View File

@ -21,13 +21,15 @@
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]:after { [data-tooltip]:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
@ -37,7 +39,9 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent; border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }
@ -143,7 +147,9 @@
border-radius: 9999px; border-radius: 9999px;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s, color 0.2s; transition:
background-color 0.2s,
color 0.2s;
min-width: auto; min-width: auto;
&:hover { &:hover {
@ -163,12 +169,28 @@
color: rgba(51, 51, 51, 0.7); color: rgba(51, 51, 51, 0.7);
} }
.segment-color-1 { background-color: rgba(59, 130, 246, 0.15); } .segment-color-1 {
.segment-color-2 { background-color: rgba(16, 185, 129, 0.15); } background-color: rgba(59, 130, 246, 0.15);
.segment-color-3 { background-color: rgba(245, 158, 11, 0.15); } }
.segment-color-4 { background-color: rgba(239, 68, 68, 0.15); } .segment-color-2 {
.segment-color-5 { background-color: rgba(139, 92, 246, 0.15); } background-color: rgba(16, 185, 129, 0.15);
.segment-color-6 { background-color: rgba(236, 72, 153, 0.15); } }
.segment-color-7 { background-color: rgba(6, 182, 212, 0.15); } .segment-color-3 {
.segment-color-8 { background-color: rgba(250, 204, 21, 0.15); } background-color: rgba(245, 158, 11, 0.15);
}
.segment-color-4 {
background-color: rgba(239, 68, 68, 0.15);
}
.segment-color-5 {
background-color: rgba(139, 92, 246, 0.15);
}
.segment-color-6 {
background-color: rgba(236, 72, 153, 0.15);
}
.segment-color-7 {
background-color: rgba(6, 182, 212, 0.15);
}
.segment-color-8 {
background-color: rgba(250, 204, 21, 0.15);
}
} }

View File

@ -21,13 +21,15 @@
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]:after { [data-tooltip]:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
@ -37,7 +39,9 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent; border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }

View File

@ -76,11 +76,11 @@
/* Prevent text selection on buttons */ /* Prevent text selection on buttons */
.no-select { .no-select {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */ -webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */ -khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */ -moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, supported by Chrome and Opera */ user-select: none; /* Non-prefixed version, supported by Chrome and Opera */
cursor: default; cursor: default;
} }

View File

@ -1,302 +1,306 @@
#video-editor-trim-root { #video-editor-trim-root {
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 1000; z-index: 1000;
}
.modal-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: modal-fade-in 0.3s ease-out;
}
@keyframes modal-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
} }
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #333;
}
.modal-close-button {
background: none;
border: none;
cursor: pointer;
color: #666;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.modal-close-button:hover {
color: #000;
}
.modal-content {
padding: 20px;
color: #333;
font-size: 1rem;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
}
.modal-actions {
display: flex;
justify-content: flex-end;
padding: 16px 20px;
border-top: 1px solid #eee;
gap: 12px;
}
.modal-button {
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.modal-button-primary {
background-color: #0066cc;
color: white;
}
.modal-button-primary:hover {
background-color: #0055aa;
}
.modal-button-secondary {
background-color: #f0f0f0;
color: #333;
}
.modal-button-secondary:hover {
background-color: #e0e0e0;
}
.modal-button-danger {
background-color: #dc3545;
color: white;
}
.modal-button-danger:hover {
background-color: #bd2130;
}
/* Modal content styles */
.modal-message {
margin-bottom: 16px;
font-size: 1rem;
}
.text-center {
text-align: center;
}
.modal-spinner {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #0066cc;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.modal-success-icon {
display: flex;
justify-content: center;
margin-bottom: 16px;
color: #28a745;
font-size: 2rem;
}
.modal-success-icon svg {
width: 60px;
height: 60px;
color: #4CAF50;
animation: success-pop 0.5s ease-out;
}
@keyframes success-pop {
0% {
transform: scale(0);
opacity: 0;
}
70% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.modal-error-icon {
display: flex;
justify-content: center;
margin-bottom: 16px;
color: #dc3545;
font-size: 2rem;
}
.modal-error-icon svg {
width: 60px;
height: 60px;
color: #F44336;
animation: error-pop 0.5s ease-out;
}
@keyframes error-pop {
0% {
transform: scale(0);
opacity: 0;
}
70% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.modal-choices {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
}
.modal-choice-button {
padding: 12px 16px;
border: none;
border-radius: 4px;
background-color: #0066cc;
text-align: center;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
text-decoration: none;
color: white;
}
.modal-choice-button:hover {
background-color: #0055aa;
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.modal-choice-button svg {
margin-right: 8px;
}
.success-link {
background-color: #4CAF50;
}
.success-link:hover {
background-color: #3d8b40;
}
.centered-choice {
margin: 0 auto;
width: auto;
min-width: 220px;
background-color: #0066cc;
color: white;
}
.centered-choice:hover {
background-color: #0055aa;
}
@media (max-width: 480px) {
.modal-container { .modal-container {
width: 95%; background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: modal-fade-in 0.3s ease-out;
}
@keyframes modal-fade-in {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #333;
}
.modal-close-button {
background: none;
border: none;
cursor: pointer;
color: #666;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s;
}
.modal-close-button:hover {
color: #000;
}
.modal-content {
padding: 20px;
color: #333;
font-size: 1rem;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
} }
.modal-actions { .modal-actions {
flex-direction: column; display: flex;
justify-content: flex-end;
padding: 16px 20px;
border-top: 1px solid #eee;
gap: 12px;
} }
.modal-button { .modal-button {
width: 100%; padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.modal-button-primary {
background-color: #0066cc;
color: white;
}
.modal-button-primary:hover {
background-color: #0055aa;
}
.modal-button-secondary {
background-color: #f0f0f0;
color: #333;
}
.modal-button-secondary:hover {
background-color: #e0e0e0;
}
.modal-button-danger {
background-color: #dc3545;
color: white;
}
.modal-button-danger:hover {
background-color: #bd2130;
}
/* Modal content styles */
.modal-message {
margin-bottom: 16px;
font-size: 1rem;
}
.text-center {
text-align: center;
}
.modal-spinner {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #0066cc;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.modal-success-icon {
display: flex;
justify-content: center;
margin-bottom: 16px;
color: #28a745;
font-size: 2rem;
}
.modal-success-icon svg {
width: 60px;
height: 60px;
color: #4caf50;
animation: success-pop 0.5s ease-out;
}
@keyframes success-pop {
0% {
transform: scale(0);
opacity: 0;
}
70% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.modal-error-icon {
display: flex;
justify-content: center;
margin-bottom: 16px;
color: #dc3545;
font-size: 2rem;
}
.modal-error-icon svg {
width: 60px;
height: 60px;
color: #f44336;
animation: error-pop 0.5s ease-out;
}
@keyframes error-pop {
0% {
transform: scale(0);
opacity: 0;
}
70% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.modal-choices {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 20px;
}
.modal-choice-button {
padding: 12px 16px;
border: none;
border-radius: 4px;
background-color: #0066cc;
text-align: center;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-weight: 500;
text-decoration: none;
color: white;
}
.modal-choice-button:hover {
background-color: #0055aa;
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.modal-choice-button svg {
margin-right: 8px;
}
.success-link {
background-color: #4caf50;
}
.success-link:hover {
background-color: #3d8b40;
}
.centered-choice {
margin: 0 auto;
width: auto;
min-width: 220px;
background-color: #0066cc;
color: white;
}
.centered-choice:hover {
background-color: #0055aa;
}
@media (max-width: 480px) {
.modal-container {
width: 95%;
}
.modal-actions {
flex-direction: column;
}
.modal-button {
width: 100%;
}
}
.error-message {
color: #f44336;
font-weight: 500;
background-color: rgba(244, 67, 54, 0.1);
padding: 10px;
border-radius: 4px;
border-left: 4px solid #f44336;
margin-top: 10px;
}
.redirect-message {
margin-top: 20px;
color: #555;
font-size: 0.95rem;
padding: 0;
margin: 0;
}
.countdown {
font-weight: bold;
color: #0066cc;
font-size: 1.1rem;
} }
} }
.error-message {
color: #F44336;
font-weight: 500;
background-color: rgba(244, 67, 54, 0.1);
padding: 10px;
border-radius: 4px;
border-left: 4px solid #F44336;
margin-top: 10px;
}
.redirect-message {
margin-top: 20px;
color: #555;
font-size: 0.95rem;
padding: 0;
margin: 0;
}
.countdown {
font-weight: bold;
color: #0066cc;
font-size: 1.1rem;
}
}

View File

@ -257,7 +257,7 @@
} }
.clip-segment-handle:after { .clip-segment-handle:after {
content: ''; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -321,7 +321,7 @@
.segment-tooltip:after, .segment-tooltip:after,
.empty-space-tooltip:after { .empty-space-tooltip:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -5px; bottom: -5px;
left: 50%; left: 50%;
@ -335,7 +335,7 @@
.segment-tooltip:before, .segment-tooltip:before,
.empty-space-tooltip:before { .empty-space-tooltip:before {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -6px; bottom: -6px;
left: 50%; left: 50%;
@ -612,13 +612,15 @@
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]:after { [data-tooltip]:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
@ -628,7 +630,9 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent; border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }

View File

@ -111,7 +111,9 @@
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 2500; /* High z-index */ z-index: 2500; /* High z-index */
pointer-events: none; pointer-events: none;
} }
@ -130,7 +132,9 @@
margin-left: 0; /* Reset margin */ margin-left: 0; /* Reset margin */
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 2500; /* High z-index */ z-index: 2500; /* High z-index */
pointer-events: none; pointer-events: none;
} }

View File

@ -21,13 +21,15 @@
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]:after { [data-tooltip]:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
@ -37,7 +39,9 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent; border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }
@ -112,7 +116,7 @@
} }
.play-pause-indicator::before { .play-pause-indicator::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
@ -160,9 +164,18 @@
} }
@keyframes pulse { @keyframes pulse {
0% { opacity: 0.7; transform: scale(1); } 0% {
50% { opacity: 1; transform: scale(1.05); } opacity: 0.7;
100% { opacity: 0.7; transform: scale(1); } transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
100% {
opacity: 0.7;
transform: scale(1);
}
} }
.video-controls { .video-controls {
@ -232,7 +245,10 @@
background-color: #ff0000; background-color: #ff0000;
border-radius: 50%; border-radius: 50%;
cursor: grab; cursor: grab;
transition: transform 0.1s ease, width 0.1s ease, height 0.1s ease; transition:
transform 0.1s ease,
width 0.1s ease,
height 0.1s ease;
} }
/* Make the scrubber larger when dragging for better control */ /* Make the scrubber larger when dragging for better control */
@ -258,7 +274,7 @@
/* Create a larger invisible touch target */ /* Create a larger invisible touch target */
.video-scrubber:before { .video-scrubber:before {
content: ''; content: "";
position: absolute; position: absolute;
top: -10px; top: -10px;
left: -10px; left: -10px;
@ -312,7 +328,7 @@
/* Add a small arrow to the tooltip */ /* Add a small arrow to the tooltip */
.video-time-tooltip:after { .video-time-tooltip:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -4px; bottom: -4px;
left: 50%; left: 50%;