mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 07:28:53 -05:00
chore: video-editor update with new prettier settings
This commit is contained in:
parent
eaf87e20d8
commit
074638e237
@ -1,5 +1,5 @@
|
||||
import { formatTime, formatLongTime } from "@/lib/timeUtils";
|
||||
import "../styles/ClipSegments.css";
|
||||
import { formatTime, formatLongTime } from '@/lib/timeUtils';
|
||||
import '../styles/ClipSegments.css';
|
||||
|
||||
export interface Segment {
|
||||
id: number;
|
||||
@ -20,8 +20,8 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
|
||||
// Handle delete segment click
|
||||
const handleDeleteSegment = (segmentId: number) => {
|
||||
// Create and dispatch the delete event
|
||||
const deleteEvent = new CustomEvent("delete-segment", {
|
||||
detail: { segmentId }
|
||||
const deleteEvent = new CustomEvent('delete-segment', {
|
||||
detail: { segmentId },
|
||||
});
|
||||
document.dispatchEvent(deleteEvent);
|
||||
};
|
||||
@ -74,9 +74,7 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
|
||||
))}
|
||||
|
||||
{sortedSegments.length === 0 && (
|
||||
<div className="empty-message">
|
||||
No segments created yet. Use the split button to create segments.
|
||||
</div>
|
||||
<div className="empty-message">No segments created yet. Use the split button to create segments.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import "../styles/EditingTools.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import '../styles/EditingTools.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface EditingToolsProps {
|
||||
onSplit: () => void;
|
||||
@ -24,7 +24,7 @@ const EditingTools = ({
|
||||
canUndo,
|
||||
canRedo,
|
||||
isPlaying = false,
|
||||
isPlayingSegments = false
|
||||
isPlayingSegments = false,
|
||||
}: EditingToolsProps) => {
|
||||
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
||||
|
||||
@ -34,15 +34,15 @@ const EditingTools = ({
|
||||
};
|
||||
|
||||
checkScreenSize();
|
||||
window.addEventListener("resize", checkScreenSize);
|
||||
return () => window.removeEventListener("resize", checkScreenSize);
|
||||
window.addEventListener('resize', checkScreenSize);
|
||||
return () => window.removeEventListener('resize', checkScreenSize);
|
||||
}, []);
|
||||
|
||||
// Handle play button click with iOS fix
|
||||
const handlePlay = () => {
|
||||
// Ensure lastSeekedPosition is used when play is clicked
|
||||
if (typeof window !== "undefined") {
|
||||
console.log("Play button clicked, current lastSeekedPosition:", window.lastSeekedPosition);
|
||||
if (typeof window !== 'undefined') {
|
||||
console.log('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
|
||||
}
|
||||
|
||||
// Call the original handler
|
||||
@ -59,9 +59,9 @@ const EditingTools = ({
|
||||
className={`button segments-button`}
|
||||
onClick={onPlaySegments}
|
||||
data-tooltip={
|
||||
isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow"
|
||||
isPlayingSegments ? 'Stop segments playback' : 'Play segments in one continuous flow'
|
||||
}
|
||||
style={{ fontSize: "0.875rem" }}
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
>
|
||||
{isPlayingSegments ? (
|
||||
<>
|
||||
@ -133,10 +133,10 @@ const EditingTools = ({
|
||||
{/* Standard Play button (only shown when not in segments playback on small screens) */}
|
||||
{(!isPlayingSegments || !isSmallScreen) && (
|
||||
<button
|
||||
className={`button play-button ${isPlayingSegments ? "greyed-out" : ""}`}
|
||||
className={`button play-button ${isPlayingSegments ? 'greyed-out' : ''}`}
|
||||
onClick={handlePlay}
|
||||
data-tooltip={isPlaying ? "Pause video" : "Play full video"}
|
||||
style={{ fontSize: "0.875rem" }}
|
||||
data-tooltip={isPlaying ? 'Pause video' : 'Play full video'}
|
||||
style={{ fontSize: '0.875rem' }}
|
||||
disabled={isPlayingSegments}
|
||||
>
|
||||
{isPlaying && !isPlayingSegments ? (
|
||||
@ -208,7 +208,7 @@ const EditingTools = ({
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Undo"
|
||||
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Undo last action"}
|
||||
data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Undo last action'}
|
||||
disabled={!canUndo || isPlayingSegments}
|
||||
onClick={onUndo}
|
||||
>
|
||||
@ -229,7 +229,7 @@ const EditingTools = ({
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Redo"
|
||||
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Redo last undone action"}
|
||||
data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Redo last undone action'}
|
||||
disabled={!canRedo || isPlayingSegments}
|
||||
onClick={onRedo}
|
||||
>
|
||||
@ -251,7 +251,7 @@ const EditingTools = ({
|
||||
<button
|
||||
className="button"
|
||||
onClick={onReset}
|
||||
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Reset to full video"}
|
||||
data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Reset to full video'}
|
||||
disabled={isPlayingSegments}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import "../styles/IOSPlayPrompt.css";
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import '../styles/IOSPlayPrompt.css';
|
||||
|
||||
interface MobilePlayPromptProps {
|
||||
videoRef: React.RefObject<HTMLVideoElement>;
|
||||
@ -33,9 +33,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
video.addEventListener("play", handlePlay);
|
||||
video.addEventListener('play', handlePlay);
|
||||
return () => {
|
||||
video.removeEventListener("play", handlePlay);
|
||||
video.removeEventListener('play', handlePlay);
|
||||
};
|
||||
}, [videoRef]);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { formatTime } from "@/lib/timeUtils";
|
||||
import "../styles/IOSVideoPlayer.css";
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { formatTime } from '@/lib/timeUtils';
|
||||
import '../styles/IOSVideoPlayer.css';
|
||||
|
||||
interface IOSVideoPlayerProps {
|
||||
videoRef: React.RefObject<HTMLVideoElement>;
|
||||
@ -9,7 +9,7 @@ interface IOSVideoPlayerProps {
|
||||
}
|
||||
|
||||
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
|
||||
const [videoUrl, setVideoUrl] = useState<string>("");
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
|
||||
|
||||
// Refs for hold-to-continue functionality
|
||||
@ -26,14 +26,14 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
|
||||
// Get the video source URL from the main player
|
||||
useEffect(() => {
|
||||
if (videoRef.current && videoRef.current.querySelector("source")) {
|
||||
const source = videoRef.current.querySelector("source") as HTMLSourceElement;
|
||||
if (videoRef.current && videoRef.current.querySelector('source')) {
|
||||
const source = videoRef.current.querySelector('source') as HTMLSourceElement;
|
||||
if (source && source.src) {
|
||||
setVideoUrl(source.src);
|
||||
}
|
||||
} else {
|
||||
// Fallback to sample video if needed
|
||||
setVideoUrl("/videos/sample-video-10m.mp4");
|
||||
setVideoUrl('/videos/sample-video.mp4');
|
||||
}
|
||||
}, [videoRef]);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import "../styles/Modal.css";
|
||||
import React, { useEffect } from 'react';
|
||||
import '../styles/Modal.css';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
@ -13,21 +13,21 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions
|
||||
// Close modal when Escape key is pressed
|
||||
useEffect(() => {
|
||||
const handleEscapeKey = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isOpen) {
|
||||
if (event.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleEscapeKey);
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
|
||||
// Disable body scrolling when modal is open
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscapeKey);
|
||||
document.body.style.overflow = "";
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { formatTime, formatDetailedTime } from "@/lib/timeUtils";
|
||||
import logger from "../lib/logger";
|
||||
import "../styles/VideoPlayer.css";
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { formatTime, formatDetailedTime } from '@/lib/timeUtils';
|
||||
import logger from '../lib/logger';
|
||||
import '../styles/VideoPlayer.css';
|
||||
|
||||
interface VideoPlayerProps {
|
||||
videoRef: React.RefObject<HTMLVideoElement>;
|
||||
@ -22,7 +22,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
isMuted = false,
|
||||
onPlayPause,
|
||||
onSeek,
|
||||
onToggleMute
|
||||
onToggleMute,
|
||||
}) => {
|
||||
const progressRef = useRef<HTMLDivElement>(null);
|
||||
const [isIOS, setIsIOS] = useState(false);
|
||||
@ -30,12 +30,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const [lastPosition, setLastPosition] = useState<number | null>(null);
|
||||
const [isDraggingProgress, setIsDraggingProgress] = useState(false);
|
||||
const isDraggingProgressRef = useRef(false);
|
||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 });
|
||||
const [tooltipPosition, setTooltipPosition] = useState({
|
||||
x: 0,
|
||||
});
|
||||
const [tooltipTime, setTooltipTime] = useState(0);
|
||||
|
||||
const sampleVideoUrl =
|
||||
(typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) ||
|
||||
"/videos/sample-video-10m.mp4";
|
||||
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.mp4';
|
||||
|
||||
// Detect iOS device
|
||||
useEffect(() => {
|
||||
@ -47,8 +48,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
setIsIOS(checkIOS());
|
||||
|
||||
// Check if video was previously initialized
|
||||
if (typeof window !== "undefined") {
|
||||
const wasInitialized = localStorage.getItem("video_initialized") === "true";
|
||||
if (typeof window !== 'undefined') {
|
||||
const wasInitialized = localStorage.getItem('video_initialized') === 'true';
|
||||
setHasInitialized(wasInitialized);
|
||||
}
|
||||
}, []);
|
||||
@ -57,8 +58,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
useEffect(() => {
|
||||
if (isPlaying && !hasInitialized) {
|
||||
setHasInitialized(true);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("video_initialized", "true");
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('video_initialized', 'true');
|
||||
}
|
||||
}
|
||||
}, [isPlaying, hasInitialized]);
|
||||
@ -70,15 +71,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
// These attributes need to be set directly on the DOM element
|
||||
// for iOS Safari to respect inline playback
|
||||
video.setAttribute("playsinline", "true");
|
||||
video.setAttribute("webkit-playsinline", "true");
|
||||
video.setAttribute("x-webkit-airplay", "allow");
|
||||
video.setAttribute('playsinline', 'true');
|
||||
video.setAttribute('webkit-playsinline', 'true');
|
||||
video.setAttribute('x-webkit-airplay', 'allow');
|
||||
|
||||
// Store the last known good position for iOS
|
||||
const handleTimeUpdate = () => {
|
||||
if (!isDraggingProgressRef.current) {
|
||||
setLastPosition(video.currentTime);
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.lastSeekedPosition = video.currentTime;
|
||||
}
|
||||
}
|
||||
@ -86,25 +87,25 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
// Handle iOS-specific play/pause state
|
||||
const handlePlay = () => {
|
||||
logger.debug("Video play event fired");
|
||||
logger.debug('Video play event fired');
|
||||
if (isIOS) {
|
||||
setHasInitialized(true);
|
||||
localStorage.setItem("video_initialized", "true");
|
||||
localStorage.setItem('video_initialized', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePause = () => {
|
||||
logger.debug("Video pause event fired");
|
||||
logger.debug('Video pause event fired');
|
||||
};
|
||||
|
||||
video.addEventListener("timeupdate", handleTimeUpdate);
|
||||
video.addEventListener("play", handlePlay);
|
||||
video.addEventListener("pause", handlePause);
|
||||
video.addEventListener('timeupdate', handleTimeUpdate);
|
||||
video.addEventListener('play', handlePlay);
|
||||
video.addEventListener('pause', handlePause);
|
||||
|
||||
return () => {
|
||||
video.removeEventListener("timeupdate", handleTimeUpdate);
|
||||
video.removeEventListener("play", handlePlay);
|
||||
video.removeEventListener("pause", handlePause);
|
||||
video.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
video.removeEventListener('play', handlePlay);
|
||||
video.removeEventListener('pause', handlePause);
|
||||
};
|
||||
}, [videoRef, isIOS, isDraggingProgressRef]);
|
||||
|
||||
@ -150,12 +151,12 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const handleMouseUp = () => {
|
||||
setIsDraggingProgress(false);
|
||||
isDraggingProgressRef.current = false;
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
// Handle progress dragging for both mouse and touch events
|
||||
@ -167,14 +168,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const seekTime = duration * clickPosition;
|
||||
|
||||
// Update tooltip position and time
|
||||
setTooltipPosition({ x: e.clientX });
|
||||
setTooltipPosition({
|
||||
x: e.clientX,
|
||||
});
|
||||
setTooltipTime(seekTime);
|
||||
|
||||
// Store position locally for iOS Safari - critical for timeline seeking
|
||||
setLastPosition(seekTime);
|
||||
|
||||
// Also store globally for integration with other components
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).lastSeekedPosition = seekTime;
|
||||
}
|
||||
|
||||
@ -202,14 +205,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const handleTouchEnd = () => {
|
||||
setIsDraggingProgress(false);
|
||||
isDraggingProgressRef.current = false;
|
||||
document.removeEventListener("touchmove", handleTouchMove);
|
||||
document.removeEventListener("touchend", handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", handleTouchEnd);
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
document.removeEventListener('touchcancel', handleTouchEnd);
|
||||
};
|
||||
|
||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
||||
document.addEventListener("touchend", handleTouchEnd);
|
||||
document.addEventListener("touchcancel", handleTouchEnd);
|
||||
document.addEventListener('touchmove', handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
document.addEventListener('touchcancel', handleTouchEnd);
|
||||
};
|
||||
|
||||
// Handle touch dragging on progress bar
|
||||
@ -217,7 +222,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
if (!progressRef.current) return;
|
||||
|
||||
// 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;
|
||||
|
||||
e.preventDefault(); // Prevent scrolling while dragging
|
||||
@ -227,14 +232,16 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const seekTime = duration * touchPosition;
|
||||
|
||||
// Update tooltip position and time
|
||||
setTooltipPosition({ x: touch.clientX });
|
||||
setTooltipPosition({
|
||||
x: touch.clientX,
|
||||
});
|
||||
setTooltipTime(seekTime);
|
||||
|
||||
// Store position for iOS Safari
|
||||
setLastPosition(seekTime);
|
||||
|
||||
// Also store globally for integration with other components
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).lastSeekedPosition = seekTime;
|
||||
}
|
||||
|
||||
@ -255,7 +262,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
setLastPosition(seekTime);
|
||||
|
||||
// Also store globally for integration with other components
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).lastSeekedPosition = seekTime;
|
||||
}
|
||||
|
||||
@ -283,7 +290,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
if (video.paused) {
|
||||
// For iOS Safari: Before playing, explicitly seek to the remembered position
|
||||
if (isIOS && lastPosition !== null && lastPosition > 0) {
|
||||
logger.debug("iOS: Explicitly setting position before play:", lastPosition);
|
||||
logger.debug('iOS: Explicitly setting position before play:', lastPosition);
|
||||
|
||||
// First, seek to the position
|
||||
video.currentTime = lastPosition;
|
||||
@ -296,13 +303,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
.play()
|
||||
.then(() => {
|
||||
logger.debug(
|
||||
"iOS: Play started successfully at position:",
|
||||
'iOS: Play started successfully at position:',
|
||||
videoRef.current?.currentTime
|
||||
);
|
||||
onPlayPause(); // Update parent state after successful play
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("iOS: Error playing video:", err);
|
||||
console.error('iOS: Error playing video:', err);
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
@ -311,11 +318,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
video
|
||||
.play()
|
||||
.then(() => {
|
||||
logger.debug("Normal: Play started successfully");
|
||||
logger.debug('Normal: Play started successfully');
|
||||
onPlayPause(); // Update parent state after successful play
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error playing video:", err);
|
||||
console.error('Error playing video:', err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@ -350,7 +357,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
)}
|
||||
|
||||
{/* 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 */}
|
||||
<div className="video-controls">
|
||||
@ -363,13 +370,23 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
{/* Progress Bar with enhanced dragging */}
|
||||
<div
|
||||
ref={progressRef}
|
||||
className={`video-progress ${isDraggingProgress ? "dragging" : ""}`}
|
||||
className={`video-progress ${isDraggingProgress ? 'dragging' : ''}`}
|
||||
onClick={handleProgressClick}
|
||||
onMouseDown={handleProgressDragStart}
|
||||
onTouchStart={handleProgressTouchStart}
|
||||
>
|
||||
<div className="video-progress-fill" style={{ width: `${progressPercentage}%` }}></div>
|
||||
<div className="video-scrubber" style={{ left: `${progressPercentage}%` }}></div>
|
||||
<div
|
||||
className="video-progress-fill"
|
||||
style={{
|
||||
width: `${progressPercentage}%`,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
className="video-scrubber"
|
||||
style={{
|
||||
left: `${progressPercentage}%`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
{/* Floating time tooltip when dragging */}
|
||||
{isDraggingProgress && (
|
||||
@ -377,7 +394,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
className="video-time-tooltip"
|
||||
style={{
|
||||
left: `${tooltipPosition.x}px`,
|
||||
transform: "translateX(-50%)"
|
||||
transform: 'translateX(-50%)',
|
||||
}}
|
||||
>
|
||||
{formatDetailedTime(tooltipTime)}
|
||||
@ -391,9 +408,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
{onToggleMute && (
|
||||
<button
|
||||
className="mute-button"
|
||||
aria-label={isMuted ? "Unmute" : "Mute"}
|
||||
aria-label={isMuted ? 'Unmute' : 'Mute'}
|
||||
onClick={onToggleMute}
|
||||
data-tooltip={isMuted ? "Unmute" : "Mute"}
|
||||
data-tooltip={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{isMuted ? (
|
||||
<svg
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { generateThumbnail } from "@/lib/videoUtils";
|
||||
import { formatDetailedTime } from "@/lib/timeUtils";
|
||||
import logger from "@/lib/logger";
|
||||
import type { Segment } from "@/components/ClipSegments";
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { generateThumbnail } from '@/lib/videoUtils';
|
||||
import { formatDetailedTime } from '@/lib/timeUtils';
|
||||
import logger from '@/lib/logger';
|
||||
import type { Segment } from '@/components/ClipSegments';
|
||||
|
||||
// Represents a state of the editor for undo/redo
|
||||
interface EditorState {
|
||||
@ -46,21 +46,18 @@ const useVideoTrimmer = () => {
|
||||
useEffect(() => {
|
||||
if (history.length > 0) {
|
||||
// For debugging - moved to console.debug
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug(
|
||||
`History state updated: ${history.length} entries, position: ${historyPosition}`
|
||||
);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug(`History state updated: ${history.length} entries, position: ${historyPosition}`);
|
||||
// Log actions in history to help debug undo/redo
|
||||
const actions = history.map(
|
||||
(state, idx) =>
|
||||
`${idx}: ${state.action || "unknown"} (segments: ${state.clipSegments.length})`
|
||||
(state, idx) => `${idx}: ${state.action || 'unknown'} (segments: ${state.clipSegments.length})`
|
||||
);
|
||||
console.debug("History actions:", actions);
|
||||
console.debug('History actions:', actions);
|
||||
}
|
||||
|
||||
// If there's at least one history entry and it wasn't a save operation, mark as having unsaved changes
|
||||
const lastAction = history[historyPosition]?.action || "";
|
||||
if (lastAction !== "save" && lastAction !== "save_copy" && lastAction !== "save_segments") {
|
||||
const lastAction = history[historyPosition]?.action || '';
|
||||
if (lastAction !== 'save' && lastAction !== 'save_copy' && lastAction !== 'save_segments') {
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
}
|
||||
@ -72,7 +69,7 @@ const useVideoTrimmer = () => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (hasUnsavedChanges) {
|
||||
// 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.returnValue = message; // Chrome requires returnValue to be set
|
||||
return message; // For other browsers
|
||||
@ -80,11 +77,11 @@ const useVideoTrimmer = () => {
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
@ -105,10 +102,10 @@ const useVideoTrimmer = () => {
|
||||
// Create an initial segment that spans the entire video
|
||||
const initialSegment: Segment = {
|
||||
id: 1,
|
||||
name: "segment",
|
||||
name: 'segment',
|
||||
startTime: 0,
|
||||
endTime: video.duration,
|
||||
thumbnail: segmentThumbnail
|
||||
thumbnail: segmentThumbnail,
|
||||
};
|
||||
|
||||
// Initialize history state with the full-length segment
|
||||
@ -116,7 +113,7 @@ const useVideoTrimmer = () => {
|
||||
trimStart: 0,
|
||||
trimEnd: video.duration,
|
||||
splitPoints: [],
|
||||
clipSegments: [initialSegment]
|
||||
clipSegments: [initialSegment],
|
||||
};
|
||||
|
||||
setHistory([initialState]);
|
||||
@ -159,19 +156,19 @@ const useVideoTrimmer = () => {
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
video.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
video.addEventListener("timeupdate", handleTimeUpdate);
|
||||
video.addEventListener("play", handlePlay);
|
||||
video.addEventListener("pause", handlePause);
|
||||
video.addEventListener("ended", handleEnded);
|
||||
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
video.addEventListener('timeupdate', handleTimeUpdate);
|
||||
video.addEventListener('play', handlePlay);
|
||||
video.addEventListener('pause', handlePause);
|
||||
video.addEventListener('ended', handleEnded);
|
||||
|
||||
return () => {
|
||||
// Remove event listeners
|
||||
video.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
video.removeEventListener("timeupdate", handleTimeUpdate);
|
||||
video.removeEventListener("play", handlePlay);
|
||||
video.removeEventListener("pause", handlePause);
|
||||
video.removeEventListener("ended", handleEnded);
|
||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
video.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
video.removeEventListener('play', handlePlay);
|
||||
video.removeEventListener('pause', handlePause);
|
||||
video.removeEventListener('ended', handleEnded);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -184,7 +181,7 @@ const useVideoTrimmer = () => {
|
||||
video.pause();
|
||||
} else {
|
||||
// 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
|
||||
// This avoids unnecessary seeking which might cause playback issues
|
||||
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
|
||||
@ -201,12 +198,12 @@ const useVideoTrimmer = () => {
|
||||
.then(() => {
|
||||
// Play started successfully
|
||||
// Reset the last seeked position after successfully starting playback
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.lastSeekedPosition = 0;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error starting playback:", err);
|
||||
console.error('Error starting playback:', err);
|
||||
setIsPlaying(false); // Reset state if play failed
|
||||
});
|
||||
}
|
||||
@ -226,7 +223,7 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Store the position in a global state accessible to iOS Safari
|
||||
// This ensures when play is pressed later, it remembers the position
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.lastSeekedPosition = time;
|
||||
}
|
||||
|
||||
@ -239,7 +236,7 @@ const useVideoTrimmer = () => {
|
||||
setIsPlaying(true); // Update state to reflect we're playing
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error resuming playback:", err);
|
||||
console.error('Error resuming playback:', err);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
}
|
||||
@ -254,7 +251,7 @@ const useVideoTrimmer = () => {
|
||||
trimEnd,
|
||||
splitPoints: [...splitPoints],
|
||||
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
|
||||
@ -339,16 +336,16 @@ const useVideoTrimmer = () => {
|
||||
if (recordHistory) {
|
||||
// Use a small timeout to ensure the state is updated
|
||||
setTimeout(() => {
|
||||
saveState(action || (isStart ? "adjust_trim_start" : "adjust_trim_end"));
|
||||
saveState(action || (isStart ? 'adjust_trim_start' : 'adjust_trim_end'));
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("update-trim", handleTrimUpdate as EventListener);
|
||||
document.addEventListener('update-trim', handleTrimUpdate as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("update-trim", handleTrimUpdate as EventListener);
|
||||
document.removeEventListener('update-trim', handleTrimUpdate as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@ -360,11 +357,11 @@ const useVideoTrimmer = () => {
|
||||
// Default to true to ensure all segment changes are recorded
|
||||
const isSignificantChange = e.detail.recordHistory !== false;
|
||||
// Get the action type if provided
|
||||
const actionType = e.detail.action || "update_segments";
|
||||
const actionType = e.detail.action || 'update_segments';
|
||||
|
||||
// Log the update details
|
||||
logger.debug(
|
||||
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}`
|
||||
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? 'true' : 'false'}`
|
||||
);
|
||||
|
||||
// Update segment state immediately for UI feedback
|
||||
@ -384,7 +381,7 @@ const useVideoTrimmer = () => {
|
||||
trimEnd,
|
||||
splitPoints: [...splitPoints],
|
||||
clipSegments: segmentsClone,
|
||||
action: actionType // Store the action type in the state
|
||||
action: actionType, // Store the action type in the state
|
||||
};
|
||||
|
||||
// Get the current history position to ensure we're using the latest value
|
||||
@ -405,16 +402,12 @@ const useVideoTrimmer = () => {
|
||||
// Ensure the historyPosition is updated to the correct position
|
||||
setHistoryPosition((prev) => {
|
||||
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;
|
||||
});
|
||||
}, 20); // Slightly increased delay to ensure state updates are complete
|
||||
} 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)`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -423,8 +416,8 @@ const useVideoTrimmer = () => {
|
||||
const customEvent = e as CustomEvent;
|
||||
if (
|
||||
customEvent.detail &&
|
||||
typeof customEvent.detail.time === "number" &&
|
||||
typeof customEvent.detail.segmentId === "number"
|
||||
typeof customEvent.detail.time === 'number' &&
|
||||
typeof customEvent.detail.segmentId === 'number'
|
||||
) {
|
||||
// Get the time and segment ID from the event
|
||||
const timeToSplit = customEvent.detail.time;
|
||||
@ -457,7 +450,7 @@ const useVideoTrimmer = () => {
|
||||
name: `${segmentToSplit.name}-A`,
|
||||
startTime: segmentToSplit.startTime,
|
||||
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
|
||||
@ -466,7 +459,7 @@ const useVideoTrimmer = () => {
|
||||
name: `${segmentToSplit.name}-B`,
|
||||
startTime: timeToSplit,
|
||||
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
|
||||
@ -477,14 +470,14 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Update state
|
||||
setClipSegments(newSegments);
|
||||
saveState("split_segment");
|
||||
saveState('split_segment');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete segment event
|
||||
const handleDeleteSegment = async (e: Event) => {
|
||||
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;
|
||||
|
||||
// Find and remove the segment
|
||||
@ -497,10 +490,10 @@ const useVideoTrimmer = () => {
|
||||
// No need to generate a thumbnail - we'll use dynamic colors
|
||||
const defaultSegment: Segment = {
|
||||
id: Date.now(),
|
||||
name: "segment",
|
||||
name: 'segment',
|
||||
startTime: 0,
|
||||
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
|
||||
@ -512,32 +505,32 @@ const useVideoTrimmer = () => {
|
||||
// Just update the segments normally
|
||||
setClipSegments(newSegments);
|
||||
}
|
||||
saveState("delete_segment");
|
||||
saveState('delete_segment');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("update-segments", handleUpdateSegments as EventListener);
|
||||
document.addEventListener("split-segment", handleSplitSegment as EventListener);
|
||||
document.addEventListener("delete-segment", handleDeleteSegment as EventListener);
|
||||
document.addEventListener('update-segments', handleUpdateSegments as EventListener);
|
||||
document.addEventListener('split-segment', handleSplitSegment as EventListener);
|
||||
document.addEventListener('delete-segment', handleDeleteSegment as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("update-segments", handleUpdateSegments as EventListener);
|
||||
document.removeEventListener("split-segment", handleSplitSegment as EventListener);
|
||||
document.removeEventListener("delete-segment", handleDeleteSegment as EventListener);
|
||||
document.removeEventListener('update-segments', handleUpdateSegments as EventListener);
|
||||
document.removeEventListener('split-segment', handleSplitSegment as EventListener);
|
||||
document.removeEventListener('delete-segment', handleDeleteSegment as EventListener);
|
||||
};
|
||||
}, [clipSegments, duration]);
|
||||
|
||||
// Handle trim start change
|
||||
const handleTrimStartChange = (time: number) => {
|
||||
setTrimStart(time);
|
||||
saveState("adjust_trim_start");
|
||||
saveState('adjust_trim_start');
|
||||
};
|
||||
|
||||
// Handle trim end change
|
||||
const handleTrimEndChange = (time: number) => {
|
||||
setTrimEnd(time);
|
||||
saveState("adjust_trim_end");
|
||||
saveState('adjust_trim_end');
|
||||
};
|
||||
|
||||
// Handle split at current position
|
||||
@ -563,7 +556,7 @@ const useVideoTrimmer = () => {
|
||||
name: `Segment ${i + 1}`,
|
||||
startTime,
|
||||
endTime,
|
||||
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
||||
});
|
||||
|
||||
startTime = endTime;
|
||||
@ -571,7 +564,7 @@ const useVideoTrimmer = () => {
|
||||
}
|
||||
|
||||
setClipSegments(newSegments);
|
||||
saveState("create_split_points");
|
||||
saveState('create_split_points');
|
||||
}
|
||||
};
|
||||
|
||||
@ -587,14 +580,14 @@ const useVideoTrimmer = () => {
|
||||
// No need to generate thumbnails - we'll use dynamic colors
|
||||
const defaultSegment: Segment = {
|
||||
id: Date.now(),
|
||||
name: "segment",
|
||||
name: 'segment',
|
||||
startTime: 0,
|
||||
endTime: duration,
|
||||
thumbnail: "" // Empty placeholder - we'll use dynamic colors instead
|
||||
thumbnail: '', // Empty placeholder - we'll use dynamic colors instead
|
||||
};
|
||||
|
||||
setClipSegments([defaultSegment]);
|
||||
saveState("reset_all");
|
||||
saveState('reset_all');
|
||||
};
|
||||
|
||||
// Handle undo
|
||||
@ -607,7 +600,7 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Log segment details to help debug
|
||||
logger.debug(
|
||||
"Segment details after undo:",
|
||||
'Segment details after undo:',
|
||||
previousState.clipSegments.map(
|
||||
(seg) =>
|
||||
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
||||
@ -621,7 +614,7 @@ const useVideoTrimmer = () => {
|
||||
setClipSegments(JSON.parse(JSON.stringify(previousState.clipSegments)));
|
||||
setHistoryPosition(historyPosition - 1);
|
||||
} else {
|
||||
logger.debug("Cannot undo: at earliest history position");
|
||||
logger.debug('Cannot undo: at earliest history position');
|
||||
}
|
||||
};
|
||||
|
||||
@ -635,7 +628,7 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Log segment details to help debug
|
||||
logger.debug(
|
||||
"Segment details after redo:",
|
||||
'Segment details after redo:',
|
||||
nextState.clipSegments.map(
|
||||
(seg) =>
|
||||
`ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}`
|
||||
@ -649,7 +642,7 @@ const useVideoTrimmer = () => {
|
||||
setClipSegments(JSON.parse(JSON.stringify(nextState.clipSegments)));
|
||||
setHistoryPosition(historyPosition + 1);
|
||||
} else {
|
||||
logger.debug("Cannot redo: at latest history position");
|
||||
logger.debug('Cannot redo: at latest history position');
|
||||
}
|
||||
};
|
||||
|
||||
@ -669,10 +662,10 @@ const useVideoTrimmer = () => {
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
// 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
|
||||
if (Math.abs(video.currentTime - window.lastSeekedPosition) > 0.1) {
|
||||
console.log("handlePlay: Using lastSeekedPosition", window.lastSeekedPosition);
|
||||
console.log('handlePlay: Using lastSeekedPosition', window.lastSeekedPosition);
|
||||
video.currentTime = window.lastSeekedPosition;
|
||||
}
|
||||
}
|
||||
@ -683,12 +676,12 @@ const useVideoTrimmer = () => {
|
||||
.then(() => {
|
||||
setIsPlaying(true);
|
||||
// Reset lastSeekedPosition after successful play
|
||||
if (typeof window !== "undefined") {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.lastSeekedPosition = 0;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error playing video:", err);
|
||||
console.error('Error playing video:', err);
|
||||
setIsPlaying(false); // Reset state if play failed
|
||||
});
|
||||
}
|
||||
@ -710,28 +703,28 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Create the JSON data for saving
|
||||
const saveData = {
|
||||
type: "save",
|
||||
type: 'save',
|
||||
segments: sortedSegments.map((segment) => ({
|
||||
startTime: formatDetailedTime(segment.startTime),
|
||||
endTime: formatDetailedTime(segment.endTime)
|
||||
}))
|
||||
endTime: formatDetailedTime(segment.endTime),
|
||||
})),
|
||||
};
|
||||
|
||||
// Display JSON in alert (for demonstration purposes)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Saving data:", saveData);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('Saving data:', saveData);
|
||||
}
|
||||
|
||||
// Mark as saved - no unsaved changes
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// Debug message
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Changes saved - reset unsaved changes flag");
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('Changes saved - reset unsaved changes flag');
|
||||
}
|
||||
|
||||
// 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
|
||||
// logger.debug("Save data:", saveData);
|
||||
@ -744,28 +737,28 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Create the JSON data for saving as a copy
|
||||
const saveData = {
|
||||
type: "save_as_a_copy",
|
||||
type: 'save_as_a_copy',
|
||||
segments: sortedSegments.map((segment) => ({
|
||||
startTime: formatDetailedTime(segment.startTime),
|
||||
endTime: formatDetailedTime(segment.endTime)
|
||||
}))
|
||||
endTime: formatDetailedTime(segment.endTime),
|
||||
})),
|
||||
};
|
||||
|
||||
// Display JSON in alert (for demonstration purposes)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Saving data as copy:", saveData);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('Saving data as copy:', saveData);
|
||||
}
|
||||
|
||||
// Mark as saved - no unsaved changes
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// Debug message
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Changes saved as copy - reset unsaved changes flag");
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('Changes saved as copy - reset unsaved changes flag');
|
||||
}
|
||||
|
||||
// Save to history with special "save_copy" action to mark saved state
|
||||
saveState("save_copy");
|
||||
saveState('save_copy');
|
||||
};
|
||||
|
||||
// Handle save segments individually action
|
||||
@ -775,27 +768,27 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Create the JSON data for saving individual segments
|
||||
const saveData = {
|
||||
type: "save_segments",
|
||||
type: 'save_segments',
|
||||
segments: sortedSegments.map((segment) => ({
|
||||
name: segment.name,
|
||||
startTime: formatDetailedTime(segment.startTime),
|
||||
endTime: formatDetailedTime(segment.endTime)
|
||||
}))
|
||||
endTime: formatDetailedTime(segment.endTime),
|
||||
})),
|
||||
};
|
||||
|
||||
// Display JSON in alert (for demonstration purposes)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
console.debug("Saving data as segments:", saveData);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug('Saving data as segments:', saveData);
|
||||
}
|
||||
|
||||
// Mark as saved - no unsaved changes
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
// Debug message
|
||||
logger.debug("All segments saved individually - reset unsaved changes flag");
|
||||
logger.debug('All segments saved individually - reset unsaved changes flag');
|
||||
|
||||
// Save to history with special "save_segments" action to mark saved state
|
||||
saveState("save_segments");
|
||||
saveState('save_segments');
|
||||
};
|
||||
|
||||
// Handle seeking with mobile check
|
||||
@ -808,10 +801,8 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// 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
|
||||
);
|
||||
typeof window !== 'undefined' &&
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent);
|
||||
|
||||
// Add videoInitialized state
|
||||
const [videoInitialized, setVideoInitialized] = useState(false);
|
||||
@ -845,9 +836,9 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// If video is somehow paused, ensure it keeps playing
|
||||
if (video.paused) {
|
||||
logger.debug("Ensuring playback continues to next segment");
|
||||
logger.debug('Ensuring playback continues to next segment');
|
||||
video.play().catch((err) => {
|
||||
console.error("Error continuing segment playback:", err);
|
||||
console.error('Error continuing segment playback:', err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@ -855,12 +846,12 @@ const useVideoTrimmer = () => {
|
||||
video.pause();
|
||||
setIsPlayingSegments(false);
|
||||
setCurrentSegmentIndex(0);
|
||||
video.removeEventListener("timeupdate", handleSegmentsPlayback);
|
||||
video.removeEventListener('timeupdate', handleSegmentsPlayback);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
video.addEventListener("timeupdate", handleSegmentsPlayback);
|
||||
video.addEventListener('timeupdate', handleSegmentsPlayback);
|
||||
|
||||
// Start playing if not already playing
|
||||
if (video.paused && orderedSegments.length > 0) {
|
||||
@ -869,7 +860,7 @@ const useVideoTrimmer = () => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
video.removeEventListener("timeupdate", handleSegmentsPlayback);
|
||||
video.removeEventListener('timeupdate', handleSegmentsPlayback);
|
||||
};
|
||||
}, [isPlayingSegments, currentSegmentIndex, clipSegments]);
|
||||
|
||||
@ -878,20 +869,15 @@ const useVideoTrimmer = () => {
|
||||
const handleSegmentIndexUpdate = (event: CustomEvent) => {
|
||||
const { segmentIndex } = event.detail;
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("update-segment-index", handleSegmentIndexUpdate as EventListener);
|
||||
document.addEventListener('update-segment-index', handleSegmentIndexUpdate as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener(
|
||||
"update-segment-index",
|
||||
handleSegmentIndexUpdate as EventListener
|
||||
);
|
||||
document.removeEventListener('update-segment-index', handleSegmentIndexUpdate as EventListener);
|
||||
};
|
||||
}, [isPlayingSegments, currentSegmentIndex]);
|
||||
|
||||
@ -920,11 +906,11 @@ const useVideoTrimmer = () => {
|
||||
|
||||
// Start playback with proper error handling
|
||||
video.play().catch((err) => {
|
||||
console.error("Error starting segments playback:", err);
|
||||
console.error('Error starting segments playback:', err);
|
||||
setIsPlayingSegments(false);
|
||||
});
|
||||
|
||||
logger.debug("Starting playback of all segments continuously");
|
||||
logger.debug('Starting playback of all segments continuously');
|
||||
}
|
||||
};
|
||||
|
||||
@ -960,7 +946,7 @@ const useVideoTrimmer = () => {
|
||||
handleSaveSegments,
|
||||
isMobile,
|
||||
videoInitialized,
|
||||
setVideoInitialized
|
||||
setVideoInitialized,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ const logger = {
|
||||
* Logs debug messages only in development environment
|
||||
*/
|
||||
debug: (...args: any[]) => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.debug(...args);
|
||||
}
|
||||
},
|
||||
@ -25,7 +25,7 @@ const logger = {
|
||||
/**
|
||||
* Always logs info messages
|
||||
*/
|
||||
info: (...args: any[]) => console.info(...args)
|
||||
info: (...args: any[]) => console.info(...args),
|
||||
};
|
||||
|
||||
export default logger;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { QueryClient, QueryFunction } from "@tanstack/react-query";
|
||||
import { QueryClient, QueryFunction } from '@tanstack/react-query';
|
||||
|
||||
async function throwIfResNotOk(res: Response) {
|
||||
if (!res.ok) {
|
||||
@ -7,31 +7,27 @@ async function throwIfResNotOk(res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
data?: unknown | undefined
|
||||
): Promise<Response> {
|
||||
export async function apiRequest(method: string, url: string, data?: unknown | undefined): Promise<Response> {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: data ? { "Content-Type": "application/json" } : {},
|
||||
headers: data ? { 'Content-Type': 'application/json' } : {},
|
||||
body: data ? JSON.stringify(data) : undefined,
|
||||
credentials: "include"
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
await throwIfResNotOk(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
type UnauthorizedBehavior = "returnNull" | "throw";
|
||||
type UnauthorizedBehavior = 'returnNull' | 'throw';
|
||||
export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> =
|
||||
({ on401: unauthorizedBehavior }) =>
|
||||
async ({ queryKey }) => {
|
||||
const res = await fetch(queryKey[0] as string, {
|
||||
credentials: "include"
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
|
||||
if (unauthorizedBehavior === 'returnNull' && res.status === 401) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -42,14 +38,14 @@ export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryF
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
queryFn: getQueryFn({ on401: "throw" }),
|
||||
queryFn: getQueryFn({ on401: 'throw' }),
|
||||
refetchInterval: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
retry: false
|
||||
retry: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: false
|
||||
}
|
||||
}
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -2,17 +2,17 @@
|
||||
* Format seconds to HH:MM:SS.mmm format with millisecond precision
|
||||
*/
|
||||
export const formatDetailedTime = (seconds: number): string => {
|
||||
if (isNaN(seconds)) return "00:00:00.000";
|
||||
if (isNaN(seconds)) return '00:00:00.000';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
const milliseconds = Math.round((seconds % 1) * 1000);
|
||||
|
||||
const formattedHours = String(hours).padStart(2, "0");
|
||||
const formattedMinutes = String(minutes).padStart(2, "0");
|
||||
const formattedSeconds = String(remainingSeconds).padStart(2, "0");
|
||||
const formattedMilliseconds = String(milliseconds).padStart(3, "0");
|
||||
const formattedHours = String(hours).padStart(2, '0');
|
||||
const formattedMinutes = String(minutes).padStart(2, '0');
|
||||
const formattedSeconds = String(remainingSeconds).padStart(2, '0');
|
||||
const formattedMilliseconds = String(milliseconds).padStart(3, '0');
|
||||
|
||||
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`;
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
||||
@ -20,17 +20,14 @@ export const generateSolidColor = (time: number, duration: number): string => {
|
||||
* Legacy function kept for compatibility
|
||||
* Now returns a data URL for a solid color square instead of a video thumbnail
|
||||
*/
|
||||
export const generateThumbnail = async (
|
||||
videoElement: HTMLVideoElement,
|
||||
time: number
|
||||
): Promise<string> => {
|
||||
export const generateThumbnail = async (videoElement: HTMLVideoElement, time: number): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
// Create a small canvas for the solid color
|
||||
const canvas = document.createElement("canvas");
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 10; // Much smaller - we only need a color
|
||||
canvas.height = 10;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
// Get the solid color based on time
|
||||
const color = generateSolidColor(time, videoElement.duration);
|
||||
@ -41,7 +38,7 @@ export const generateThumbnail = async (
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
};
|
||||
|
||||
@ -22,16 +22,13 @@ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// For now, we'll use a mock API that returns a promise
|
||||
// This can be replaced with actual API calls later
|
||||
export const trimVideo = async (
|
||||
mediaId: string,
|
||||
data: TrimVideoRequest
|
||||
): Promise<TrimVideoResponse> => {
|
||||
export const trimVideo = async (mediaId: string, data: TrimVideoRequest): Promise<TrimVideoResponse> => {
|
||||
try {
|
||||
// Attempt the real API call
|
||||
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data)
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@ -43,17 +40,17 @@ export const trimVideo = async (
|
||||
const errorData = await response.json();
|
||||
return {
|
||||
status: 400,
|
||||
error: errorData.error || "An error occurred during processing",
|
||||
msg: "Video Processing Error",
|
||||
url_redirect: ""
|
||||
error: errorData.error || 'An error occurred during processing',
|
||||
msg: 'Video Processing Error',
|
||||
url_redirect: '',
|
||||
};
|
||||
} catch (parseError) {
|
||||
// If can't parse response JSON, return generic error
|
||||
return {
|
||||
status: 400,
|
||||
error: "An error occurred during video processing",
|
||||
msg: "Video Processing Error",
|
||||
url_redirect: ""
|
||||
error: 'An error occurred during video processing',
|
||||
msg: 'Video Processing Error',
|
||||
url_redirect: '',
|
||||
};
|
||||
}
|
||||
} else if (response.status !== 404) {
|
||||
@ -63,17 +60,17 @@ export const trimVideo = async (
|
||||
const errorData = await response.json();
|
||||
return {
|
||||
status: response.status,
|
||||
error: errorData.error || "An error occurred during processing",
|
||||
msg: "Video Processing Error",
|
||||
url_redirect: ""
|
||||
error: errorData.error || 'An error occurred during processing',
|
||||
msg: 'Video Processing Error',
|
||||
url_redirect: '',
|
||||
};
|
||||
} catch (parseError) {
|
||||
// If can't parse response JSON, return generic error
|
||||
return {
|
||||
status: response.status,
|
||||
error: "An error occurred during video processing",
|
||||
msg: "Video Processing Error",
|
||||
url_redirect: ""
|
||||
error: 'An error occurred during video processing',
|
||||
msg: 'Video Processing Error',
|
||||
url_redirect: '',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@ -81,8 +78,8 @@ export const trimVideo = async (
|
||||
await delay(1500); // Simulate 1.5 second server delay
|
||||
return {
|
||||
status: 200, // Mock success status
|
||||
msg: "Video Processed Successfully", // Updated per requirements
|
||||
url_redirect: `./view?m=${mediaId}`
|
||||
msg: 'Video Processed Successfully', // Updated per requirements
|
||||
url_redirect: `./view?m=${mediaId}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -91,17 +88,17 @@ export const trimVideo = async (
|
||||
const jsonResponse = await response.json();
|
||||
return {
|
||||
status: 200,
|
||||
msg: "Video Processed Successfully", // Ensure the success message is correct
|
||||
msg: 'Video Processed Successfully', // Ensure the success message is correct
|
||||
url_redirect: jsonResponse.url_redirect || `./view?m=${mediaId}`,
|
||||
...jsonResponse
|
||||
...jsonResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
// For any fetch errors, return mock success response with delay
|
||||
await delay(1500); // Simulate 1.5 second server delay
|
||||
return {
|
||||
status: 200, // Mock success status
|
||||
msg: "Video Processed Successfully", // Consistent with requirements
|
||||
url_redirect: `./view?m=${mediaId}`
|
||||
msg: 'Video Processed Successfully', // Consistent with requirements
|
||||
url_redirect: `./view?m=${mediaId}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
12
frontend/package-lock.json
generated
12
frontend/package-lock.json
generated
@ -3788,9 +3788,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
@ -12590,9 +12590,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.22.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
|
||||
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
5595
frontend/yarn.lock
5595
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user