fix: Disable Segment Tools and Reset Preview State During Playback (#1305)

* fix: Disable Segment Tools and Reset Preview State During Playback

* chore: remove some unnecessary comments

* chore: build assets

* fix: do not display the handles (left/right) on preview mode

* fix: Disable all tools on preview mode (undo, redo, reset, etc.)

* Update README.md

* feat: Prettier configuration for video editor

* Update README.md

* Update .prettierrc

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

* fix: During segments playback mode, disable button interactions but keep hover working

* feat: Add yarn format

* prettier format

* Update package.json

* feat: Install prettier and improve formatting

* build assets

* Update version.py 6.2.0
This commit is contained in:
Yiannis Christodoulou 2025-07-01 15:33:39 +03:00 committed by GitHub
parent 83f3eec940
commit 4f1c4a2b4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 2959 additions and 2305 deletions

4
.gitignore vendored
View File

@ -25,3 +25,7 @@ yt.readme.md
frontend-tools/.DS_Store
static/video_editor/videos/sample-video-30s.mp4
static/video_editor/videos/sample-video-37s.mp4
/frontend-tools/video-editor-v2
.DS_Store
static/video_editor/videos/sample-video-10m.mp4
static/video_editor/videos/sample-video-10s.mp4

View File

@ -1 +1 @@
VERSION = "6.1.0"
VERSION = "6.2.0"

View File

@ -10,3 +10,6 @@ client/public/videos/sample-video-30s.mp4
client/public/videos/sample-video-37s.mp4
videos/sample-video-37s.mp4
client/public/videos/sample-video-30s.mp4
client/public/videos/sample-video-1.mp4
client/public/videos/sample-video-10m.mp4
client/public/videos/sample-video-10s.mp4

View File

@ -1 +0,0 @@
*

View File

@ -0,0 +1,22 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"trailingComma": "none",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto",
"overrides": [
{
"files": ["*.css", "*.scss"],
"options": {
"singleQuote": false
}
}
]
}

View File

@ -0,0 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"prettier.configPath": ".prettierrc"
}

View File

@ -129,3 +129,43 @@ npm run deploy
## API Integration
The video editor interfaces with MediaCMS through a set of API endpoints for retrieving and saving video edits.
Sure! Here's your updated `README.md` section with a new **"Code Formatting"** section using Prettier. I placed it after the "Development" section to keep the flow logical:
---
## Code Formatting
To automatically format all source files using [Prettier](https://prettier.io):
```bash
# Format all code in the src directory
npx prettier --write src/
```
Or for specific file types:
```bash
cd frontend-tools/video-editor/
npx prettier --write "client/src/**/*.{js,jsx,ts,tsx,json,css,scss,md}"
```
You can also add this as a script in `package.json`:
```json
"scripts": {
"format": "prettier --write client/src/"
}
```
Then run:
```bash
yarn format
# or
npm run format
```
---
Let me know if you'd like to auto-format on commit using `lint-staged` + `husky`.

View File

@ -16,7 +16,6 @@ const App = () => {
isPlaying,
setIsPlaying,
isMuted,
isPreviewMode,
thumbnails,
trimStart,
trimEnd,
@ -34,7 +33,6 @@ const App = () => {
handleReset,
handleUndo,
handleRedo,
handlePreview,
toggleMute,
handleSave,
handleSaveACopy,
@ -43,7 +41,7 @@ const App = () => {
videoInitialized,
setVideoInitialized,
isPlayingSegments,
handlePlaySegments,
handlePlaySegments
} = useVideoTrimmer();
// Function to play from the beginning
@ -92,7 +90,7 @@ const App = () => {
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
// 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 segEndTime = Number(seg.endTime.toFixed(6));
@ -114,7 +112,7 @@ const App = () => {
// If we're not in a segment, find the next segment
if (!currentSegment) {
nextSegment = sortedSegments.find(seg => {
nextSegment = sortedSegments.find((seg) => {
const segStartTime = Number(seg.startTime.toFixed(6));
return segStartTime > currentPosition;
});
@ -173,24 +171,32 @@ const App = () => {
setTimeout(setExactPosition, 50); // Final verification
// Remove our boundary checker
video.removeEventListener('timeupdate', checkBoundary);
video.removeEventListener("timeupdate", checkBoundary);
setIsPlaying(false);
// Log the final position for debugging
logger.debug("Stopped at position:", {
target: formatDetailedTime(stopTime),
actual: formatDetailedTime(video.currentTime),
type: currentSegment ? "segment end" : (nextSegment ? "next segment start" : "end of video"),
segment: currentSegment ? {
type: currentSegment
? "segment end"
: nextSegment
? "next segment start"
: "end of video",
segment: currentSegment
? {
id: currentSegment.id,
start: formatDetailedTime(currentSegment.startTime),
end: formatDetailedTime(currentSegment.endTime)
} : null,
nextSegment: nextSegment ? {
}
: null,
nextSegment: nextSegment
? {
id: nextSegment.id,
start: formatDetailedTime(nextSegment.startTime),
end: formatDetailedTime(nextSegment.endTime)
} : null
}
: null
});
return;
@ -198,39 +204,41 @@ const App = () => {
};
// Start our boundary checker
video.addEventListener('timeupdate', checkBoundary);
video.addEventListener("timeupdate", checkBoundary);
// Start playing
video.play()
video
.play()
.then(() => {
setIsPlaying(true);
setVideoInitialized(true);
logger.debug("Playback started:", {
from: formatDetailedTime(currentPosition),
to: formatDetailedTime(stopTime),
currentSegment: currentSegment ? {
currentSegment: currentSegment
? {
id: currentSegment.id,
start: formatDetailedTime(currentSegment.startTime),
end: formatDetailedTime(currentSegment.endTime)
} : 'None',
nextSegment: nextSegment ? {
}
: "None",
nextSegment: nextSegment
? {
id: nextSegment.id,
start: formatDetailedTime(nextSegment.startTime),
end: formatDetailedTime(nextSegment.endTime)
} : 'None'
}
: "None"
});
})
.catch(err => {
.catch((err) => {
console.error("Error playing video:", err);
});
};
return (
<div className="bg-background min-h-screen">
<MobilePlayPrompt
videoRef={videoRef}
onPlay={handlePlay}
/>
<MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />
<div className="container mx-auto px-4 py-6 max-w-6xl">
{/* Video Player */}
@ -251,10 +259,8 @@ const App = () => {
onReset={handleReset}
onUndo={handleUndo}
onRedo={handleRedo}
onPreview={handlePreview}
onPlaySegments={handlePlaySegments}
onPlay={handlePlay}
isPreviewMode={isPreviewMode}
isPlaying={isPlaying}
isPlayingSegments={isPlayingSegments}
canUndo={historyPosition > 0}
@ -279,7 +285,6 @@ const App = () => {
onSave={handleSave}
onSaveACopy={handleSaveACopy}
onSaveSegments={handleSaveSegments}
isPreviewMode={isPreviewMode}
hasUnsavedChanges={hasUnsavedChanges}
isIOSUninitialized={isMobile && !videoInitialized}
isPlaying={isPlaying}

View File

@ -1,5 +1,5 @@
import { formatTime, formatLongTime } from "@/lib/timeUtils";
import '../styles/ClipSegments.css';
import "../styles/ClipSegments.css";
export interface Segment {
id: number;
@ -20,7 +20,7 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
// Handle delete segment click
const handleDeleteSegment = (segmentId: number) => {
// Create and dispatch the delete event
const deleteEvent = new CustomEvent('delete-segment', {
const deleteEvent = new CustomEvent("delete-segment", {
detail: { segmentId }
});
document.dispatchEvent(deleteEvent);
@ -38,19 +38,14 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
<h3 className="clip-segments-title">Clip Segments</h3>
{sortedSegments.map((segment, index) => (
<div
key={segment.id}
className={`segment-item ${getSegmentColorClass(index)}`}
>
<div key={segment.id} className={`segment-item ${getSegmentColorClass(index)}`}>
<div className="segment-content">
<div
className="segment-thumbnail"
style={{ backgroundImage: `url(${segment.thumbnail})` }}
></div>
<div className="segment-info">
<div className="segment-title">
Segment {index + 1}
</div>
<div className="segment-title">Segment {index + 1}</div>
<div className="segment-time">
{formatTime(segment.startTime)} - {formatTime(segment.endTime)}
</div>
@ -67,7 +62,11 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
onClick={() => handleDeleteSegment(segment.id)}
>
<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>
</button>
</div>

View File

@ -1,17 +1,15 @@
import '../styles/EditingTools.css';
import { useEffect, useState } from 'react';
import "../styles/EditingTools.css";
import { useEffect, useState } from "react";
interface EditingToolsProps {
onSplit: () => void;
onReset: () => void;
onUndo: () => void;
onRedo: () => void;
onPreview: () => void;
onPlaySegments: () => void;
onPlay: () => void;
canUndo: boolean;
canRedo: boolean;
isPreviewMode?: boolean;
isPlaying?: boolean;
isPlayingSegments?: boolean;
}
@ -21,14 +19,12 @@ const EditingTools = ({
onReset,
onUndo,
onRedo,
onPreview,
onPlaySegments,
onPlay,
canUndo,
canRedo,
isPreviewMode = false,
isPlaying = false,
isPlayingSegments = false,
isPlayingSegments = false
}: EditingToolsProps) => {
const [isSmallScreen, setIsSmallScreen] = useState(false);
@ -38,14 +34,14 @@ 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') {
if (typeof window !== "undefined") {
console.log("Play button clicked, current lastSeekedPosition:", window.lastSeekedPosition);
}
@ -62,12 +58,22 @@ const EditingTools = ({
<button
className={`button segments-button`}
onClick={onPlaySegments}
data-tooltip={isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow"}
style={{ fontSize: '0.875rem' }}
data-tooltip={
isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow"
}
style={{ fontSize: "0.875rem" }}
>
{isPlayingSegments ? (
<>
<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"
>
<circle cx="12" cy="12" r="10" />
<line x1="10" y1="15" x2="10" y2="9" />
<line x1="14" y1="15" x2="14" y2="9" />
@ -77,7 +83,15 @@ const EditingTools = ({
</>
) : (
<>
<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"
>
<circle cx="12" cy="12" r="10" />
<polygon points="10 8 16 12 10 16 10 8" />
</svg>
@ -116,18 +130,26 @@ const EditingTools = ({
)}
</button> */}
{/* Standard Play button (only shown when not in preview mode or segments playback) */}
{!isPreviewMode && (!isPlayingSegments || !isSmallScreen) && (
{/* 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' }}
style={{ fontSize: "0.875rem" }}
disabled={isPlayingSegments}
>
{isPlaying ? (
{isPlaying && !isPlayingSegments ? (
<>
<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"
>
<circle cx="12" cy="12" r="10" />
<line x1="10" y1="15" x2="10" y2="9" />
<line x1="14" y1="15" x2="14" y2="9" />
@ -137,7 +159,15 @@ const EditingTools = ({
</>
) : (
<>
<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"
>
<circle cx="12" cy="12" r="10" />
<polygon points="10 8 16 12 10 16 10 8" />
</svg>
@ -178,26 +208,42 @@ const EditingTools = ({
<button
className="button"
aria-label="Undo"
data-tooltip="Undo last action"
disabled={!canUndo}
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Undo last action"}
disabled={!canUndo || isPlayingSegments}
onClick={onUndo}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 14 4 9l5-5"/>
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9 14 4 9l5-5" />
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11" />
</svg>
<span className="button-text">Undo</span>
</button>
<button
className="button"
aria-label="Redo"
data-tooltip="Redo last undone action"
disabled={!canRedo}
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Redo last undone action"}
disabled={!canRedo || isPlayingSegments}
onClick={onRedo}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m15 14 5-5-5-5"/>
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5v0A5.5 5.5 0 0 0 9.5 20H13"/>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m15 14 5-5-5-5" />
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5v0A5.5 5.5 0 0 0 9.5 20H13" />
</svg>
<span className="button-text">Redo</span>
</button>
@ -205,10 +251,15 @@ const EditingTools = ({
<button
className="button"
onClick={onReset}
data-tooltip="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">
<path fillRule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clipRule="evenodd" />
<path
fillRule="evenodd"
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
clipRule="evenodd"
/>
</svg>
<span className="reset-text">Reset</span>
</button>

View File

@ -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>;
@ -13,7 +13,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
useEffect(() => {
const checkIsMobile = () => {
// 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
@ -31,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]);
@ -63,10 +65,7 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
</ol>
</div> */}
<button
className="mobile-play-button"
onClick={handlePlayClick}
>
<button className="mobile-play-button" onClick={handlePlayClick}>
Click to start editing...
</button>
</div>

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from "react";
import { formatTime } from "@/lib/timeUtils";
import '../styles/IOSVideoPlayer.css';
import "../styles/IOSVideoPlayer.css";
interface IOSVideoPlayerProps {
videoRef: React.RefObject<HTMLVideoElement>;
@ -8,11 +8,7 @@ interface IOSVideoPlayerProps {
duration: number;
}
const IOSVideoPlayer = ({
videoRef,
currentTime,
duration,
}: IOSVideoPlayerProps) => {
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>("");
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
@ -30,14 +26,14 @@ const IOSVideoPlayer = ({
// 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-37s.mp4");
setVideoUrl("/videos/sample-video-10m.mp4");
}
}, [videoRef]);
@ -115,12 +111,14 @@ const IOSVideoPlayer = ({
<div className="ios-video-player-container">
{/* Current Time / Duration Display */}
<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>
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={ref => setIosVideoRef(ref)}
ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md"
src={videoUrl}
controls

View File

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

View File

@ -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 logger from "../lib/logger";
import "../styles/VideoPlayer.css";
interface VideoPlayerProps {
videoRef: React.RefObject<HTMLVideoElement>;
@ -33,9 +33,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
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-37s.mp4";
const sampleVideoUrl =
(typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) ||
"/videos/sample-video-10m.mp4";
// Detect iOS device
useEffect(() => {
@ -47,8 +47,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 +57,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 +70,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 +86,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 +150,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
@ -174,7 +174,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;
}
@ -202,14 +202,14 @@ 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 +217,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
@ -234,7 +234,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;
}
@ -255,7 +255,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;
}
@ -292,24 +292,29 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
setTimeout(() => {
if (videoRef.current) {
// Try to play with proper promise handling
videoRef.current.play()
videoRef.current
.play()
.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
})
.catch(err => {
.catch((err) => {
console.error("iOS: Error playing video:", err);
});
}
}, 50);
} else {
// Normal play (non-iOS or no remembered position)
video.play()
video
.play()
.then(() => {
logger.debug("Normal: Play started successfully");
onPlayPause(); // Update parent state after successful play
})
.catch(err => {
.catch((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 */}
{isIOS && !hasInitialized && !isPlaying && (
<div className="ios-first-play-indicator">
<div className="ios-play-message">
Tap Play to initialize video controls
</div>
<div className="ios-play-message">Tap Play to initialize video controls</div>
</div>
)}
{/* 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">
@ -360,26 +363,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 && (
<div className="video-time-tooltip" style={{
<div
className="video-time-tooltip"
style={{
left: `${tooltipPosition.x}px`,
transform: 'translateX(-50%)'
}}>
transform: "translateX(-50%)"
}}
>
{formatDetailedTime(tooltipTime)}
</div>
)}
@ -396,7 +396,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
data-tooltip={isMuted ? "Unmute" : "Mute"}
>
{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>
<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>
@ -404,7 +412,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<line x1="8" y1="23" x2="16" y2="23"></line>
</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>
<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>
@ -420,7 +436,11 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
data-tooltip="Toggle fullscreen"
>
<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>
</button>
</div>

View File

@ -21,10 +21,6 @@ const useVideoTrimmer = () => {
const [isPlaying, setIsPlaying] = useState(false);
const [isMuted, setIsMuted] = useState(false);
// Preview mode state for playing only segments
const [isPreviewMode, setIsPreviewMode] = useState(false);
const [previewSegmentIndex, setPreviewSegmentIndex] = useState(0);
// Timeline state
const [thumbnails, setThumbnails] = useState<string[]>([]);
const [trimStart, setTrimStart] = useState(0);
@ -50,18 +46,21 @@ 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}`);
// Log actions in history to help debug undo/redo
const actions = history.map((state, idx) =>
`${idx}: ${state.action || 'unknown'} (segments: ${state.clipSegments.length})`
if (process.env.NODE_ENV === "development") {
console.debug(
`History state updated: ${history.length} entries, position: ${historyPosition}`
);
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
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);
}
}
@ -73,7 +72,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
@ -81,11 +80,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]);
@ -146,18 +145,12 @@ const useVideoTrimmer = () => {
};
const handlePlay = () => {
// Only update isPlaying if we're not in preview mode
if (!isPreviewMode) {
setIsPlaying(true);
setVideoInitialized(true);
}
};
const handlePause = () => {
// Only update isPlaying if we're not in preview mode
if (!isPreviewMode) {
setIsPlaying(false);
}
};
const handleEnded = () => {
@ -166,21 +159,21 @@ 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);
};
}, [isPreviewMode]);
}, []);
// Play/pause video
const playPauseVideo = () => {
@ -191,7 +184,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) {
@ -203,15 +196,16 @@ const useVideoTrimmer = () => {
video.currentTime = trimStart;
}
video.play()
video
.play()
.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 => {
.catch((err) => {
console.error("Error starting playback:", err);
setIsPlaying(false); // Reset state if play failed
});
@ -226,51 +220,25 @@ const useVideoTrimmer = () => {
// Track if the video was playing before seeking
const wasPlaying = !video.paused;
// Store current preview mode state to preserve it
const wasInPreviewMode = isPreviewMode;
// Update the video position
video.currentTime = time;
setCurrentTime(time);
// 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;
}
// Find segment at this position for preview mode playback
if (wasInPreviewMode) {
const segmentAtPosition = clipSegments.find(
seg => time >= seg.startTime && time <= seg.endTime
);
if (segmentAtPosition) {
// Update the active segment index in preview mode
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
const newSegmentIndex = orderedSegments.findIndex(seg => seg.id === segmentAtPosition.id);
if (newSegmentIndex !== -1) {
setPreviewSegmentIndex(newSegmentIndex);
}
}
}
// Resume playback in two scenarios:
// 1. If it was playing before (regular mode)
// 2. If we're in preview mode (regardless of previous state)
if (wasPlaying || wasInPreviewMode) {
// Ensure preview mode stays on if it was on before
if (wasInPreviewMode) {
setIsPreviewMode(true);
}
// Resume playback if it was playing before
if (wasPlaying) {
// Play immediately without delay
video.play()
video
.play()
.then(() => {
setIsPlaying(true); // Update state to reflect we're playing
// "Resumed playback after seeking in " + (wasInPreviewMode ? "preview" : "regular") + " mode"
})
.catch(err => {
.catch((err) => {
console.error("Error resuming playback:", err);
setIsPlaying(false);
});
@ -286,7 +254,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
@ -306,8 +274,10 @@ const useVideoTrimmer = () => {
if (!oldSeg || !newSeg) return true;
// Check if any time values changed by more than 0.001 seconds (1ms)
if (Math.abs(oldSeg.startTime - newSeg.startTime) > 0.001 ||
Math.abs(oldSeg.endTime - newSeg.endTime) > 0.001) {
if (
Math.abs(oldSeg.startTime - newSeg.startTime) > 0.001 ||
Math.abs(oldSeg.endTime - newSeg.endTime) > 0.001
) {
return true;
}
}
@ -315,7 +285,8 @@ const useVideoTrimmer = () => {
return false; // No significant changes found
};
const isSignificantChange = !lastState ||
const isSignificantChange =
!lastState ||
lastState.trimStart !== newState.trimStart ||
lastState.trimEnd !== newState.trimEnd ||
lastState.splitPoints.length !== newState.splitPoints.length ||
@ -330,7 +301,7 @@ const useVideoTrimmer = () => {
const currentPosition = historyPosition;
// 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 (currentPosition < prevHistory.length - 1) {
const newHistory = prevHistory.slice(0, currentPosition + 1);
@ -342,7 +313,7 @@ const useVideoTrimmer = () => {
});
// Update position using functional update
setHistoryPosition(prev => {
setHistoryPosition((prev) => {
const newPosition = prev + 1;
// "Saved state to history position", newPosition)
return newPosition;
@ -368,16 +339,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);
};
}, []);
@ -389,10 +360,12 @@ 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"}`);
logger.debug(
`Updating segments with action: ${actionType}, recordHistory: ${isSignificantChange ? "true" : "false"}`
);
// Update segment state immediately for UI feedback
setClipSegments(e.detail.segments);
@ -418,7 +391,7 @@ const useVideoTrimmer = () => {
const currentHistoryPosition = historyPosition;
// 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 (currentHistoryPosition < prevHistory.length - 1) {
const newHistory = prevHistory.slice(0, currentHistoryPosition + 1);
@ -430,24 +403,29 @@ const useVideoTrimmer = () => {
});
// Ensure the historyPosition is updated to the correct position
setHistoryPosition(prev => {
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)`
);
}
}
};
const handleSplitSegment = async (e: Event) => {
const customEvent = e as CustomEvent;
if (customEvent.detail &&
typeof customEvent.detail.time === 'number' &&
typeof customEvent.detail.segmentId === 'number') {
if (
customEvent.detail &&
typeof customEvent.detail.time === "number" &&
typeof customEvent.detail.segmentId === "number"
) {
// Get the time and segment ID from the event
const timeToSplit = customEvent.detail.time;
const segmentId = customEvent.detail.segmentId;
@ -456,7 +434,7 @@ const useVideoTrimmer = () => {
seekVideo(timeToSplit);
// Find the segment to split
const segmentToSplit = clipSegments.find(seg => seg.id === segmentId);
const segmentToSplit = clipSegments.find((seg) => seg.id === segmentId);
if (!segmentToSplit) return;
// Make sure the split point is within the segment
@ -468,7 +446,7 @@ const useVideoTrimmer = () => {
const newSegments = [...clipSegments];
// Remove the original segment
const segmentIndex = newSegments.findIndex(seg => seg.id === segmentId);
const segmentIndex = newSegments.findIndex((seg) => seg.id === segmentId);
if (segmentIndex === -1) return;
newSegments.splice(segmentIndex, 1);
@ -479,7 +457,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
@ -488,7 +466,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
@ -499,18 +477,18 @@ 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
const newSegments = clipSegments.filter(segment => segment.id !== segmentId);
const newSegments = clipSegments.filter((segment) => segment.id !== segmentId);
if (newSegments.length !== clipSegments.length) {
// If all segments are deleted, create a new full video segment
@ -522,7 +500,7 @@ const useVideoTrimmer = () => {
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
@ -534,179 +512,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]);
// Preview mode effect to handle playing only segments
useEffect(() => {
if (!isPreviewMode || !videoRef.current) return;
// Sort segments by start time
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
if (orderedSegments.length === 0) return;
const video = videoRef.current;
// Function to handle segment playback
const handleSegmentPlayback = () => {
if (!isPreviewMode || !video) return;
const currentSegment = orderedSegments[previewSegmentIndex];
if (!currentSegment) return;
const currentTime = video.currentTime;
// If we're before the current segment's start, jump to it
if (currentTime < currentSegment.startTime) {
video.currentTime = currentSegment.startTime;
return;
}
// If we've reached the end of the current segment
if (currentTime >= currentSegment.endTime - 0.01) { // Small threshold to ensure smooth transition
// Move to the next segment if available
if (previewSegmentIndex < orderedSegments.length - 1) {
// Play next segment
const nextSegment = orderedSegments[previewSegmentIndex + 1];
video.currentTime = nextSegment.startTime;
setPreviewSegmentIndex(previewSegmentIndex + 1);
logger.debug("Preview: Moving to next segment", {
from: formatDetailedTime(currentSegment.endTime),
to: formatDetailedTime(nextSegment.startTime),
segmentIndex: previewSegmentIndex + 1
});
} else {
// Loop back to first segment
logger.debug("Preview: Looping back to first segment");
video.currentTime = orderedSegments[0].startTime;
setPreviewSegmentIndex(0);
}
// Ensure playback continues
video.play().catch(err => {
console.error("Error continuing preview playback:", err);
});
}
};
// Add event listener for timeupdate to check segment boundaries
video.addEventListener('timeupdate', handleSegmentPlayback);
// Start playing if not already playing
if (video.paused) {
video.currentTime = orderedSegments[previewSegmentIndex].startTime;
video.play().catch(err => {
console.error("Error starting preview playback:", err);
});
}
return () => {
if (video) {
video.removeEventListener('timeupdate', handleSegmentPlayback);
}
};
}, [isPreviewMode, previewSegmentIndex, clipSegments]);
// Handle starting preview mode
const handleStartPreview = () => {
const video = videoRef.current;
if (!video || clipSegments.length === 0) return;
// If preview is already active, do nothing
if (isPreviewMode) {
return;
}
// If normal playback is happening, pause it
if (isPlaying) {
video.pause();
setIsPlaying(false);
}
// Sort segments by start time
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
if (orderedSegments.length === 0) return;
// Set the preview mode flag
setIsPreviewMode(true);
logger.debug("Entering preview mode");
// Set the first segment as the current one in the preview sequence
setPreviewSegmentIndex(0);
// Move to the start of the first segment
video.currentTime = orderedSegments[0].startTime;
};
// Handle playing/stopping preview mode
const handlePreview = () => {
const video = videoRef.current;
if (!video || clipSegments.length === 0) return;
// If preview is already active, turn it off
if (isPreviewMode) {
setIsPreviewMode(false);
// Always pause the video when exiting preview mode
video.pause();
setIsPlaying(false);
logger.debug("Exiting preview mode - video paused");
return;
}
// Sort segments by start time
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
if (orderedSegments.length === 0) return;
// Set the preview mode flag
setIsPreviewMode(true);
logger.debug("Entering preview mode");
// Set the first segment as the current one in the preview sequence
setPreviewSegmentIndex(0);
// Start preview mode by playing the first segment
video.currentTime = orderedSegments[0].startTime;
// Start playback
video.play()
.then(() => {
setIsPlaying(true);
logger.debug("Preview started successfully");
})
.catch(err => {
console.error("Error starting preview:", err);
setIsPreviewMode(false);
setIsPlaying(false);
});
};
// 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
@ -732,7 +563,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;
@ -740,7 +571,7 @@ const useVideoTrimmer = () => {
}
setClipSegments(newSegments);
saveState('create_split_points');
saveState("create_split_points");
}
};
@ -759,23 +590,29 @@ const useVideoTrimmer = () => {
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
const handleUndo = () => {
if (historyPosition > 0) {
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
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)}`
));
)
);
// Apply the previous state with deep cloning to avoid reference issues
setTrimStart(previousState.trimStart);
@ -792,12 +629,18 @@ const useVideoTrimmer = () => {
const handleRedo = () => {
if (historyPosition < history.length - 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
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)}`
));
)
);
// Apply the next state with deep cloning to avoid reference issues
setTrimStart(nextState.trimStart);
@ -820,23 +663,13 @@ const useVideoTrimmer = () => {
const video = videoRef.current;
if (!video) return;
// If in preview mode, exit it before toggling normal play
if (isPreviewMode) {
setIsPreviewMode(false);
// Don't immediately start playing when exiting preview mode
// Just update the state and return
setIsPlaying(false);
video.pause();
return;
}
if (isPlaying) {
// Pause the video
video.pause();
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);
@ -845,15 +678,16 @@ const useVideoTrimmer = () => {
}
// Play the video from current position with proper promise handling
video.play()
video
.play()
.then(() => {
setIsPlaying(true);
// Reset lastSeekedPosition after successful play
if (typeof window !== 'undefined') {
if (typeof window !== "undefined") {
window.lastSeekedPosition = 0;
}
})
.catch(err => {
.catch((err) => {
console.error("Error playing video:", err);
setIsPlaying(false); // Reset state if play failed
});
@ -877,14 +711,14 @@ const useVideoTrimmer = () => {
// Create the JSON data for saving
const saveData = {
type: "save",
segments: sortedSegments.map(segment => ({
segments: sortedSegments.map((segment) => ({
startTime: formatDetailedTime(segment.startTime),
endTime: formatDetailedTime(segment.endTime)
}))
};
// Display JSON in alert (for demonstration purposes)
if (process.env.NODE_ENV === 'development') {
if (process.env.NODE_ENV === "development") {
console.debug("Saving data:", saveData);
}
@ -892,12 +726,12 @@ const useVideoTrimmer = () => {
setHasUnsavedChanges(false);
// Debug message
if (process.env.NODE_ENV === 'development') {
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);
@ -911,14 +745,14 @@ const useVideoTrimmer = () => {
// Create the JSON data for saving as a copy
const saveData = {
type: "save_as_a_copy",
segments: sortedSegments.map(segment => ({
segments: sortedSegments.map((segment) => ({
startTime: formatDetailedTime(segment.startTime),
endTime: formatDetailedTime(segment.endTime)
}))
};
// 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);
}
@ -926,12 +760,12 @@ const useVideoTrimmer = () => {
setHasUnsavedChanges(false);
// Debug message
if (process.env.NODE_ENV === 'development') {
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
@ -942,7 +776,7 @@ const useVideoTrimmer = () => {
// Create the JSON data for saving individual segments
const saveData = {
type: "save_segments",
segments: sortedSegments.map(segment => ({
segments: sortedSegments.map((segment) => ({
name: segment.name,
startTime: formatDetailedTime(segment.startTime),
endTime: formatDetailedTime(segment.endTime)
@ -950,7 +784,7 @@ const useVideoTrimmer = () => {
};
// 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);
}
@ -961,7 +795,7 @@ const useVideoTrimmer = () => {
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
@ -973,7 +807,11 @@ 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);
const isMobile =
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);
@ -1008,7 +846,7 @@ const useVideoTrimmer = () => {
// If video is somehow paused, ensure it keeps playing
if (video.paused) {
logger.debug("Ensuring playback continues to next segment");
video.play().catch(err => {
video.play().catch((err) => {
console.error("Error continuing segment playback:", err);
});
}
@ -1017,12 +855,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) {
@ -1031,7 +869,7 @@ const useVideoTrimmer = () => {
}
return () => {
video.removeEventListener('timeupdate', handleSegmentsPlayback);
video.removeEventListener("timeupdate", handleSegmentsPlayback);
};
}, [isPlayingSegments, currentSegmentIndex, clipSegments]);
@ -1040,15 +878,20 @@ 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]);
@ -1067,10 +910,7 @@ const useVideoTrimmer = () => {
setIsPlayingSegments(true);
setCurrentSegmentIndex(0);
// Exit preview mode if active
if (isPreviewMode) {
setIsPreviewMode(false);
}
// Start segments playback
// Sort segments by start time
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
@ -1079,7 +919,7 @@ const useVideoTrimmer = () => {
video.currentTime = orderedSegments[0].startTime;
// Start playback with proper error handling
video.play().catch(err => {
video.play().catch((err) => {
console.error("Error starting segments playback:", err);
setIsPlayingSegments(false);
});
@ -1095,7 +935,6 @@ const useVideoTrimmer = () => {
isPlaying,
setIsPlaying,
isMuted,
isPreviewMode,
isPlayingSegments,
thumbnails,
trimStart,
@ -1114,7 +953,6 @@ const useVideoTrimmer = () => {
handleReset,
handleUndo,
handleRedo,
handlePreview,
handlePlaySegments,
toggleMute,
handleSave,
@ -1122,7 +960,7 @@ const useVideoTrimmer = () => {
handleSaveSegments,
isMobile,
videoInitialized,
setVideoInitialized,
setVideoInitialized
};
};

View File

@ -125,13 +125,13 @@
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 0.75rem;
background-color: #EEE; /* Very light gray background */
background-color: #eee; /* Very light gray background */
position: relative;
}
.timeline-container {
position: relative;
background-color: #EEE; /* Very light gray background */
background-color: #eee; /* Very light gray background */
height: 6rem;
width: 100%;
cursor: pointer;
@ -208,17 +208,27 @@
overflow: hidden;
cursor: grab;
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 */
z-index: 15;
}
/* 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;
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;
border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */
}
@ -315,7 +325,7 @@
input[type="range"] {
-webkit-appearance: none;
height: 6px;
background: #E0E0E0;
background: #e0e0e0;
border-radius: 3px;
}
@ -350,12 +360,14 @@ input[type="range"]::-webkit-slider-thumb {
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none;
}
[data-tooltip]::after {
content: '';
content: "";
position: absolute;
bottom: 100%;
left: 50%;
@ -366,7 +378,9 @@ input[type="range"]::-webkit-slider-thumb {
margin-bottom: 0px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none;
}
@ -464,7 +478,7 @@ button[disabled][data-tooltip]::after {
}
.segment-tooltip::after {
content: '';
content: "";
position: absolute;
bottom: -6px;
left: 50%;
@ -539,7 +553,7 @@ button[disabled][data-tooltip]::after {
}
.empty-space-tooltip::after {
content: '';
content: "";
position: absolute;
bottom: -8px;
left: 50%;
@ -617,7 +631,9 @@ button[disabled][data-tooltip]::after {
}
/* 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);
color: white;
border: none;
@ -628,7 +644,8 @@ button[disabled][data-tooltip]::after {
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);
}
@ -735,7 +752,8 @@ button[disabled][data-tooltip]::after {
font-size: 1.1rem;
}
.current-time, .duration-time {
.current-time,
.duration-time {
white-space: nowrap;
}
@ -770,7 +788,8 @@ button[disabled][data-tooltip]::after {
gap: 8px;
}
.save-button, .save-copy-button {
.save-button,
.save-copy-button {
margin-top: 8px;
width: 100%;
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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))
return twMerge(clsx(inputs));
}

View File

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

View File

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

View File

@ -18,7 +18,7 @@ interface TrimVideoResponse {
}
// 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
// This can be replaced with actual API calls later
@ -29,8 +29,8 @@ export const trimVideo = async (
try {
// Attempt the real API call
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});

View File

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

View File

@ -1,5 +1,4 @@
#video-editor-trim-root {
/* Tooltip styles - only on desktop where hover is available */
@media (hover: hover) and (pointer: fine) {
[data-tooltip] {
@ -22,13 +21,15 @@
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000;
pointer-events: none;
}
[data-tooltip]:after {
content: '';
content: "";
position: absolute;
bottom: 100%;
left: 50%;
@ -38,7 +39,9 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none;
}
@ -122,7 +125,6 @@
cursor: pointer;
min-width: auto;
/* Disabled hover effect as requested */
&:hover:not(:disabled) {
color: inherit;
}
@ -147,7 +149,8 @@
}
/* Style for play buttons with highlight effect */
.play-button, .preview-button {
.play-button,
.preview-button {
font-weight: 600;
display: flex;
align-items: center;
@ -185,7 +188,8 @@
}
/* Completely disable ALL hover effects for play buttons */
.play-button:hover:not(:disabled), .preview-button:hover:not(:disabled) {
.play-button:hover:not(:disabled),
.preview-button:hover:not(:disabled) {
/* Reset everything to prevent any changes */
color: inherit !important;
transform: none !important;
@ -194,26 +198,14 @@
background: none !important;
}
.play-button svg, .preview-button svg {
.play-button svg,
.preview-button svg {
height: 1.5rem;
width: 1.5rem;
/* Make sure SVG scales with the button but doesn't change layout */
flex-shrink: 0;
}
/* Style for the preview mode message that replaces the play button */
.preview-mode-message {
display: flex;
align-items: center;
background-color: rgba(59, 130, 246, 0.1);
color: #3b82f6;
padding: 6px 12px;
border-radius: 4px;
font-weight: 600;
font-size: 0.875rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 0.8;
@ -226,13 +218,6 @@
}
}
.preview-mode-message svg {
height: 1.25rem;
width: 1.25rem;
margin-right: 0.5rem;
color: #3b82f6;
}
/* Add responsive button text class */
.button-text {
margin-left: 0.25rem;
@ -257,7 +242,8 @@
}
/* Keep font size consistent regardless of screen size */
.preview-button, .play-button {
.preview-button,
.play-button {
font-size: 0.875rem !important;
}
}
@ -340,12 +326,6 @@
padding: 0.25rem;
}
/* Smaller preview mode message */
.preview-mode-message {
font-size: 0.8rem;
padding: 4px 8px;
}
.divider {
margin: 0 0.25rem;
}

View File

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

View File

@ -321,7 +321,7 @@
.segment-tooltip:after,
.empty-space-tooltip:after {
content: '';
content: "";
position: absolute;
bottom: -5px;
left: 50%;
@ -335,7 +335,7 @@
.segment-tooltip:before,
.empty-space-tooltip:before {
content: '';
content: "";
position: absolute;
bottom: -6px;
left: 50%;
@ -438,7 +438,7 @@
font-size: 0.875rem;
border: none;
cursor: pointer;
margin-right: 0.50rem;
margin-right: 0.5rem;
}
.time-button:hover {
@ -612,13 +612,15 @@
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000;
pointer-events: none;
}
[data-tooltip]:after {
content: '';
content: "";
position: absolute;
bottom: 100%;
left: 50%;
@ -628,7 +630,9 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none;
}
@ -669,27 +673,27 @@
}
.modal-success-icon svg {
color: #4CAF50;
color: #4caf50;
animation: fadeIn 0.5s ease-in-out;
}
.modal-error-icon svg {
color: #F44336;
color: #f44336;
animation: fadeIn 0.5s ease-in-out;
}
.success-link {
background-color: #4CAF50;
background-color: #4caf50;
color: white;
transition: background-color 0.3s;
}
.success-link:hover {
background-color: #388E3C;
background-color: #388e3c;
}
.error-message {
color: #F44336;
color: #f44336;
font-weight: 500;
}
@ -809,47 +813,18 @@
}
@keyframes pulse {
0% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
100% { opacity: 0.7; transform: scale(1); }
}
/* Preview mode styles */
.preview-mode .tooltip-action-btn {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.preview-mode .tooltip-time-btn {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
/* Timeline preview mode styles */
.timeline-container-card.preview-mode {
pointer-events: none;
}
.timeline-container-card.preview-mode .timeline-marker-head,
.timeline-container-card.preview-mode .timeline-marker-drag,
.timeline-container-card.preview-mode .clip-segment,
.timeline-container-card.preview-mode .clip-segment-handle,
.timeline-container-card.preview-mode .time-button,
.timeline-container-card.preview-mode .zoom-button,
.timeline-container-card.preview-mode .save-button,
.timeline-container-card.preview-mode .save-copy-button,
.timeline-container-card.preview-mode .save-segments-button {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
.timeline-container-card.preview-mode .clip-segment:hover {
box-shadow: none;
border-color: rgba(0, 0, 0, 0.15);
background-color: inherit !important;
0% {
opacity: 0.7;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
100% {
opacity: 0.7;
transform: scale(1);
}
}
/* Segments playback mode styles - minimal functional styling */
@ -858,19 +833,26 @@
cursor: pointer;
}
.segments-playback-mode .tooltip-action-btn.set-in,
.segments-playback-mode .tooltip-action-btn.set-out,
.segments-playback-mode .tooltip-action-btn.play-from-start {
opacity: 0.5;
pointer-events: none;
}
.segments-playback-mode .tooltip-action-btn.play,
.segments-playback-mode .tooltip-action-btn.pause {
opacity: 1;
cursor: pointer;
}
/* During segments playback mode, disable button interactions but keep hover working */
.segments-playback-mode .tooltip-time-btn[disabled],
.segments-playback-mode .tooltip-action-btn[disabled] {
opacity: 0.5 !important;
cursor: not-allowed !important;
}
/* Ensure disabled buttons still show tooltips on hover */
.segments-playback-mode [data-tooltip][disabled]:hover:before,
.segments-playback-mode [data-tooltip][disabled]:hover:after {
opacity: 1 !important;
visibility: visible !important;
}
/* Show segments playback message */
.segments-playback-message {
display: flex;

View File

@ -56,6 +56,26 @@
overflow: hidden !important;
}
/* Disabled state for time display */
.tooltip-time-display.disabled {
pointer-events: none !important;
cursor: not-allowed !important;
opacity: 0.6 !important;
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
}
/* Force disabled tooltips to show on hover for better user feedback */
.tooltip-time-btn.disabled[data-tooltip]:hover:before,
.tooltip-time-btn.disabled[data-tooltip]:hover:after,
.tooltip-action-btn.disabled[data-tooltip]:hover:before,
.tooltip-action-btn.disabled[data-tooltip]:hover:after {
opacity: 1 !important;
visibility: visible !important;
}
.tooltip-actions {
display: flex;
justify-content: space-between;
@ -100,14 +120,16 @@
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
z-index: 2500; /* High z-index */
pointer-events: none;
}
/* Triangle arrow pointing up to the button */
.tooltip-action-btn[data-tooltip]:after {
content: '';
content: "";
position: absolute;
top: 35px; /* Match the before element */
left: 50%; /* Center horizontally */
@ -119,7 +141,9 @@
margin-left: 0; /* Reset margin */
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
transition:
opacity 0.2s,
visibility 0.2s;
z-index: 2500; /* High z-index */
pointer-events: none;
}
@ -227,6 +251,43 @@
color: #9ca3af;
}
/* Ensure pause button is properly styled when disabled */
.tooltip-action-btn.pause.disabled {
color: #9ca3af !important;
opacity: 0.5;
cursor: not-allowed;
}
.tooltip-action-btn.pause.disabled:hover {
background-color: #f3f4f6 !important;
color: #9ca3af !important;
}
/* Ensure play button is properly styled when disabled */
.tooltip-action-btn.play.disabled {
color: #9ca3af !important;
opacity: 0.5;
cursor: not-allowed;
}
.tooltip-action-btn.play.disabled:hover {
background-color: #f3f4f6 !important;
color: #9ca3af !important;
}
/* Ensure time adjustment buttons are properly styled when disabled */
.tooltip-time-btn.disabled {
opacity: 0.5 !important;
cursor: not-allowed !important;
background-color: #f3f4f6 !important;
color: #9ca3af !important;
}
.tooltip-time-btn.disabled:hover {
background-color: #f3f4f6 !important;
color: #9ca3af !important;
}
/* Additional mobile optimizations */
@media (max-width: 768px) {
.two-row-tooltip {

View File

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

View File

@ -7,7 +7,8 @@
"dev": "vite",
"start": "NODE_ENV=production node dist/index.js",
"check": "tsc",
"build:django": "vite build --config vite.video-editor.config.ts --outDir ../../../static/video_editor"
"build:django": "vite build --config vite.video-editor.config.ts --outDir ../../../static/video_editor",
"format": "npx prettier --write client/src/**/*.{ts,tsx,css}"
},
"dependencies": {
"@tanstack/react-query": "^5.74.4",
@ -35,6 +36,7 @@
"autoprefixer": "^10.4.20",
"esbuild": "^0.25.0",
"postcss": "^8.4.47",
"prettier": "^3.6.0",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"vite": "^5.4.18"

View File

@ -1834,6 +1834,11 @@ postcss@^8.4.43, postcss@^8.4.47:
picocolors "^1.1.1"
source-map-js "^1.2.1"
prettier@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.0.tgz#18ec98d62cb0757a5d4eab40253ff3e6d0fc8dea"
integrity sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
@ -2087,6 +2092,7 @@ statuses@2.0.1:
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -2105,6 +2111,7 @@ string-width@^5.0.1, string-width@^5.1.2:
strip-ansi "^7.0.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long