mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 07:28:53 -05:00
style: Format entire codebase (video-editor) with Prettier
This commit is contained in:
parent
e101a48c48
commit
add6f6a704
@ -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;
|
||||||
});
|
});
|
||||||
@ -171,24 +171,32 @@ const App = () => {
|
|||||||
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"
|
||||||
|
: nextSegment
|
||||||
|
? "next segment start"
|
||||||
|
: "end of video",
|
||||||
|
segment: currentSegment
|
||||||
|
? {
|
||||||
id: currentSegment.id,
|
id: currentSegment.id,
|
||||||
start: formatDetailedTime(currentSegment.startTime),
|
start: formatDetailedTime(currentSegment.startTime),
|
||||||
end: formatDetailedTime(currentSegment.endTime)
|
end: formatDetailedTime(currentSegment.endTime)
|
||||||
} : null,
|
}
|
||||||
nextSegment: nextSegment ? {
|
: null,
|
||||||
|
nextSegment: nextSegment
|
||||||
|
? {
|
||||||
id: nextSegment.id,
|
id: nextSegment.id,
|
||||||
start: formatDetailedTime(nextSegment.startTime),
|
start: formatDetailedTime(nextSegment.startTime),
|
||||||
end: formatDetailedTime(nextSegment.endTime)
|
end: formatDetailedTime(nextSegment.endTime)
|
||||||
} : null
|
}
|
||||||
|
: 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,
|
id: currentSegment.id,
|
||||||
start: formatDetailedTime(currentSegment.startTime),
|
start: formatDetailedTime(currentSegment.startTime),
|
||||||
end: formatDetailedTime(currentSegment.endTime)
|
end: formatDetailedTime(currentSegment.endTime)
|
||||||
} : 'None',
|
}
|
||||||
nextSegment: nextSegment ? {
|
: "None",
|
||||||
|
nextSegment: nextSegment
|
||||||
|
? {
|
||||||
id: nextSegment.id,
|
id: nextSegment.id,
|
||||||
start: formatDetailedTime(nextSegment.startTime),
|
start: formatDetailedTime(nextSegment.startTime),
|
||||||
end: formatDetailedTime(nextSegment.endTime)
|
end: formatDetailedTime(nextSegment.endTime)
|
||||||
} : 'None'
|
}
|
||||||
|
: "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 */}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
|
className="video-time-tooltip"
|
||||||
|
style={{
|
||||||
left: `${tooltipPosition.x}px`,
|
left: `${tooltipPosition.x}px`,
|
||||||
transform: 'translateX(-50%)'
|
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>
|
||||||
|
|||||||
@ -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(
|
||||||
|
"Segment details after undo:",
|
||||||
|
previousState.clipSegments.map(
|
||||||
|
(seg) =>
|
||||||
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
`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(
|
||||||
|
"Segment details after redo:",
|
||||||
|
nextState.clipSegments.map(
|
||||||
|
(seg) =>
|
||||||
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
`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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ interface TrimVideoResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to simulate delay
|
// Helper function to simulate delay
|
||||||
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
// For now, we'll use a mock API that returns a promise
|
// 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)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -145,8 +145,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-success-icon {
|
.modal-success-icon {
|
||||||
@ -160,7 +164,7 @@
|
|||||||
.modal-success-icon svg {
|
.modal-success-icon svg {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
color: #4CAF50;
|
color: #4caf50;
|
||||||
animation: success-pop 0.5s ease-out;
|
animation: success-pop 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +194,7 @@
|
|||||||
.modal-error-icon svg {
|
.modal-error-icon svg {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
color: #F44336;
|
color: #f44336;
|
||||||
animation: error-pop 0.5s ease-out;
|
animation: error-pop 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,7 +247,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.success-link {
|
.success-link {
|
||||||
background-color: #4CAF50;
|
background-color: #4caf50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-link:hover {
|
.success-link:hover {
|
||||||
@ -277,12 +281,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
color: #F44336;
|
color: #f44336;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background-color: rgba(244, 67, 54, 0.1);
|
background-color: rgba(244, 67, 54, 0.1);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-left: 4px solid #F44336;
|
border-left: 4px solid #f44336;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user