diff --git a/.gitignore b/.gitignore index 58120f11..5ffbf747 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/cms/version.py b/cms/version.py index fd07739e..c35c3d37 100644 --- a/cms/version.py +++ b/cms/version.py @@ -1 +1 @@ -VERSION = "6.1.0" +VERSION = "6.2.0" \ No newline at end of file diff --git a/frontend-tools/video-editor/.gitignore b/frontend-tools/video-editor/.gitignore index a3da5d9f..1b13e1f8 100644 --- a/frontend-tools/video-editor/.gitignore +++ b/frontend-tools/video-editor/.gitignore @@ -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 diff --git a/frontend-tools/video-editor/.prettierignore b/frontend-tools/video-editor/.prettierignore index f59ec20a..e69de29b 100644 --- a/frontend-tools/video-editor/.prettierignore +++ b/frontend-tools/video-editor/.prettierignore @@ -1 +0,0 @@ -* \ No newline at end of file diff --git a/frontend-tools/video-editor/.prettierrc b/frontend-tools/video-editor/.prettierrc new file mode 100644 index 00000000..b2ed06a8 --- /dev/null +++ b/frontend-tools/video-editor/.prettierrc @@ -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 + } + } + ] +} diff --git a/frontend-tools/video-editor/.vscode/settings.json b/frontend-tools/video-editor/.vscode/settings.json new file mode 100644 index 00000000..13734cb4 --- /dev/null +++ b/frontend-tools/video-editor/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "prettier.configPath": ".prettierrc" +} diff --git a/frontend-tools/video-editor/README.md b/frontend-tools/video-editor/README.md index ddb91455..bbd7a80b 100644 --- a/frontend-tools/video-editor/README.md +++ b/frontend-tools/video-editor/README.md @@ -128,4 +128,44 @@ npm run deploy ## API Integration -The video editor interfaces with MediaCMS through a set of API endpoints for retrieving and saving video edits. \ No newline at end of file +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`. diff --git a/frontend-tools/video-editor/client/src/App.tsx b/frontend-tools/video-editor/client/src/App.tsx index e2f1e0be..a51f6379 100644 --- a/frontend-tools/video-editor/client/src/App.tsx +++ b/frontend-tools/video-editor/client/src/App.tsx @@ -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 @@ -71,31 +69,31 @@ const App = () => { const handlePlay = () => { if (!videoRef.current) return; - + const video = videoRef.current; - + // If already playing, just pause the video if (isPlaying) { video.pause(); setIsPlaying(false); return; } - + const currentPosition = Number(video.currentTime.toFixed(6)); // Fix to microsecond precision - + // Find the next stopping point based on current position let stopTime = duration; let currentSegment = null; let nextSegment = null; - + // Sort segments by start time to ensure correct order 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)); - + // Check if we're inside the segment if (currentPosition > segStartTime && currentPosition < segEndTime) { return true; @@ -111,15 +109,15 @@ const App = () => { } return false; }); - + // 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; }); } - + // Determine where to stop based on position if (currentSegment) { // If we're in a segment, stop at its end @@ -128,113 +126,123 @@ const App = () => { // If we're in a cutaway and there's a next segment, stop at its start stopTime = Number(nextSegment.startTime.toFixed(6)); } - + // Create a boundary checker function with high precision const checkBoundary = () => { if (!video) return; - + const currentPosition = Number(video.currentTime.toFixed(6)); const timeLeft = Number((stopTime - currentPosition).toFixed(6)); - + // If we've reached or passed the boundary if (timeLeft <= 0 || currentPosition >= stopTime) { // First pause playback video.pause(); - + // Force exact position with multiple verification attempts const setExactPosition = () => { if (!video) return; - + // Set to exact boundary time video.currentTime = stopTime; handleMobileSafeSeek(stopTime); - + const actualPosition = Number(video.currentTime.toFixed(6)); const difference = Number(Math.abs(actualPosition - stopTime).toFixed(6)); - + logger.debug("Position verification:", { target: formatDetailedTime(stopTime), actual: formatDetailedTime(actualPosition), difference: difference }); - + // If we're not exactly at the target position, try one more time if (difference > 0) { video.currentTime = stopTime; handleMobileSafeSeek(stopTime); } }; - + // Multiple attempts to ensure precision, with increasing delays setExactPosition(); - setTimeout(setExactPosition, 5); // Quick first retry + setTimeout(setExactPosition, 5); // Quick first retry setTimeout(setExactPosition, 10); // Second retry setTimeout(setExactPosition, 20); // Third retry if needed 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 ? { - id: currentSegment.id, - start: formatDetailedTime(currentSegment.startTime), - end: formatDetailedTime(currentSegment.endTime) - } : null, - nextSegment: nextSegment ? { - id: nextSegment.id, - start: formatDetailedTime(nextSegment.startTime), - end: formatDetailedTime(nextSegment.endTime) - } : null + 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 + ? { + id: nextSegment.id, + start: formatDetailedTime(nextSegment.startTime), + end: formatDetailedTime(nextSegment.endTime) + } + : null }); - + return; } }; - + // 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 ? { - id: currentSegment.id, - start: formatDetailedTime(currentSegment.startTime), - end: formatDetailedTime(currentSegment.endTime) - } : 'None', - nextSegment: nextSegment ? { - id: nextSegment.id, - start: formatDetailedTime(nextSegment.startTime), - end: formatDetailedTime(nextSegment.endTime) - } : 'None' + currentSegment: currentSegment + ? { + id: currentSegment.id, + start: formatDetailedTime(currentSegment.startTime), + end: formatDetailedTime(currentSegment.endTime) + } + : "None", + nextSegment: nextSegment + ? { + id: nextSegment.id, + start: formatDetailedTime(nextSegment.startTime), + end: formatDetailedTime(nextSegment.endTime) + } + : "None" }); }) - .catch(err => { + .catch((err) => { console.error("Error playing video:", err); }); }; return (
- - + +
{/* Video Player */} - { /> {/* Editing Tools */} - 0} @@ -262,7 +268,7 @@ const App = () => { /> {/* Timeline Controls */} - { onSave={handleSave} onSaveACopy={handleSaveACopy} onSaveSegments={handleSaveSegments} - isPreviewMode={isPreviewMode} hasUnsavedChanges={hasUnsavedChanges} isIOSUninitialized={isMobile && !videoInitialized} isPlaying={isPlaying} diff --git a/frontend-tools/video-editor/client/src/components/ClipSegments.tsx b/frontend-tools/video-editor/client/src/components/ClipSegments.tsx index f38e5a22..65b7215c 100644 --- a/frontend-tools/video-editor/client/src/components/ClipSegments.tsx +++ b/frontend-tools/video-editor/client/src/components/ClipSegments.tsx @@ -1,5 +1,5 @@ import { formatTime, formatLongTime } from "@/lib/timeUtils"; -import '../styles/ClipSegments.css'; +import "../styles/ClipSegments.css"; export interface Segment { id: number; @@ -16,41 +16,36 @@ interface ClipSegmentsProps { const ClipSegments = ({ segments }: ClipSegmentsProps) => { // Sort segments by startTime const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime); - + // 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); }; - + // Generate the same color background for a segment as shown in the timeline const getSegmentColorClass = (index: number) => { - // Return CSS class based on index modulo 8 + // Return CSS class based on index modulo 8 // This matches the CSS nth-child selectors in the timeline return `segment-default-color segment-color-${(index % 8) + 1}`; }; - + return (

Clip Segments

- + {sortedSegments.map((segment, index) => ( -
+
-
-
- Segment {index + 1} -
+
Segment {index + 1}
{formatTime(segment.startTime)} - {formatTime(segment.endTime)}
@@ -60,20 +55,24 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
-
))} - + {sortedSegments.length === 0 && (
No segments created yet. Use the split button to create segments. diff --git a/frontend-tools/video-editor/client/src/components/EditingTools.tsx b/frontend-tools/video-editor/client/src/components/EditingTools.tsx index 674b81b2..34e7b91a 100644 --- a/frontend-tools/video-editor/client/src/components/EditingTools.tsx +++ b/frontend-tools/video-editor/client/src/components/EditingTools.tsx @@ -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,17 +34,17 @@ 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); } - + // Call the original handler onPlay(); }; @@ -59,15 +55,25 @@ const EditingTools = ({ {/* Left side - Play buttons group */}
{/* Play Segments button */} - */} - {/* Standard Play button (only shown when not in preview mode or segments playback) */} - {!isPreviewMode && (!isPlayingSegments || !isSmallScreen) && ( - )} - + {/* Segments Playback message (replaces play button during segments playback) */} {/* {isPlayingSegments && !isSmallScreen && (
@@ -159,7 +189,7 @@ const EditingTools = ({ Preview Mode
)} */} - + {/* Preview mode message (replaces play button) */} {/* {isPreviewMode && (
@@ -172,43 +202,64 @@ const EditingTools = ({
)} */}
- + {/* Right side - Editing tools */}
- -
- diff --git a/frontend-tools/video-editor/client/src/components/IOSPlayPrompt.tsx b/frontend-tools/video-editor/client/src/components/IOSPlayPrompt.tsx index 3ba77642..8719730e 100644 --- a/frontend-tools/video-editor/client/src/components/IOSPlayPrompt.tsx +++ b/frontend-tools/video-editor/client/src/components/IOSPlayPrompt.tsx @@ -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; @@ -13,7 +13,9 @@ const MobilePlayPrompt: React.FC = ({ 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 = ({ videoRef, onPlay }) setIsVisible(false); }; - video.addEventListener('play', handlePlay); + video.addEventListener("play", handlePlay); return () => { - video.removeEventListener('play', handlePlay); + video.removeEventListener("play", handlePlay); }; }, [videoRef]); @@ -62,11 +64,8 @@ const MobilePlayPrompt: React.FC = ({ videoRef, onPlay })
  • Then you'll be able to use all timeline controls
  • */} - -
    @@ -74,4 +73,4 @@ const MobilePlayPrompt: React.FC = ({ videoRef, onPlay }) ); }; -export default MobilePlayPrompt; \ No newline at end of file +export default MobilePlayPrompt; diff --git a/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx b/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx index cb2bda5e..fe045628 100644 --- a/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx +++ b/frontend-tools/video-editor/client/src/components/IOSVideoPlayer.tsx @@ -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; @@ -8,14 +8,10 @@ interface IOSVideoPlayerProps { duration: number; } -const IOSVideoPlayer = ({ - videoRef, - currentTime, - duration, -}: IOSVideoPlayerProps) => { +const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => { const [videoUrl, setVideoUrl] = useState(""); const [iosVideoRef, setIosVideoRef] = useState(null); - + // Refs for hold-to-continue functionality const incrementIntervalRef = useRef(null); const decrementIntervalRef = useRef(null); @@ -27,17 +23,17 @@ const IOSVideoPlayer = ({ if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current); }; }, []); - + // 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]); @@ -61,13 +57,13 @@ const IOSVideoPlayer = ({ const startIncrement = (e: React.MouseEvent | React.TouchEvent) => { // Prevent default to avoid text selection e.preventDefault(); - + if (!iosVideoRef) return; if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current); - + // First immediate adjustment iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05); - + // Setup continuous adjustment incrementIntervalRef.current = setInterval(() => { if (iosVideoRef) { @@ -88,13 +84,13 @@ const IOSVideoPlayer = ({ const startDecrement = (e: React.MouseEvent | React.TouchEvent) => { // Prevent default to avoid text selection e.preventDefault(); - + if (!iosVideoRef) return; if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current); - + // First immediate adjustment iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05); - + // Setup continuous adjustment decrementIntervalRef.current = setInterval(() => { if (iosVideoRef) { @@ -115,12 +111,14 @@ const IOSVideoPlayer = ({
    {/* Current Time / Duration Display */}
    - {formatTime(currentTime)} / {formatTime(duration)} + + {formatTime(currentTime)} / {formatTime(duration)} +
    - + {/* iOS-optimized Video Element with Native Controls */} - + {/* iOS Video Skip Controls */}
    - -
    - + {/* iOS Fine Control Buttons */}
    - -
    - +

    This player uses native iOS controls for better compatibility with iOS devices.

    @@ -183,4 +181,4 @@ const IOSVideoPlayer = ({ ); }; -export default IOSVideoPlayer; \ No newline at end of file +export default IOSVideoPlayer; diff --git a/frontend-tools/video-editor/client/src/components/Modal.tsx b/frontend-tools/video-editor/client/src/components/Modal.tsx index 9a3ff7b4..d5f27c87 100644 --- a/frontend-tools/video-editor/client/src/components/Modal.tsx +++ b/frontend-tools/video-editor/client/src/components/Modal.tsx @@ -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,36 +9,30 @@ interface ModalProps { actions?: React.ReactNode; } -const Modal: React.FC = ({ - isOpen, - onClose, - title, - children, - actions -}) => { +const Modal: React.FC = ({ 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]); - + if (!isOpen) return null; - + // Handle click outside the modal content to close it const handleClickOutside = (event: React.MouseEvent) => { if (event.target === event.currentTarget) { @@ -48,23 +42,19 @@ const Modal: React.FC = ({ return (
    -
    e.stopPropagation()}> +
    e.stopPropagation()}>

    {title}

    -
    - -
    - {children} -
    - - {actions && ( -
    - {actions} -
    - )} + +
    {children}
    + + {actions &&
    {actions}
    }
    ); }; -export default Modal; \ No newline at end of file +export default Modal; diff --git a/frontend-tools/video-editor/client/src/components/TimelineControls.tsx b/frontend-tools/video-editor/client/src/components/TimelineControls.tsx index aaeca32c..3bf7e6fe 100644 --- a/frontend-tools/video-editor/client/src/components/TimelineControls.tsx +++ b/frontend-tools/video-editor/client/src/components/TimelineControls.tsx @@ -5,24 +5,24 @@ import { Segment } from "./ClipSegments"; import Modal from "./Modal"; import { trimVideo } from "../services/videoApi"; import logger from "../lib/logger"; -import '../styles/TimelineControls.css'; -import '../styles/TwoRowTooltip.css'; -import playIcon from '../assets/play-icon.svg'; -import pauseIcon from '../assets/pause-icon.svg'; -import playFromBeginningIcon from '../assets/play-from-beginning-icon.svg'; -import segmentEndIcon from '../assets/segment-end-new.svg'; -import segmentStartIcon from '../assets/segment-start-new.svg'; -import segmentNewStartIcon from '../assets/segment-start-new-cutaway.svg'; -import segmentNewEndIcon from '../assets/segment-end-new-cutaway.svg'; +import "../styles/TimelineControls.css"; +import "../styles/TwoRowTooltip.css"; +import playIcon from "../assets/play-icon.svg"; +import pauseIcon from "../assets/pause-icon.svg"; +import playFromBeginningIcon from "../assets/play-from-beginning-icon.svg"; +import segmentEndIcon from "../assets/segment-end-new.svg"; +import segmentStartIcon from "../assets/segment-start-new.svg"; +import segmentNewStartIcon from "../assets/segment-start-new-cutaway.svg"; +import segmentNewEndIcon from "../assets/segment-end-new-cutaway.svg"; // Add styles for the media page link const mediaPageLinkStyles = { - color: '#007bff', - textDecoration: 'none', - fontWeight: 'bold', - '&:hover': { - textDecoration: 'underline', - color: '#0056b3' + color: "#007bff", + textDecoration: "none", + fontWeight: "bold", + "&:hover": { + textDecoration: "underline", + color: "#0056b3" } } as const; @@ -43,7 +43,6 @@ interface TimelineControlsProps { onSave?: () => void; onSaveACopy?: () => void; onSaveSegments?: () => void; - isPreviewMode?: boolean; hasUnsavedChanges?: boolean; isIOSUninitialized?: boolean; isPlaying: boolean; @@ -60,19 +59,20 @@ const constrainTooltipPosition = (positionPercent: number) => { const leftTransitionEnd = 25; const rightTransitionStart = 75; const rightTransitionEnd = 100; - + let leftValue: string; let transform: string; - + if (positionPercent <= leftTransitionEnd) { // Left side: smooth transition from center to left-aligned if (positionPercent <= leftTransitionStart) { // Fully left-aligned - leftValue = '0%'; - transform = 'none'; + leftValue = "0%"; + transform = "none"; } else { // Smooth transition zone - const transitionProgress = (positionPercent - leftTransitionStart) / (leftTransitionEnd - leftTransitionStart); + const transitionProgress = + (positionPercent - leftTransitionStart) / (leftTransitionEnd - leftTransitionStart); const translateAmount = -50 * transitionProgress; // Gradually reduce from 0% to -50% leftValue = `${positionPercent}%`; transform = `translateX(${translateAmount}%)`; @@ -81,19 +81,20 @@ const constrainTooltipPosition = (positionPercent: number) => { // Right side: smooth transition from center to right-aligned if (positionPercent >= rightTransitionEnd) { // Fully right-aligned - leftValue = '100%'; - transform = 'translateX(-100%)'; + leftValue = "100%"; + transform = "translateX(-100%)"; } else { // Smooth transition zone - const transitionProgress = (positionPercent - rightTransitionStart) / (rightTransitionEnd - rightTransitionStart); - const translateAmount = -50 - (50 * transitionProgress); // Gradually change from -50% to -100% + const transitionProgress = + (positionPercent - rightTransitionStart) / (rightTransitionEnd - rightTransitionStart); + const translateAmount = -50 - 50 * transitionProgress; // Gradually change from -50% to -100% leftValue = `${positionPercent}%`; transform = `translateX(${translateAmount}%)`; } } else { // Center zone: normal centered positioning leftValue = `${positionPercent}%`; - transform = 'translateX(-50%)'; + transform = "translateX(-50%)"; } return { left: leftValue, transform }; @@ -116,13 +117,12 @@ const TimelineControls = ({ onSave, onSaveACopy, onSaveSegments, - isPreviewMode, hasUnsavedChanges = false, isIOSUninitialized = false, isPlaying, setIsPlaying, onPlayPause, // Add this prop - isPlayingSegments = false, + isPlayingSegments = false }: TimelineControlsProps) => { const timelineRef = useRef(null); const leftHandleRef = useRef(null); @@ -147,9 +147,10 @@ const TimelineControls = ({ e.stopPropagation(); // Calculate new time based on offset (positive or negative) - const newTime = offsetSeconds < 0 - ? Math.max(0, clickedTime + offsetSeconds) // For negative offsets (going back) - : Math.min(duration, clickedTime + offsetSeconds); // For positive offsets (going forward) + const newTime = + offsetSeconds < 0 + ? Math.max(0, clickedTime + offsetSeconds) // For negative offsets (going back) + : Math.min(duration, clickedTime + offsetSeconds); // For positive offsets (going forward) // Save the current playing state before seeking const wasPlaying = isPlayingSegment; @@ -181,9 +182,10 @@ const TimelineControls = ({ // Function to perform time adjustment const adjustTime = () => { // Calculate new time based on fixed offset (positive or negative) - const newTime = adjustmentValue < 0 - ? Math.max(0, lastTimeValue + adjustmentValue) // For negative offsets (going back) - : Math.min(duration, lastTimeValue + adjustmentValue); // For positive offsets (going forward) + const newTime = + adjustmentValue < 0 + ? Math.max(0, lastTimeValue + adjustmentValue) // For negative offsets (going back) + : Math.min(duration, lastTimeValue + adjustmentValue); // For positive offsets (going forward) // Update our last time value for next adjustment lastTimeValue = newTime; @@ -202,7 +204,7 @@ const TimelineControls = ({ if (timelineRef.current) { const rect = timelineRef.current.getBoundingClientRect(); const positionPercent = (newTime / duration) * 100; - const xPos = rect.left + (rect.width * (positionPercent / 100)); + const xPos = rect.left + rect.width * (positionPercent / 100); setTooltipPosition({ x: xPos, y: rect.top - 10 @@ -210,7 +212,7 @@ const TimelineControls = ({ // Find if we're in a segment at the new time const segmentAtTime = clipSegments.find( - seg => newTime >= seg.startTime && newTime <= seg.endTime + (seg) => newTime >= seg.startTime && newTime <= seg.endTime ); if (segmentAtTime) { @@ -261,16 +263,17 @@ const TimelineControls = ({ clearInterval(continuousTimer); continuousTimer = null; } - document.removeEventListener('mouseup', clearTimers); - document.removeEventListener('mouseleave', clearTimers); + document.removeEventListener("mouseup", clearTimers); + document.removeEventListener("mouseleave", clearTimers); }; - document.addEventListener('mouseup', clearTimers); - document.addEventListener('mouseleave', clearTimers); + document.addEventListener("mouseup", clearTimers); + document.addEventListener("mouseleave", clearTimers); }, onTouchStart: (e: React.TouchEvent) => { e.stopPropagation(); - e.preventDefault();21 + e.preventDefault(); + 21; // Update the initial last time value lastTimeValue = clickedTime; @@ -294,12 +297,12 @@ const TimelineControls = ({ clearInterval(continuousTimer); continuousTimer = null; } - document.removeEventListener('touchend', clearTimers); - document.removeEventListener('touchcancel', clearTimers); + document.removeEventListener("touchend", clearTimers); + document.removeEventListener("touchcancel", clearTimers); }; - document.addEventListener('touchend', clearTimers); - document.addEventListener('touchcancel', clearTimers); + document.addEventListener("touchend", clearTimers); + document.addEventListener("touchcancel", clearTimers); }, onClick: (e: React.MouseEvent) => { // This prevents the click event from firing twice @@ -336,16 +339,23 @@ const TimelineControls = ({ try { // Format segments data for API request - const segments = clipSegments.map(segment => ({ + const segments = clipSegments.map((segment) => ({ startTime: formatDetailedTime(segment.startTime), endTime: formatDetailedTime(segment.endTime) })); - const mediaId = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId || null; - const redirectURL = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.redirectURL || null; + const mediaId = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.mediaId) || null; + const redirectURL = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.redirectURL) || null; // Log the request details for debugging - logger.debug("Save request:", { mediaId, segments, saveAsCopy: false, redirectURL }); + logger.debug("Save request:", { + mediaId, + segments, + saveAsCopy: false, + redirectURL + }); const response = await trimVideo(mediaId, { segments, @@ -386,7 +396,8 @@ const TimelineControls = ({ setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : "An error occurred during processing"; + const errorMsg = + error instanceof Error ? error.message : "An error occurred during processing"; logger.debug("Save error (exception):", errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); @@ -401,16 +412,23 @@ const TimelineControls = ({ try { // Format segments data for API request - const segments = clipSegments.map(segment => ({ + const segments = clipSegments.map((segment) => ({ startTime: formatDetailedTime(segment.startTime), endTime: formatDetailedTime(segment.endTime) })); - const mediaId = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId || null; - const redirectUserMediaURL = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.redirectUserMediaURL || null; + const mediaId = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.mediaId) || null; + const redirectUserMediaURL = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.redirectUserMediaURL) || null; // Log the request details for debugging - logger.debug("Save as copy request:", { mediaId, segments, saveAsCopy: true, redirectUserMediaURL }); + logger.debug("Save as copy request:", { + mediaId, + segments, + saveAsCopy: true, + redirectUserMediaURL + }); const response = await trimVideo(mediaId, { segments, @@ -451,7 +469,8 @@ const TimelineControls = ({ setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : "An error occurred during processing"; + const errorMsg = + error instanceof Error ? error.message : "An error occurred during processing"; logger.debug("Save as copy error (exception):", errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); @@ -466,14 +485,16 @@ const TimelineControls = ({ try { // Format segments data for API request, with each segment saved as a separate file - const segments = clipSegments.map(segment => ({ + const segments = clipSegments.map((segment) => ({ startTime: formatDetailedTime(segment.startTime), endTime: formatDetailedTime(segment.endTime), name: segment.name // Include segment name for individual files })); - const mediaId = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId || null; - const redirectUserMediaURL = typeof window !== 'undefined' && (window as any).MEDIA_DATA?.redirectUserMediaURL || null; + const mediaId = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.mediaId) || null; + const redirectUserMediaURL = + (typeof window !== "undefined" && (window as any).MEDIA_DATA?.redirectUserMediaURL) || null; // Log the request details for debugging logger.debug("Save segments request:", { @@ -525,7 +546,8 @@ const TimelineControls = ({ setShowProcessingModal(false); // Set error message and show error modal - const errorMsg = error instanceof Error ? error.message : "An error occurred during processing"; + const errorMsg = + error instanceof Error ? error.message : "An error occurred during processing"; logger.debug("Save segments error (exception):", errorMsg); setErrorMessage(errorMsg); setShowErrorModal(true); @@ -546,7 +568,7 @@ const TimelineControls = ({ // Smooth scroll to the desired position scrollContainerRef.current.scrollTo({ left: desiredScrollPosition, - behavior: 'smooth' + behavior: "smooth" }); // Update tooltip position to stay with the marker @@ -555,7 +577,7 @@ const TimelineControls = ({ // Calculate the visible position of the marker after scrolling const containerRect = scrollContainerRef.current.getBoundingClientRect(); const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - const markerX = visibleTimelineLeft + (currentTimePercent / 100 * rect.width); + const markerX = visibleTimelineLeft + (currentTimePercent / 100) * rect.width; // Only update if we have a tooltip showing if (selectedSegmentId !== null || showEmptySpaceTooltip) { @@ -566,18 +588,25 @@ const TimelineControls = ({ setClickedTime(currentTime); } } - }, [currentTime, zoomLevel, duration, selectedSegmentId, showEmptySpaceTooltip, currentTimePercent]); + }, [ + currentTime, + zoomLevel, + duration, + selectedSegmentId, + showEmptySpaceTooltip, + currentTimePercent + ]); // Effect to check active segment boundaries during playback useEffect(() => { // Skip if no video or no active segment const video = videoRef.current; - if (!video || !activeSegment || !isPlayingSegment || isPreviewMode) { + if (!video || !activeSegment || !isPlayingSegment) { // Log why we're skipping if (!video) logger.debug("Skipping segment boundary check - no video element"); else if (!activeSegment) logger.debug("Skipping segment boundary check - no active segment"); - else if (!isPlayingSegment) logger.debug("Skipping segment boundary check - not in segment playback mode"); - else if (isPreviewMode) logger.debug("Skipping segment boundary check in preview mode"); + else if (!isPlayingSegment) + logger.debug("Skipping segment boundary check - not in segment playback mode"); return; } @@ -587,10 +616,13 @@ const TimelineControls = ({ return; } - logger.debug("Segment boundary check ACTIVATED for segment:", + logger.debug( + "Segment boundary check ACTIVATED for segment:", activeSegment.id, - "Start:", formatDetailedTime(activeSegment.startTime), - "End:", formatDetailedTime(activeSegment.endTime) + "Start:", + formatDetailedTime(activeSegment.startTime), + "End:", + formatDetailedTime(activeSegment.endTime) ); const handleTimeUpdate = () => { @@ -598,11 +630,15 @@ const TimelineControls = ({ // Log every second to show we're actually checking if (Math.round(timeLeft * 10) % 10 === 0) { - logger.debug("Segment playback - time remaining:", + logger.debug( + "Segment playback - time remaining:", formatDetailedTime(timeLeft), - "Current:", formatDetailedTime(video.currentTime), - "End:", formatDetailedTime(activeSegment.endTime), - "ContinuePastBoundary:", continuePastBoundary + "Current:", + formatDetailedTime(video.currentTime), + "End:", + formatDetailedTime(activeSegment.endTime), + "ContinuePastBoundary:", + continuePastBoundary ); } @@ -613,7 +649,10 @@ const TimelineControls = ({ setIsPlayingSegment(false); // Reset continuePastBoundary when stopping at boundary setContinuePastBoundary(false); - logger.debug("Passed segment end - setting back to exact boundary:", formatDetailedTime(activeSegment.endTime)); + logger.debug( + "Passed segment end - setting back to exact boundary:", + formatDetailedTime(activeSegment.endTime) + ); return; } @@ -626,11 +665,14 @@ const TimelineControls = ({ video.pause(); video.currentTime = activeSegment.endTime; setIsPlayingSegment(false); - logger.debug("Paused at segment end boundary:", formatDetailedTime(activeSegment.endTime)); + logger.debug( + "Paused at segment end boundary:", + formatDetailedTime(activeSegment.endTime) + ); // Look for the next segment after this one (for potential continuation) const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const nextSegment = sortedSegments.find(seg => seg.startTime > activeSegment.endTime); + const nextSegment = sortedSegments.find((seg) => seg.startTime > activeSegment.endTime); // If there's a next segment immediately after this one, update the tooltip to show that segment if (nextSegment && Math.abs(nextSegment.startTime - activeSegment.endTime) < 0.1) { @@ -643,7 +685,10 @@ const TimelineControls = ({ } } else { // We're continuing past the boundary - logger.debug("Continuing past segment boundary:", formatDetailedTime(activeSegment.endTime)); + logger.debug( + "Continuing past segment boundary:", + formatDetailedTime(activeSegment.endTime) + ); // Reset the flag after we've passed the boundary to ensure we stop at the next boundary if (video.currentTime > activeSegment.endTime) { @@ -651,20 +696,20 @@ const TimelineControls = ({ logger.debug("Past segment boundary - resetting continuePastBoundary flag"); // Remove the active segment to avoid boundary checking until next segment is activated setActiveSegment(null); - sessionStorage.removeItem('continuingPastSegment'); + sessionStorage.removeItem("continuingPastSegment"); } } } }; // Add event listener for timeupdate to check segment boundaries - video.addEventListener('timeupdate', handleTimeUpdate); + video.addEventListener("timeupdate", handleTimeUpdate); return () => { - video.removeEventListener('timeupdate', handleTimeUpdate); + video.removeEventListener("timeupdate", handleTimeUpdate); logger.debug("Segment boundary check DEACTIVATED"); }; - }, [activeSegment, isPlayingSegment, isPreviewMode, continuePastBoundary, clipSegments]); + }, [activeSegment, isPlayingSegment, continuePastBoundary, clipSegments]); // Update display time and check for transitions between segments and empty spaces useEffect(() => { @@ -687,7 +732,7 @@ const TimelineControls = ({ // Check if we're in any segment at current time const segmentAtCurrentTime = clipSegments.find( - seg => currentTime >= seg.startTime && currentTime <= seg.endTime + (seg) => currentTime >= seg.startTime && currentTime <= seg.endTime ); // Update tooltip position based on current time percentage @@ -702,7 +747,7 @@ const TimelineControls = ({ } // Check for the special "continue past segment" state in sessionStorage - const isContinuingPastSegment = sessionStorage.getItem('continuingPastSegment') === 'true'; + const isContinuingPastSegment = sessionStorage.getItem("continuingPastSegment") === "true"; // If we're in a segment now if (segmentAtCurrentTime) { @@ -716,24 +761,34 @@ const TimelineControls = ({ // If the active segment is different from the current segment and it's not a virtual segment // and we're not in "continue past boundary" mode, set this segment as the active segment - if (activeSegment?.id !== segmentAtCurrentTime.id && - !isPlayingVirtualSegment && - !isContinuingPastSegment && - !continuePastBoundary) { + if ( + activeSegment?.id !== segmentAtCurrentTime.id && + !isPlayingVirtualSegment && + !isContinuingPastSegment && + !continuePastBoundary + ) { // We've entered a new segment during normal playback - logger.debug(`Entered a new segment during playback: ${segmentAtCurrentTime.id}, setting as active`); + logger.debug( + `Entered a new segment during playback: ${segmentAtCurrentTime.id}, setting as active` + ); setActiveSegment(segmentAtCurrentTime); setSelectedSegmentId(segmentAtCurrentTime.id); setShowEmptySpaceTooltip(false); // Reset continuation flags to ensure boundary detection works for this new segment setContinuePastBoundary(false); - sessionStorage.removeItem('continuingPastSegment'); + sessionStorage.removeItem("continuingPastSegment"); } // If we're playing a virtual segment and enter a real segment, we've reached our boundary // We should stop playback if (isPlayingVirtualSegment && video && segmentAtCurrentTime) { - logger.debug(`CUTAWAY BOUNDARY REACHED: Current position ${formatDetailedTime(video.currentTime)} at segment ${segmentAtCurrentTime.id} - STOPPING at boundary ${formatDetailedTime(segmentAtCurrentTime.startTime)}`); + logger.debug( + `CUTAWAY BOUNDARY REACHED: Current position ${formatDetailedTime( + video.currentTime + )} at segment ${segmentAtCurrentTime.id} - STOPPING at boundary ${formatDetailedTime( + segmentAtCurrentTime.startTime + )}` + ); video.pause(); // Force exact time position with high precision and multiple attempts setTimeout(() => { @@ -766,28 +821,32 @@ const TimelineControls = ({ setDisplayTime(segmentAtCurrentTime.startTime); setClickedTime(segmentAtCurrentTime.startTime); - logger.debug(`Position corrected to exact segment boundary: ${formatDetailedTime(videoRef.current.currentTime)} (target: ${formatDetailedTime(segmentAtCurrentTime.startTime)})`); + logger.debug( + `Position corrected to exact segment boundary: ${formatDetailedTime( + videoRef.current.currentTime + )} (target: ${formatDetailedTime(segmentAtCurrentTime.startTime)})` + ); } }; // Apply multiple correction attempts with increasing delays - setTimeout(verifyPosition, 10); // Immediate correction - setTimeout(verifyPosition, 20); // First correction - setTimeout(verifyPosition, 50); // Second correction - setTimeout(verifyPosition, 100); // Third correction - setTimeout(verifyPosition, 200); // Final correction + setTimeout(verifyPosition, 10); // Immediate correction + setTimeout(verifyPosition, 20); // First correction + setTimeout(verifyPosition, 50); // Second correction + setTimeout(verifyPosition, 100); // Third correction + setTimeout(verifyPosition, 200); // Final correction // Also add event listeners to ensure position is corrected whenever video state changes - videoRef.current.addEventListener('seeked', verifyPosition); - videoRef.current.addEventListener('canplay', verifyPosition); - videoRef.current.addEventListener('waiting', verifyPosition); + videoRef.current.addEventListener("seeked", verifyPosition); + videoRef.current.addEventListener("canplay", verifyPosition); + videoRef.current.addEventListener("waiting", verifyPosition); // Remove these event listeners after a short time setTimeout(() => { if (videoRef.current) { - videoRef.current.removeEventListener('seeked', verifyPosition); - videoRef.current.removeEventListener('canplay', verifyPosition); - videoRef.current.removeEventListener('waiting', verifyPosition); + videoRef.current.removeEventListener("seeked", verifyPosition); + videoRef.current.removeEventListener("canplay", verifyPosition); + videoRef.current.removeEventListener("waiting", verifyPosition); } }, 300); } @@ -809,18 +868,23 @@ const TimelineControls = ({ // or playing a cutaway area // Just update the tooltip, but don't reactivate boundary checking if (selectedSegmentId !== segmentAtCurrentTime.id || showEmptySpaceTooltip) { - logger.debug("Tooltip updated for segment during continued playback:", segmentAtCurrentTime.id, - isPlayingVirtualSegment ? "(cutaway playback - keeping virtual segment)" : ""); + logger.debug( + "Tooltip updated for segment during continued playback:", + segmentAtCurrentTime.id, + isPlayingVirtualSegment ? "(cutaway playback - keeping virtual segment)" : "" + ); setSelectedSegmentId(segmentAtCurrentTime.id); setShowEmptySpaceTooltip(false); // If we're in a different segment now, clear the continuation flag // but only if it's not the same segment we were in before // AND we're not playing a cutaway area - if (!isPlayingVirtualSegment && - sessionStorage.getItem('lastSegmentId') !== segmentAtCurrentTime.id.toString()) { + if ( + !isPlayingVirtualSegment && + sessionStorage.getItem("lastSegmentId") !== segmentAtCurrentTime.id.toString() + ) { logger.debug("Moved to a different segment - ending continuation mode"); - sessionStorage.removeItem('continuingPastSegment'); + sessionStorage.removeItem("continuingPastSegment"); } } } else { @@ -832,7 +896,7 @@ const TimelineControls = ({ setShowEmptySpaceTooltip(false); // Store the current segment ID for comparison later - sessionStorage.setItem('lastSegmentId', segmentAtCurrentTime.id.toString()); + sessionStorage.setItem("lastSegmentId", segmentAtCurrentTime.id.toString()); } } } @@ -867,21 +931,21 @@ const TimelineControls = ({ logger.debug("Video paused at:", formatDetailedTime(currentTime)); } } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentTime, isPlayingSegment, activeSegment, selectedSegmentId, clipSegments]); // Close zoom dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as HTMLElement; - if (isZoomDropdownOpen && !target.closest('.zoom-dropdown-container')) { + if (isZoomDropdownOpen && !target.closest(".zoom-dropdown-container")) { setIsZoomDropdownOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); + document.addEventListener("mousedown", handleClickOutside); return () => { - document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener("mousedown", handleClickOutside); }; }, [isZoomDropdownOpen]); @@ -911,40 +975,55 @@ const TimelineControls = ({ // Use custom events to indicate drag state const createCustomEvent = (type: string) => { - return new CustomEvent('trim-handle-event', { + return new CustomEvent("trim-handle-event", { detail: { type, isStart: isLeft } }); }; // Dispatch start drag event to signal not to record history during drag - document.dispatchEvent(createCustomEvent('drag-start')); + document.dispatchEvent(createCustomEvent("drag-start")); const onMouseMove = (moveEvent: MouseEvent) => { if (!isDragging) return; const timelineWidth = timelineRect.width; - const position = Math.max(0, Math.min(1, (moveEvent.clientX - timelineRect.left) / timelineWidth)); + const position = Math.max( + 0, + Math.min(1, (moveEvent.clientX - timelineRect.left) / timelineWidth) + ); const newTime = position * duration; // Store position globally for iOS Safari - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.lastSeekedPosition = newTime; } if (isLeft) { if (newTime < trimEnd) { // Don't record in history during drag - this avoids multiple history entries - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { time: newTime, isStart: true, recordHistory: false } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: newTime, + isStart: true, + recordHistory: false + } + }) + ); finalTime = newTime; } } else { if (newTime > trimStart) { // Don't record in history during drag - this avoids multiple history entries - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { time: newTime, isStart: false, recordHistory: false } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: newTime, + isStart: false, + recordHistory: false + } + }) + ); finalTime = newTime; } } @@ -952,45 +1031,49 @@ const TimelineControls = ({ const onMouseUp = () => { isDragging = false; - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); // Now record the final position in history with action type if (isLeft) { // Final update with history recording - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { - time: finalTime, - isStart: true, - recordHistory: true, - action: 'adjust_trim_start' - } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: finalTime, + isStart: true, + recordHistory: true, + action: "adjust_trim_start" + } + }) + ); } else { - document.dispatchEvent(new CustomEvent('update-trim', { - detail: { - time: finalTime, - isStart: false, - recordHistory: true, - action: 'adjust_trim_end' - } - })); + document.dispatchEvent( + new CustomEvent("update-trim", { + detail: { + time: finalTime, + isStart: false, + recordHistory: true, + action: "adjust_trim_end" + } + }) + ); } // Dispatch end drag event - document.dispatchEvent(createCustomEvent('drag-end')); + document.dispatchEvent(createCustomEvent("drag-end")); }; - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); }; - leftHandle.addEventListener('mousedown', initDrag(true)); - rightHandle.addEventListener('mousedown', initDrag(false)); + leftHandle.addEventListener("mousedown", initDrag(true)); + rightHandle.addEventListener("mousedown", initDrag(false)); return () => { - leftHandle.removeEventListener('mousedown', initDrag(true)); - rightHandle.removeEventListener('mousedown', initDrag(false)); + leftHandle.removeEventListener("mousedown", initDrag(true)); + rightHandle.removeEventListener("mousedown", initDrag(false)); }; }, [duration, trimStart, trimEnd, onTrimStartChange, onTrimEndChange]); @@ -1014,7 +1097,7 @@ const TimelineControls = ({ className="timeline-thumbnail" style={{ width: `${100 / numSections}%`, - backgroundColor: backgroundColor, + backgroundColor: backgroundColor // Remove background image and use solid color instead }} /> @@ -1026,13 +1109,7 @@ const TimelineControls = ({ const renderSplitPoints = () => { return splitPoints.map((point, index) => { const pointPercent = (point / duration) * 100; - return ( -
    - ); + return
    ; }); }; @@ -1049,8 +1126,8 @@ const TimelineControls = ({ const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); // Find the next and previous segments - const nextSegment = sortedSegments.find(seg => seg.startTime > startTime); - const prevSegment = [...sortedSegments].reverse().find(seg => seg.endTime < startTime); + const nextSegment = sortedSegments.find((seg) => seg.startTime > startTime); + const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < startTime); // Calculate the actual available space let availableSpace; @@ -1079,7 +1156,7 @@ const TimelineControls = ({ if (!timelineRef.current) return; // Find if we're in a segment at the current position with a small tolerance - const segmentAtPosition = clipSegments.find(seg => { + const segmentAtPosition = clipSegments.find((seg) => { const isWithinSegment = currentPosition >= seg.startTime && currentPosition <= seg.endTime; const isVeryCloseToStart = Math.abs(currentPosition - seg.startTime) < 0.001; const isVeryCloseToEnd = Math.abs(currentPosition - seg.endTime) < 0.001; @@ -1088,8 +1165,8 @@ const TimelineControls = ({ // Find the next and previous segments const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const nextSegment = sortedSegments.find(seg => seg.startTime > currentPosition); - const prevSegment = [...sortedSegments].reverse().find(seg => seg.endTime < currentPosition); + const nextSegment = sortedSegments.find((seg) => seg.startTime > currentPosition); + const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < currentPosition); if (segmentAtPosition) { // We're in or exactly at a segment boundary @@ -1122,10 +1199,10 @@ const TimelineControls = ({ if (zoomLevel > 1 && scrollContainerRef.current) { // For zoomed timeline, adjust for scroll position const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - xPos = visibleTimelineLeft + (rect.width * (positionPercent / 100)); + xPos = visibleTimelineLeft + rect.width * (positionPercent / 100); } else { // For non-zoomed timeline, use simple calculation - xPos = rect.left + (rect.width * (positionPercent / 100)); + xPos = rect.left + rect.width * (positionPercent / 100); } setTooltipPosition({ @@ -1170,11 +1247,15 @@ const TimelineControls = ({ const newTime = position * duration; // Log the position for debugging - logger.debug("Timeline clicked at:", formatDetailedTime(newTime), - "distance from end:", formatDetailedTime(duration - newTime)); + logger.debug( + "Timeline clicked at:", + formatDetailedTime(newTime), + "distance from end:", + formatDetailedTime(duration - newTime) + ); // Store position globally for iOS Safari (this is critical for first-time visits) - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.lastSeekedPosition = newTime; } @@ -1186,7 +1267,7 @@ const TimelineControls = ({ setDisplayTime(newTime); // Find if we clicked in a segment with a small tolerance for boundaries - const segmentAtClickedTime = clipSegments.find(seg => { + const segmentAtClickedTime = clipSegments.find((seg) => { // Standard check for being inside a segment const isInside = newTime >= seg.startTime && newTime <= seg.endTime; // Additional checks for being exactly at the start or end boundary (with small tolerance) @@ -1208,40 +1289,44 @@ const TimelineControls = ({ // Update the current segment index if we clicked into a segment if (segmentAtClickedTime) { const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const targetSegmentIndex = orderedSegments.findIndex(seg => seg.id === segmentAtClickedTime.id); - + const targetSegmentIndex = orderedSegments.findIndex( + (seg) => seg.id === segmentAtClickedTime.id + ); + if (targetSegmentIndex !== -1) { // Dispatch a custom event to update the current segment index - const updateSegmentIndexEvent = new CustomEvent('update-segment-index', { + const updateSegmentIndexEvent = new CustomEvent("update-segment-index", { detail: { segmentIndex: targetSegmentIndex } }); document.dispatchEvent(updateSegmentIndexEvent); - logger.debug(`Segments playback mode: updating segment index to ${targetSegmentIndex} for timeline click in segment ${segmentAtClickedTime.id}`); + logger.debug( + `Segments playback mode: updating segment index to ${targetSegmentIndex} for timeline click in segment ${segmentAtClickedTime.id}` + ); } } - + logger.debug("Segments playback mode: resuming playback after timeline click"); - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Resumed segments playback after timeline seeking"); }) - .catch(err => { + .catch((err) => { console.error("Error resuming segments playback:", err); setIsPlayingSegment(false); }); } - // Resume playback in two cases (but not during segments playback): - // 1. If it was playing before (regular playback) - // 2. If we're in preview mode (regardless of previous playing state) - else if ((wasPlaying || isPreviewMode) && !isPlayingSegments) { + // Resume playback if it was playing before (but not during segments playback) + else if (wasPlaying && !isPlayingSegments) { logger.debug("Resuming playback after timeline click"); - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Resumed playback after seeking"); }) - .catch(err => { + .catch((err) => { console.error("Error resuming playback:", err); setIsPlayingSegment(false); }); @@ -1249,7 +1334,10 @@ const TimelineControls = ({ } // Only process tooltip display if clicked on the timeline background or thumbnails, not on other UI elements - if (e.target === timelineRef.current || (e.target as HTMLElement).classList.contains('timeline-thumbnail')) { + if ( + e.target === timelineRef.current || + (e.target as HTMLElement).classList.contains("timeline-thumbnail") + ) { // Check if there's a segment at the clicked position if (segmentAtClickedTime) { setSelectedSegmentId(segmentAtClickedTime.id); @@ -1268,7 +1356,7 @@ const TimelineControls = ({ // For zoomed timeline, calculate the visible position const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; const clickPosPercent = newTime / duration; - xPos = visibleTimelineLeft + (clickPosPercent * rect.width); + xPos = visibleTimelineLeft + clickPosPercent * rect.width; } else { // For 1x zoom, use the client X xPos = e.clientX; @@ -1276,7 +1364,7 @@ const TimelineControls = ({ setTooltipPosition({ x: xPos, - y: rect.top - 10 // Position tooltip above the timeline + y: rect.top - 10 // Position tooltip above the timeline }); // Always show the empty space tooltip in cutaway areas @@ -1284,8 +1372,8 @@ const TimelineControls = ({ // Log the cutaway area details const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const prevSegment = [...sortedSegments].reverse().find(seg => seg.endTime < newTime); - const nextSegment = sortedSegments.find(seg => seg.startTime > newTime); + const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < newTime); + const nextSegment = sortedSegments.find((seg) => seg.startTime > newTime); logger.debug("Clicked in cutaway area:", { position: formatDetailedTime(newTime), @@ -1298,340 +1386,384 @@ const TimelineControls = ({ }; // Handle segment resize - works with both mouse and touch events - const handleSegmentResize = (segmentId: number, isLeft: boolean) => (e: React.MouseEvent | React.TouchEvent) => { - // Remove the check that prevents interaction during preview mode - // This allows users to resize segments while previewing + const handleSegmentResize = + (segmentId: number, isLeft: boolean) => (e: React.MouseEvent | React.TouchEvent) => { + // Remove the check that prevents interaction during preview mode + // This allows users to resize segments while previewing - e.preventDefault(); - e.stopPropagation(); // Prevent triggering parent's events + e.preventDefault(); + e.stopPropagation(); // Prevent triggering parent's events - if (!timelineRef.current) return; + if (!timelineRef.current) return; - const timelineRect = timelineRef.current.getBoundingClientRect(); - const timelineWidth = timelineRect.width; + const timelineRect = timelineRef.current.getBoundingClientRect(); + const timelineWidth = timelineRect.width; - // Find the segment that's being resized - const segment = clipSegments.find(seg => seg.id === segmentId); - if (!segment) return; + // Find the segment that's being resized + const segment = clipSegments.find((seg) => seg.id === segmentId); + if (!segment) return; - const originalStartTime = segment.startTime; - const originalEndTime = segment.endTime; + const originalStartTime = segment.startTime; + const originalEndTime = segment.endTime; - // Store the original segment state to compare after dragging - const segmentBeforeDrag = {...segment}; + // Store the original segment state to compare after dragging + const segmentBeforeDrag = { ...segment }; - // Add a visual indicator that we're in resize mode (for mouse devices) - document.body.style.cursor = 'ew-resize'; + // Add a visual indicator that we're in resize mode (for mouse devices) + document.body.style.cursor = "ew-resize"; - // Add a temporary overlay to help with dragging outside the element - const overlay = document.createElement('div'); - overlay.style.position = 'fixed'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100vw'; - overlay.style.height = '100vh'; - overlay.style.zIndex = '1000'; - overlay.style.cursor = 'ew-resize'; - document.body.appendChild(overlay); + // Add a temporary overlay to help with dragging outside the element + const overlay = document.createElement("div"); + overlay.style.position = "fixed"; + overlay.style.top = "0"; + overlay.style.left = "0"; + overlay.style.width = "100vw"; + overlay.style.height = "100vh"; + overlay.style.zIndex = "1000"; + overlay.style.cursor = "ew-resize"; + document.body.appendChild(overlay); - // Track dragging state and final positions - let isDragging = true; - let finalStartTime = originalStartTime; - let finalEndTime = originalEndTime; + // Track dragging state and final positions + let isDragging = true; + let finalStartTime = originalStartTime; + let finalEndTime = originalEndTime; - // Dispatch an event to signal drag start - document.dispatchEvent(new CustomEvent('segment-drag-start', { - detail: { segmentId } - })); + // Dispatch an event to signal drag start + document.dispatchEvent( + new CustomEvent("segment-drag-start", { + detail: { segmentId } + }) + ); - // Keep the tooltip visible during drag - // Function to handle both mouse and touch movements - const handleDragMove = (clientX: number) => { - if (!isDragging || !timelineRef.current) return; + // Keep the tooltip visible during drag + // Function to handle both mouse and touch movements + const handleDragMove = (clientX: number) => { + if (!isDragging || !timelineRef.current) return; - const updatedTimelineRect = timelineRef.current.getBoundingClientRect(); - const position = Math.max(0, Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width)); - const newTime = position * duration; + const updatedTimelineRect = timelineRef.current.getBoundingClientRect(); + const position = Math.max( + 0, + Math.min(1, (clientX - updatedTimelineRect.left) / updatedTimelineRect.width) + ); + const newTime = position * duration; - // Create a temporary segment with the current drag position to check against - const draggedSegment = { - id: segmentId, - startTime: isLeft ? newTime : originalStartTime, - endTime: isLeft ? originalEndTime : newTime, - name: '', - thumbnail: '' - }; + // Create a temporary segment with the current drag position to check against + const draggedSegment = { + id: segmentId, + startTime: isLeft ? newTime : originalStartTime, + endTime: isLeft ? originalEndTime : newTime, + name: "", + thumbnail: "" + }; - // Check if the current marker position intersects with where the segment will be - const currentSegmentStart = isLeft ? newTime : originalStartTime; - const currentSegmentEnd = isLeft ? originalEndTime : newTime; - const isMarkerInSegment = currentTime >= currentSegmentStart && currentTime <= currentSegmentEnd; + // Check if the current marker position intersects with where the segment will be + const currentSegmentStart = isLeft ? newTime : originalStartTime; + const currentSegmentEnd = isLeft ? originalEndTime : newTime; + const isMarkerInSegment = + currentTime >= currentSegmentStart && currentTime <= currentSegmentEnd; - // Update tooltip based on marker intersection - if (isMarkerInSegment) { - // Show segment tooltip if marker is inside the segment - setSelectedSegmentId(segmentId); - setShowEmptySpaceTooltip(false); - } else { - // Show cutaway tooltip if marker is outside the segment - setSelectedSegmentId(null); - // Calculate available space for cutaway tooltip - const availableSpace = calculateAvailableSpace(currentTime); - setAvailableSegmentDuration(availableSpace); - setShowEmptySpaceTooltip(true); - } - - // Find neighboring segments (exclude the current one) - const otherSegments = clipSegments.filter(seg => seg.id !== segmentId); - - // Calculate new start/end times based on drag direction - let newStartTime = originalStartTime; - let newEndTime = originalEndTime; - - if (isLeft) { - // Dragging left handle - adjust start time - newStartTime = Math.min(newTime, originalEndTime - 0.5); - - // Find the closest left neighbor - const leftNeighbors = otherSegments - .filter(seg => seg.endTime <= originalStartTime) - .sort((a, b) => b.endTime - a.endTime); - - const leftNeighbor = leftNeighbors[0]; - - // Prevent overlapping with left neighbor - if (leftNeighbor && newStartTime < leftNeighbor.endTime) { - newStartTime = leftNeighbor.endTime; + // Update tooltip based on marker intersection + if (isMarkerInSegment) { + // Show segment tooltip if marker is inside the segment + setSelectedSegmentId(segmentId); + setShowEmptySpaceTooltip(false); + } else { + // Show cutaway tooltip if marker is outside the segment + setSelectedSegmentId(null); + // Calculate available space for cutaway tooltip + const availableSpace = calculateAvailableSpace(currentTime); + setAvailableSegmentDuration(availableSpace); + setShowEmptySpaceTooltip(true); } - // Snap to the nearest segment with a small threshold - const snapThreshold = 0.3; // seconds + // Find neighboring segments (exclude the current one) + const otherSegments = clipSegments.filter((seg) => seg.id !== segmentId); - if (leftNeighbor && Math.abs(newStartTime - leftNeighbor.endTime) < snapThreshold) { - newStartTime = leftNeighbor.endTime; - } + // Calculate new start/end times based on drag direction + let newStartTime = originalStartTime; + let newEndTime = originalEndTime; - // Update final value for history recording - finalStartTime = newStartTime; - } else { - // Dragging right handle - adjust end time - newEndTime = Math.max(newTime, originalStartTime + 0.5); + if (isLeft) { + // Dragging left handle - adjust start time + newStartTime = Math.min(newTime, originalEndTime - 0.5); - // Find the closest right neighbor - const rightNeighbors = otherSegments - .filter(seg => seg.startTime >= originalEndTime) - .sort((a, b) => a.startTime - b.startTime); + // Find the closest left neighbor + const leftNeighbors = otherSegments + .filter((seg) => seg.endTime <= originalStartTime) + .sort((a, b) => b.endTime - a.endTime); - const rightNeighbor = rightNeighbors[0]; + const leftNeighbor = leftNeighbors[0]; - // Prevent overlapping with right neighbor - if (rightNeighbor && newEndTime > rightNeighbor.startTime) { - newEndTime = rightNeighbor.startTime; - } - - // Snap to the nearest segment with a small threshold - const snapThreshold = 0.3; // seconds - - if (rightNeighbor && Math.abs(newEndTime - rightNeighbor.startTime) < snapThreshold) { - newEndTime = rightNeighbor.startTime; - } - - // Update final value for history recording - finalEndTime = newEndTime; - } - - // Create a new segments array with the updated segment - const updatedSegments = clipSegments.map(seg => { - if (seg.id === segmentId) { - return { - ...seg, - startTime: newStartTime, - endTime: newEndTime - }; - } - return seg; - }); - - // Create a custom event to update the segments WITHOUT recording in history during drag - const updateEvent = new CustomEvent('update-segments', { - detail: { - segments: updatedSegments, - recordHistory: false // Don't record intermediate states - } - }); - document.dispatchEvent(updateEvent); - - // During dragging, check if the current tooltip needs to be updated based on segment position - if (selectedSegmentId === segmentId && videoRef.current) { - const currentTime = videoRef.current.currentTime; - const segment = updatedSegments.find(seg => seg.id === segmentId); - - if (segment) { - // Check if playhead position is now outside the segment after dragging - const isInsideSegment = currentTime >= segment.startTime && currentTime <= segment.endTime; - - // Log the current position information for debugging - logger.debug(`During drag - playhead at ${formatDetailedTime(currentTime)} is ${isInsideSegment ? 'inside' : 'outside'} segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime(segment.endTime)})`); - - if (!isInsideSegment && isPlayingSegment) { - logger.debug("Playhead position is outside segment after dragging - updating tooltip"); - // Stop playback if we were playing and dragged the segment away from playhead - videoRef.current.pause(); - setIsPlayingSegment(false); - setActiveSegment(null); + // Prevent overlapping with left neighbor + if (leftNeighbor && newStartTime < leftNeighbor.endTime) { + newStartTime = leftNeighbor.endTime; } - // Update display time to stay in bounds of the segment - if (currentTime < segment.startTime) { - logger.debug(`Adjusting display time to segment start: ${formatDetailedTime(segment.startTime)}`); - setDisplayTime(segment.startTime); + // Snap to the nearest segment with a small threshold + const snapThreshold = 0.3; // seconds - // Update UI state to reflect that playback will be from segment start - setClickedTime(segment.startTime); - } else if (currentTime > segment.endTime) { - logger.debug(`Adjusting display time to segment end: ${formatDetailedTime(segment.endTime)}`); - setDisplayTime(segment.endTime); - - // Update UI state to reflect that playback will be from segment end - setClickedTime(segment.endTime); + if (leftNeighbor && Math.abs(newStartTime - leftNeighbor.endTime) < snapThreshold) { + newStartTime = leftNeighbor.endTime; } + + // Update final value for history recording + finalStartTime = newStartTime; + } else { + // Dragging right handle - adjust end time + newEndTime = Math.max(newTime, originalStartTime + 0.5); + + // Find the closest right neighbor + const rightNeighbors = otherSegments + .filter((seg) => seg.startTime >= originalEndTime) + .sort((a, b) => a.startTime - b.startTime); + + const rightNeighbor = rightNeighbors[0]; + + // Prevent overlapping with right neighbor + if (rightNeighbor && newEndTime > rightNeighbor.startTime) { + newEndTime = rightNeighbor.startTime; + } + + // Snap to the nearest segment with a small threshold + const snapThreshold = 0.3; // seconds + + if (rightNeighbor && Math.abs(newEndTime - rightNeighbor.startTime) < snapThreshold) { + newEndTime = rightNeighbor.startTime; + } + + // Update final value for history recording + finalEndTime = newEndTime; } - } - }; - // Function to handle the end of dragging (for both mouse and touch) - const handleDragEnd = () => { - if (!isDragging) return; + // Create a new segments array with the updated segment + const updatedSegments = clipSegments.map((seg) => { + if (seg.id === segmentId) { + return { + ...seg, + startTime: newStartTime, + endTime: newEndTime + }; + } + return seg; + }); - isDragging = false; + // Create a custom event to update the segments WITHOUT recording in history during drag + const updateEvent = new CustomEvent("update-segments", { + detail: { + segments: updatedSegments, + recordHistory: false // Don't record intermediate states + } + }); + document.dispatchEvent(updateEvent); - // Clean up event listeners for both mouse and touch - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - document.removeEventListener('touchmove', handleTouchMove); - document.removeEventListener('touchend', handleTouchEnd); - document.removeEventListener('touchcancel', handleTouchEnd); + // During dragging, check if the current tooltip needs to be updated based on segment position + if (selectedSegmentId === segmentId && videoRef.current) { + const currentTime = videoRef.current.currentTime; + const segment = updatedSegments.find((seg) => seg.id === segmentId); - // Reset styles - document.body.style.cursor = ''; - if (document.body.contains(overlay)) { - document.body.removeChild(overlay); - } + if (segment) { + // Check if playhead position is now outside the segment after dragging + const isInsideSegment = + currentTime >= segment.startTime && currentTime <= segment.endTime; - // Record the final position in history as a single action - const finalSegments = clipSegments.map(seg => { - if (seg.id === segmentId) { - return { - ...seg, - startTime: finalStartTime, - endTime: finalEndTime - }; - } - return seg; - }); + // Log the current position information for debugging + logger.debug( + `During drag - playhead at ${formatDetailedTime(currentTime)} is ${ + isInsideSegment ? "inside" : "outside" + } segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime( + segment.endTime + )})` + ); - // Now we can create a history record for the complete drag operation - const actionType = isLeft ? 'adjust_segment_start' : 'adjust_segment_end'; - document.dispatchEvent(new CustomEvent('update-segments', { - detail: { - segments: finalSegments, - recordHistory: true, - action: actionType - } - })); - - // After drag is complete, do a final check to see if playhead is inside the segment - if (selectedSegmentId === segmentId && videoRef.current) { - const currentTime = videoRef.current.currentTime; - const segment = finalSegments.find(seg => seg.id === segmentId); - - if (segment) { - const isInsideSegment = currentTime >= segment.startTime && currentTime <= segment.endTime; - - logger.debug(`Drag complete - playhead at ${formatDetailedTime(currentTime)} is ${isInsideSegment ? 'inside' : 'outside'} segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime(segment.endTime)})`); - - // Check if playhead status changed during drag - const wasInsideSegmentBefore = currentTime >= segmentBeforeDrag.startTime && currentTime <= segmentBeforeDrag.endTime; - - logger.debug(`Playhead was ${wasInsideSegmentBefore ? 'inside' : 'outside'} segment before drag, now ${isInsideSegment ? 'inside' : 'outside'}`); - - // Update UI elements based on segment position - if (!isInsideSegment) { - // If we were playing and the playhead is now outside the segment, stop playback - if (isPlayingSegment) { + if (!isInsideSegment && isPlayingSegment) { + logger.debug( + "Playhead position is outside segment after dragging - updating tooltip" + ); + // Stop playback if we were playing and dragged the segment away from playhead videoRef.current.pause(); setIsPlayingSegment(false); setActiveSegment(null); - setContinuePastBoundary(false); - logger.debug("Stopped playback because playhead is outside segment after drag completion"); } - // Update display time to be within the segment's bounds + // Update display time to stay in bounds of the segment if (currentTime < segment.startTime) { - logger.debug(`Final adjustment - setting display time to segment start: ${formatDetailedTime(segment.startTime)}`); + logger.debug( + `Adjusting display time to segment start: ${formatDetailedTime(segment.startTime)}` + ); setDisplayTime(segment.startTime); + + // Update UI state to reflect that playback will be from segment start setClickedTime(segment.startTime); } else if (currentTime > segment.endTime) { - logger.debug(`Final adjustment - setting display time to segment end: ${formatDetailedTime(segment.endTime)}`); + logger.debug( + `Adjusting display time to segment end: ${formatDetailedTime(segment.endTime)}` + ); setDisplayTime(segment.endTime); + + // Update UI state to reflect that playback will be from segment end setClickedTime(segment.endTime); } } - // Special case: playhead was outside segment before, but now it's inside - can start playback - else if (!wasInsideSegmentBefore && isInsideSegment) { - logger.debug("Playhead moved INTO segment during drag - can start playback"); - setActiveSegment(segment); - // In preview mode, we automatically start playing when playhead enters segment - if (isPreviewMode) { - videoRef.current.play() - .then(() => { - setIsPlayingSegment(true); - logger.debug("Started playback after dragging segment to include playhead"); - }) - .catch(err => { - console.error("Error starting playback:", err); - }); + } + }; + + // Function to handle the end of dragging (for both mouse and touch) + const handleDragEnd = () => { + if (!isDragging) return; + + isDragging = false; + + // Clean up event listeners for both mouse and touch + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + document.removeEventListener("touchcancel", handleTouchEnd); + + // Reset styles + document.body.style.cursor = ""; + if (document.body.contains(overlay)) { + document.body.removeChild(overlay); + } + + // Record the final position in history as a single action + const finalSegments = clipSegments.map((seg) => { + if (seg.id === segmentId) { + return { + ...seg, + startTime: finalStartTime, + endTime: finalEndTime + }; + } + return seg; + }); + + // Now we can create a history record for the complete drag operation + const actionType = isLeft ? "adjust_segment_start" : "adjust_segment_end"; + document.dispatchEvent( + new CustomEvent("update-segments", { + detail: { + segments: finalSegments, + recordHistory: true, + action: actionType + } + }) + ); + + // After drag is complete, do a final check to see if playhead is inside the segment + if (selectedSegmentId === segmentId && videoRef.current) { + const currentTime = videoRef.current.currentTime; + const segment = finalSegments.find((seg) => seg.id === segmentId); + + if (segment) { + const isInsideSegment = + currentTime >= segment.startTime && currentTime <= segment.endTime; + + logger.debug( + `Drag complete - playhead at ${formatDetailedTime(currentTime)} is ${ + isInsideSegment ? "inside" : "outside" + } segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime( + segment.endTime + )})` + ); + + // Check if playhead status changed during drag + const wasInsideSegmentBefore = + currentTime >= segmentBeforeDrag.startTime && + currentTime <= segmentBeforeDrag.endTime; + + logger.debug( + `Playhead was ${ + wasInsideSegmentBefore ? "inside" : "outside" + } segment before drag, now ${isInsideSegment ? "inside" : "outside"}` + ); + + // Update UI elements based on segment position + if (!isInsideSegment) { + // If we were playing and the playhead is now outside the segment, stop playback + if (isPlayingSegment) { + videoRef.current.pause(); + setIsPlayingSegment(false); + setActiveSegment(null); + setContinuePastBoundary(false); + logger.debug( + "Stopped playback because playhead is outside segment after drag completion" + ); + } + + // Update display time to be within the segment's bounds + if (currentTime < segment.startTime) { + logger.debug( + `Final adjustment - setting display time to segment start: ${formatDetailedTime( + segment.startTime + )}` + ); + setDisplayTime(segment.startTime); + setClickedTime(segment.startTime); + } else if (currentTime > segment.endTime) { + logger.debug( + `Final adjustment - setting display time to segment end: ${formatDetailedTime( + segment.endTime + )}` + ); + setDisplayTime(segment.endTime); + setClickedTime(segment.endTime); + } + } + // Special case: playhead was outside segment before, but now it's inside - can start playback + else if (!wasInsideSegmentBefore && isInsideSegment) { + logger.debug("Playhead moved INTO segment during drag - can start playback"); + setActiveSegment(segment); + } + // Another special case: playhead was inside segment before, but now is also inside but at a different position + else if ( + wasInsideSegmentBefore && + isInsideSegment && + (segment.startTime !== segmentBeforeDrag.startTime || + segment.endTime !== segmentBeforeDrag.endTime) + ) { + logger.debug( + "Segment boundaries changed while playhead remained inside - updating activeSegment" + ); + // Update the active segment reference to ensure boundary detection works with new bounds + setActiveSegment(segment); } } - // Another special case: playhead was inside segment before, but now is also inside but at a different position - else if (wasInsideSegmentBefore && isInsideSegment && - (segment.startTime !== segmentBeforeDrag.startTime || segment.endTime !== segmentBeforeDrag.endTime)) { - logger.debug("Segment boundaries changed while playhead remained inside - updating activeSegment"); - // Update the active segment reference to ensure boundary detection works with new bounds - setActiveSegment(segment); - } } - } - }; + }; - // Mouse-specific event handlers - const handleMouseMove = (moveEvent: MouseEvent) => { - handleDragMove(moveEvent.clientX); - }; + // Mouse-specific event handlers + const handleMouseMove = (moveEvent: MouseEvent) => { + handleDragMove(moveEvent.clientX); + }; - const handleMouseUp = () => { - handleDragEnd(); - }; + const handleMouseUp = () => { + handleDragEnd(); + }; - // Touch-specific event handlers - const handleTouchMove = (moveEvent: TouchEvent) => { - if (moveEvent.touches.length > 0) { - moveEvent.preventDefault(); // Prevent scrolling while dragging - handleDragMove(moveEvent.touches[0].clientX); - } - }; + // Touch-specific event handlers + const handleTouchMove = (moveEvent: TouchEvent) => { + if (moveEvent.touches.length > 0) { + moveEvent.preventDefault(); // Prevent scrolling while dragging + handleDragMove(moveEvent.touches[0].clientX); + } + }; - const handleTouchEnd = () => { - handleDragEnd(); - }; + const handleTouchEnd = () => { + handleDragEnd(); + }; - // Register event listeners for both mouse and touch - document.addEventListener('mousemove', handleMouseMove, { passive: false }); - document.addEventListener('mouseup', handleMouseUp); - document.addEventListener('touchmove', handleTouchMove, { passive: false }); - document.addEventListener('touchend', handleTouchEnd); - document.addEventListener('touchcancel', handleTouchEnd); - }; + // Register event listeners for both mouse and touch + document.addEventListener("mousemove", handleMouseMove, { + passive: false + }); + document.addEventListener("mouseup", handleMouseUp); + document.addEventListener("touchmove", handleTouchMove, { + passive: false + }); + document.addEventListener("touchend", handleTouchEnd); + document.addEventListener("touchcancel", handleTouchEnd); + }; // Handle segment click to show the tooltip const handleSegmentClick = (segmentId: number) => (e: React.MouseEvent) => { @@ -1639,7 +1771,7 @@ const TimelineControls = ({ // This allows users to click segments while previewing // Don't show tooltip if clicked on handle - if ((e.target as HTMLElement).classList.contains('clip-segment-handle')) { + if ((e.target as HTMLElement).classList.contains("clip-segment-handle")) { return; } @@ -1659,7 +1791,7 @@ const TimelineControls = ({ setSelectedSegmentId(segmentId); // Find the segment in our data - const segment = clipSegments.find(seg => seg.id === segmentId); + const segment = clipSegments.find((seg) => seg.id === segmentId); if (!segment) return; // Find the segment element in the DOM @@ -1670,7 +1802,7 @@ const TimelineControls = ({ const relativeX = (e.clientX - segmentRect.left) / segmentRect.width; // Convert to time based on segment's start and end times - const clickTime = segment.startTime + (relativeX * (segment.endTime - segment.startTime)); + const clickTime = segment.startTime + relativeX * (segment.endTime - segment.startTime); // Ensure time is within segment bounds const boundedTime = Math.max(segment.startTime, Math.min(segment.endTime, clickTime)); @@ -1684,22 +1816,38 @@ const TimelineControls = ({ if (videoRef.current) { const currentVideoTime = videoRef.current.currentTime; const isPlayheadInsideSegment = - currentVideoTime >= segment.startTime && - currentVideoTime <= segment.endTime; + currentVideoTime >= segment.startTime && currentVideoTime <= segment.endTime; - logger.debug(`Segment click - playhead at ${formatDetailedTime(currentVideoTime)} is ${isPlayheadInsideSegment ? 'inside' : 'outside'} segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime(segment.endTime)})`); + logger.debug( + `Segment click - playhead at ${formatDetailedTime(currentVideoTime)} is ${ + isPlayheadInsideSegment ? "inside" : "outside" + } segment (${formatDetailedTime(segment.startTime)} - ${formatDetailedTime( + segment.endTime + )})` + ); // If playhead is outside the segment, update the display time to segment boundary if (!isPlayheadInsideSegment) { // Adjust the display time based on which end is closer to the playhead - if (Math.abs(currentVideoTime - segment.startTime) < Math.abs(currentVideoTime - segment.endTime)) { + if ( + Math.abs(currentVideoTime - segment.startTime) < + Math.abs(currentVideoTime - segment.endTime) + ) { // Playhead is closer to segment start - logger.debug(`Playhead outside segment - adjusting to segment start: ${formatDetailedTime(segment.startTime)}`); + logger.debug( + `Playhead outside segment - adjusting to segment start: ${formatDetailedTime( + segment.startTime + )}` + ); setDisplayTime(segment.startTime); // Don't update clickedTime here since we already set it to the clicked position } else { // Playhead is closer to segment end - logger.debug(`Playhead outside segment - adjusting to segment end: ${formatDetailedTime(segment.endTime)}`); + logger.debug( + `Playhead outside segment - adjusting to segment end: ${formatDetailedTime( + segment.endTime + )}` + ); setDisplayTime(segment.endTime); // Don't update clickedTime here since we already set it to the clicked position } @@ -1715,57 +1863,49 @@ const TimelineControls = ({ if (isPlayingSegments && wasPlaying) { // Update the current segment index for segments playback mode const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - const targetSegmentIndex = orderedSegments.findIndex(seg => seg.id === segmentId); - + const targetSegmentIndex = orderedSegments.findIndex((seg) => seg.id === segmentId); + if (targetSegmentIndex !== -1) { // Dispatch a custom event to update the current segment index - const updateSegmentIndexEvent = new CustomEvent('update-segment-index', { + const updateSegmentIndexEvent = new CustomEvent("update-segment-index", { detail: { segmentIndex: targetSegmentIndex } }); document.dispatchEvent(updateSegmentIndexEvent); - logger.debug(`Segments playback mode: updating segment index to ${targetSegmentIndex} for segment ${segmentId}`); + logger.debug( + `Segments playback mode: updating segment index to ${targetSegmentIndex} for segment ${segmentId}` + ); } - + // In segments playback mode, we want to continue the segments playback from the new position // The segments playback will naturally handle continuing to the next segments logger.debug("Segments playback mode: continuing playback from new position"); - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Continued segments playback after segment click"); }) - .catch(err => { + .catch((err) => { console.error("Error continuing segments playback after segment click:", err); }); } - // If video was playing before OR we're in preview mode, ensure it continues playing (but not in segments mode) - else if ((wasPlaying || isPreviewMode) && !isPlayingSegments) { + // If video was playing before, ensure it continues playing (but not in segments mode) + else if (wasPlaying && !isPlayingSegments) { // Set current segment as active segment for boundary checking setActiveSegment(segment); // Reset the continuePastBoundary flag when clicking on a segment to ensure boundaries work setContinuePastBoundary(false); // Continue playing from the new position - videoRef.current.play() + videoRef.current + .play() .then(() => { setIsPlayingSegment(true); logger.debug("Continued preview playback after segment click"); }) - .catch(err => { + .catch((err) => { console.error("Error resuming playback after segment click:", err); }); } - // Always continue playback in preview mode, even if video was paused when clicking (but not in segments mode) - else if (isPreviewMode && !isPlayingSegments) { - setActiveSegment(segment); - videoRef.current.play() - .then(() => { - setIsPlayingSegment(true); - logger.debug("Continued preview playback after segment click"); - }) - .catch(err => { - console.error("Error continuing preview playback:", err); - }); - } } // Calculate tooltip position directly above click point @@ -1787,12 +1927,12 @@ const TimelineControls = ({ const clickedPosPixel = (boundedTime / duration) * timelineWidth; // Center the view on the clicked position - const targetScrollLeft = Math.max(0, clickedPosPixel - (containerWidth / 2)); + const targetScrollLeft = Math.max(0, clickedPosPixel - containerWidth / 2); // Smooth scroll to the clicked point scrollContainerRef.current.scrollTo({ left: targetScrollLeft, - behavior: 'smooth' + behavior: "smooth" }); // Update tooltip position after scrolling completes @@ -1801,7 +1941,8 @@ const TimelineControls = ({ // Calculate new position based on viewport const updatedRect = timelineRef.current.getBoundingClientRect(); const timePercent = boundedTime / duration; - const newPosition = (timePercent * timelineWidth) - scrollContainerRef.current.scrollLeft + updatedRect.left; + const newPosition = + timePercent * timelineWidth - scrollContainerRef.current.scrollLeft + updatedRect.left; setTooltipPosition({ x: newPosition, @@ -1836,76 +1977,84 @@ const TimelineControls = ({ return (
    Segment {index + 1}
    -
    {formatTime(segment.startTime)} - {formatTime(segment.endTime)}
    -
    Duration: {formatTime(segment.endTime - segment.startTime)}
    +
    + {formatTime(segment.startTime)} - {formatTime(segment.endTime)} +
    +
    + Duration: {formatTime(segment.endTime - segment.startTime)} +
    {/* Resize handles with both mouse and touch support */} -
    { - e.stopPropagation(); - handleSegmentResize(segment.id, true)(e); - }} - onTouchStart={(e) => { - e.stopPropagation(); - handleSegmentResize(segment.id, true)(e); - }} - >
    -
    { - e.stopPropagation(); - handleSegmentResize(segment.id, false)(e); - }} - onTouchStart={(e) => { - e.stopPropagation(); - handleSegmentResize(segment.id, false)(e); - }} - >
    + {isPlayingSegments ? null : ( + <> +
    { + e.stopPropagation(); + handleSegmentResize(segment.id, true)(e); + }} + onTouchStart={(e) => { + e.stopPropagation(); + handleSegmentResize(segment.id, true)(e); + }} + >
    +
    { + e.stopPropagation(); + handleSegmentResize(segment.id, false)(e); + }} + onTouchStart={(e) => { + e.stopPropagation(); + handleSegmentResize(segment.id, false)(e); + }} + >
    + + )}
    ); }); }; - // Add a new useEffect hook to listen for segment deletion events + // Add a new useEffect hook to listen for segment deletion events useEffect(() => { const handleSegmentDelete = (event: CustomEvent) => { const { segmentId } = event.detail; // Check if this was the last segment before deletion - const remainingSegments = clipSegments.filter(seg => seg.id !== segmentId); + const remainingSegments = clipSegments.filter((seg) => seg.id !== segmentId); if (remainingSegments.length === 0) { // Create a full video segment const fullVideoSegment: Segment = { id: Date.now(), - name: 'Full Video', + name: "Full Video", startTime: 0, endTime: duration, - thumbnail: '' + thumbnail: "" }; // Create and dispatch the update event to replace all segments with the full video segment - const updateEvent = new CustomEvent('update-segments', { + const updateEvent = new CustomEvent("update-segments", { detail: { segments: [fullVideoSegment], recordHistory: true, - action: 'create_full_video_segment' + action: "create_full_video_segment" } }); document.dispatchEvent(updateEvent); @@ -1921,7 +2070,7 @@ const TimelineControls = ({ if (timelineRef.current) { const rect = timelineRef.current.getBoundingClientRect(); const posPercent = (currentTime / duration) * 100; - const xPosition = rect.left + (rect.width * (posPercent / 100)); + const xPosition = rect.left + rect.width * (posPercent / 100); setTooltipPosition({ x: xPosition, @@ -1936,7 +2085,7 @@ const TimelineControls = ({ } } else if (selectedSegmentId === segmentId) { // Handle normal segment deletion - const deletedSegment = clipSegments.find(seg => seg.id === segmentId); + const deletedSegment = clipSegments.find((seg) => seg.id === segmentId); if (!deletedSegment) return; // Calculate available space after deletion @@ -1951,7 +2100,7 @@ const TimelineControls = ({ if (timelineRef.current) { const rect = timelineRef.current.getBoundingClientRect(); const posPercent = (currentTime / duration) * 100; - const xPosition = rect.left + (rect.width * (posPercent / 100)); + const xPosition = rect.left + rect.width * (posPercent / 100); setTooltipPosition({ x: xPosition, @@ -1967,11 +2116,11 @@ const TimelineControls = ({ }; // Add event listener for the custom delete-segment event - document.addEventListener('delete-segment', handleSegmentDelete as EventListener); + document.addEventListener("delete-segment", handleSegmentDelete as EventListener); // Clean up event listener on component unmount return () => { - document.removeEventListener('delete-segment', handleSegmentDelete as EventListener); + document.removeEventListener("delete-segment", handleSegmentDelete as EventListener); }; }, [selectedSegmentId, clipSegments, currentTime, duration, timelineRef]); @@ -1995,7 +2144,7 @@ const TimelineControls = ({ let nextSegment = null; // First, check if we're inside a segment with high precision - currentSegment = clipSegments.find(seg => { + currentSegment = clipSegments.find((seg) => { const isWithinSegment = currentPosition >= seg.startTime && currentPosition <= seg.endTime; const isAtExactStart = Math.abs(currentPosition - seg.startTime) < 0.001; // Within 1ms of start const isAtExactEnd = Math.abs(currentPosition - seg.endTime) < 0.001; // Within 1ms of end @@ -2004,7 +2153,7 @@ const TimelineControls = ({ // Find the next segment with high precision nextSegment = clipSegments - .filter(seg => { + .filter((seg) => { const isAfterCurrent = seg.startTime > currentPosition; const isNotAtExactPosition = Math.abs(seg.startTime - currentPosition) > 0.001; return isAfterCurrent && isNotAtExactPosition; @@ -2072,7 +2221,7 @@ const TimelineControls = ({ } // Remove our boundary checker - video.removeEventListener('timeupdate', checkBoundary); + video.removeEventListener("timeupdate", checkBoundary); setIsPlaying(false); setIsPlayingSegment(false); // Reset continuePastBoundary flag when stopping at boundary @@ -2082,21 +2231,22 @@ const TimelineControls = ({ }; // Start our boundary checker - video.addEventListener('timeupdate', checkBoundary); + video.addEventListener("timeupdate", checkBoundary); // Start playing - video.play() + video + .play() .then(() => { setIsPlaying(true); setIsPlayingSegment(true); logger.debug("Playback started:", { from: formatDetailedTime(currentPosition), to: formatDetailedTime(stopTime), - currentSegment: currentSegment ? `Segment ${currentSegment.id}` : 'None', - nextSegment: nextSegment ? `Segment ${nextSegment.id}` : 'None' + currentSegment: currentSegment ? `Segment ${currentSegment.id}` : "None", + nextSegment: nextSegment ? `Segment ${nextSegment.id}` : "None" }); }) - .catch(err => { + .catch((err) => { console.error("Error playing video:", err); }); }; @@ -2106,12 +2256,12 @@ const TimelineControls = ({ setIsPlayingSegment(false); }; - video.addEventListener('play', handlePlay); - video.addEventListener('pause', handlePause); + video.addEventListener("play", handlePlay); + video.addEventListener("pause", handlePause); return () => { - video.removeEventListener('play', handlePlay); - video.removeEventListener('pause', handlePause); + video.removeEventListener("play", handlePlay); + video.removeEventListener("pause", handlePause); }; }, [clipSegments, duration, onSeek]); @@ -2127,7 +2277,7 @@ const TimelineControls = ({ const boundedTime = Math.max(0, Math.min(duration, time)); // Store position globally for iOS Safari - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.lastSeekedPosition = boundedTime; } }; @@ -2187,7 +2337,7 @@ const TimelineControls = ({ updateTooltipForPosition(newTime); // Store position globally for iOS Safari - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { (window as any).lastSeekedPosition = newTime; } @@ -2199,13 +2349,13 @@ const TimelineControls = ({ const handleMouseUp = () => { setIsDragging(false); isDraggingRef.current = false; // Update ref immediately - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); }; // Add event listeners to track movement and release - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); }; // Handle touch events for mobile devices @@ -2256,7 +2406,7 @@ const TimelineControls = ({ updateTooltipForPosition(newTime); // Store position globally for mobile browsers - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { (window as any).lastSeekedPosition = newTime; } @@ -2268,21 +2418,23 @@ const TimelineControls = ({ const handleTouchEnd = () => { setIsDragging(false); isDraggingRef.current = false; // Update ref immediately - document.removeEventListener('touchmove', handleTouchMove); - document.removeEventListener('touchend', handleTouchEnd); - document.removeEventListener('touchcancel', handleTouchEnd); + document.removeEventListener("touchmove", handleTouchMove); + document.removeEventListener("touchend", handleTouchEnd); + document.removeEventListener("touchcancel", handleTouchEnd); }; // Add event listeners to track movement and release - 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); }; // Add a useEffect to log the redirect URL whenever it changes useEffect(() => { if (redirectUrl) { - logger.debug('Redirect URL updated:', { + logger.debug("Redirect URL updated:", { redirectUrl, saveType, isSuccessModalOpen: showSuccessModal @@ -2302,7 +2454,7 @@ const TimelineControls = ({ // Update the countdown every second countdownInterval = setInterval(() => { secondsLeft--; - const countdownElement = document.querySelector('.countdown'); + const countdownElement = document.querySelector(".countdown"); if (countdownElement) { countdownElement.textContent = secondsLeft.toString(); } @@ -2318,7 +2470,7 @@ const TimelineControls = ({ if (onSave) onSave(); // Redirect to the URL - logger.debug('Automatically redirecting to:', redirectUrl); + logger.debug("Automatically redirecting to:", redirectUrl); window.location.href = redirectUrl; }, 10000); // 10 seconds } @@ -2331,24 +2483,28 @@ const TimelineControls = ({ }, [showSuccessModal, redirectUrl, onSave]); return ( -
    +
    {/* Current Timecode with Milliseconds */}
    Timeline
    - {/* Current time display removed as requested */}
    - Total Segments: {formatDetailedTime(clipSegments.reduce((sum, segment) => sum + (segment.endTime - segment.startTime), 0))} + Total Segments:{" "} + + {formatDetailedTime( + clipSegments.reduce((sum, segment) => sum + (segment.endTime - segment.startTime), 0) + )} +
    {/* Timeline Container with Scrollable Wrapper */}
    1 ? 'auto' : 'hidden' + overflow: zoomLevel > 1 ? "auto" : "hidden" }} >
    {/* Current Position Marker */} -
    +
    {/* Top circle for popup toggle */}
    currentTime >= seg.startTime && currentTime <= seg.endTime + (seg) => currentTime >= seg.startTime && currentTime <= seg.endTime ); // Toggle tooltip visibility with a single click @@ -2395,33 +2548,29 @@ const TimelineControls = ({ }} > - {selectedSegmentId || showEmptySpaceTooltip ? '-' : '+'} + {selectedSegmentId || showEmptySpaceTooltip ? "-" : "+"}
    {/* Bottom circle for dragging */} -
    - â‹® -
    + {isPlayingSegments ? null : ( +
    + â‹® +
    + )}
    {/* Trim Line Markers - hidden when segments exist */} {clipSegments.length === 0 && ( <> -
    +
    -
    +
    @@ -2439,56 +2588,96 @@ const TimelineControls = ({ {/* Segment Tooltip */} {selectedSegmentId !== null && (
    { + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} > {/* First row with time adjustment buttons */}
    - -
    {formatDetailedTime(displayTime)}
    - + +
    { + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} + > + {formatDetailedTime(displayTime)} +
    +
    {/* Second row with action buttons */}
    - {/*
    @@ -2736,26 +3051,73 @@ const TimelineControls = ({ {/* Empty space tooltip - positioned absolutely within timeline container */} {showEmptySpaceTooltip && selectedSegmentId === null && (
    { + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} > {/* First row with time adjustment buttons - same as segment tooltip */}
    -
    {formatDetailedTime(clickedTime)}
    +
    { + if (isPlayingSegments) { + e.stopPropagation(); + e.preventDefault(); + } + }} + > + {formatDetailedTime(clickedTime)} +
    @@ -2765,9 +3127,25 @@ const TimelineControls = ({
    {/* New segment button - Moved to first position */} {/* Go to start button - play from beginning of cutaway (until next segment) */} - {/* Play/Pause button for empty space */} {/* {/* Segment end adjustment button (always shown) */} {/* Segment start adjustment button (always shown) */}
    @@ -3711,26 +4267,31 @@ const TimelineControls = ({ placeholder="00:00:00.000" data-tooltip="Enter time in format: hh:mm:ss.ms" onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { const input = e.currentTarget.value; try { // Parse time format like "00:30:15.250" or "30:15.250" or "30:15" - const parts = input.split(':'); - let hours = 0, minutes = 0, seconds = 0, milliseconds = 0; + const parts = input.split(":"); + let hours = 0, + minutes = 0, + seconds = 0, + milliseconds = 0; if (parts.length === 3) { // Format: HH:MM:SS.ms hours = parseInt(parts[0]); minutes = parseInt(parts[1]); - const secParts = parts[2].split('.'); + const secParts = parts[2].split("."); seconds = parseInt(secParts[0]); - if (secParts.length > 1) milliseconds = parseInt(secParts[1].padEnd(3, '0').substring(0, 3)); + if (secParts.length > 1) + milliseconds = parseInt(secParts[1].padEnd(3, "0").substring(0, 3)); } else if (parts.length === 2) { // Format: MM:SS.ms minutes = parseInt(parts[0]); - const secParts = parts[1].split('.'); + const secParts = parts[1].split("."); seconds = parseInt(secParts[0]); - if (secParts.length > 1) milliseconds = parseInt(secParts[1].padEnd(3, '0').substring(0, 3)); + if (secParts.length > 1) + milliseconds = parseInt(secParts[1].padEnd(3, "0").substring(0, 3)); } const totalSeconds = hours * 3600 + minutes * 60 + seconds + milliseconds / 1000; @@ -3740,8 +4301,9 @@ const TimelineControls = ({ // Create a helper function to show tooltip that uses the same logic as the millisecond buttons const showTooltipAtTime = (timeInSeconds: number) => { // Find the segment at the given time using improved matching - const segmentAtTime = clipSegments.find(seg => { - const isWithinSegment = timeInSeconds >= seg.startTime && timeInSeconds <= seg.endTime; + const segmentAtTime = clipSegments.find((seg) => { + const isWithinSegment = + timeInSeconds >= seg.startTime && timeInSeconds <= seg.endTime; const isAtExactStart = Math.abs(timeInSeconds - seg.startTime) < 0.001; // Within 1ms of start const isAtExactEnd = Math.abs(timeInSeconds - seg.endTime) < 0.001; // Within 1ms of end return isWithinSegment || isAtExactStart || isAtExactEnd; @@ -3756,16 +4318,21 @@ const TimelineControls = ({ if (zoomLevel > 1) { // For zoomed timeline, calculate position based on visible area - const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - const markerVisibleX = visibleTimelineLeft + ((timeInSeconds / duration) * rect.width); + const visibleTimelineLeft = + rect.left - scrollContainerRef.current.scrollLeft; + const markerVisibleX = + visibleTimelineLeft + (timeInSeconds / duration) * rect.width; xPos = markerVisibleX; } else { // For non-zoomed timeline, use the simple calculation - const positionPercent = (timeInSeconds / duration); - xPos = rect.left + (rect.width * positionPercent); + const positionPercent = timeInSeconds / duration; + xPos = rect.left + rect.width * positionPercent; } - setTooltipPosition({ x: xPos, y: rect.top - 10 }); + setTooltipPosition({ + x: xPos, + y: rect.top - 10 + }); setClickedTime(timeInSeconds); if (segmentAtTime) { @@ -3796,8 +4363,9 @@ const TimelineControls = ({ // Helper function to show the appropriate tooltip at the current time position const showTooltipAtCurrentTime = () => { // Find the segment at the current time (after seeking) - using improved matching for better precision - const segmentAtCurrentTime = clipSegments.find(seg => { - const isWithinSegment = currentTime >= seg.startTime && currentTime <= seg.endTime; + const segmentAtCurrentTime = clipSegments.find((seg) => { + const isWithinSegment = + currentTime >= seg.startTime && currentTime <= seg.endTime; const isAtExactStart = Math.abs(currentTime - seg.startTime) < 0.001; // Within 1ms of start const isAtExactEnd = Math.abs(currentTime - seg.endTime) < 0.001; // Within 1ms of end return isWithinSegment || isAtExactStart || isAtExactEnd; @@ -3813,15 +4381,19 @@ const TimelineControls = ({ if (zoomLevel > 1) { // For zoomed timeline, calculate position based on visible area const visibleTimelineLeft = rect.left - scrollContainerRef.current.scrollLeft; - const markerVisibleX = visibleTimelineLeft + ((currentTime / duration) * rect.width); + const markerVisibleX = + visibleTimelineLeft + (currentTime / duration) * rect.width; xPos = markerVisibleX; } else { // For non-zoomed timeline, use the simple calculation - const positionPercent = (currentTime / duration); - xPos = rect.left + (rect.width * positionPercent); + const positionPercent = currentTime / duration; + xPos = rect.left + rect.width * positionPercent; } - setTooltipPosition({ x: xPos, y: rect.top - 10 }); + setTooltipPosition({ + x: xPos, + y: rect.top - 10 + }); setClickedTime(currentTime); if (segmentAtCurrentTime) { @@ -3928,11 +4500,19 @@ const TimelineControls = ({ {isZoomDropdownOpen && ( -
    - {[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096].map(level => ( +
    + {[1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096].map((level) => (
    { onZoomChange(level); setIsZoomDropdownOpen(false); @@ -4020,7 +4600,8 @@ const TimelineControls = ({ } >

    - You're about to replace the original video with this trimmed version. This can't be undone. + You're about to replace the original video with this trimmed version. This can't be + undone.

    @@ -4051,16 +4632,13 @@ const TimelineControls = ({ } >

    - You're about to save a new copy with your edits. The original video will stay the same. Find the new file in your My Media folder - named after the original file. + You're about to save a new copy with your edits. The original video will stay the + same. Find the new file in your My Media folder - named after the original file.

    {/* Processing Modal */} - {}} - title="Processing Video" - > + {}} title="Processing Video">
    @@ -4096,7 +4674,8 @@ const TimelineControls = ({ } >

    - You're about to save each segment as a separate video. Find the new files in your My Media folder - named after the original file. + You're about to save each segment as a separate video. Find the new files in your My + Media folder - named after the original file.

    @@ -4115,9 +4694,11 @@ const TimelineControls = ({ {saveType === "segments" ? "You will be redirected to your " : "You will be redirected to your "} - media page + + media page + {" in "} - 10 seconds. {' '} + 10 seconds.{" "} {saveType === "segments" ? "The new video(s) will soon be there." : "Changes to the video might take a few minutes to be applied."} @@ -4133,22 +4714,40 @@ const TimelineControls = ({ >
    - +
    -

    - {errorMessage} -

    +

    {errorMessage}

    )} - + {/* Fullscreen Button */} -
    diff --git a/frontend-tools/video-editor/client/src/hooks/useVideoTrimmer.tsx b/frontend-tools/video-editor/client/src/hooks/useVideoTrimmer.tsx index e5cf9296..718ff86c 100644 --- a/frontend-tools/video-editor/client/src/hooks/useVideoTrimmer.tsx +++ b/frontend-tools/video-editor/client/src/hooks/useVideoTrimmer.tsx @@ -10,7 +10,7 @@ interface EditorState { trimEnd: number; splitPoints: number[]; clipSegments: Segment[]; - action?: string; + action?: string; } const useVideoTrimmer = () => { @@ -20,89 +20,88 @@ const useVideoTrimmer = () => { const [duration, setDuration] = useState(0); 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([]); const [trimStart, setTrimStart] = useState(0); const [trimEnd, setTrimEnd] = useState(0); const [splitPoints, setSplitPoints] = useState([]); const [zoomLevel, setZoomLevel] = useState(1); // Start with 1x zoom level - + // Clip segments state const [clipSegments, setClipSegments] = useState([]); - + // History state for undo/redo const [history, setHistory] = useState([]); const [historyPosition, setHistoryPosition] = useState(-1); - + // Track unsaved changes const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - + // State for playing segments const [isPlayingSegments, setIsPlayingSegments] = useState(false); const [currentSegmentIndex, setCurrentSegmentIndex] = useState(0); - + // Monitor for history changes 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); } } }, [history, historyPosition]); - + // Set up page unload warning useEffect(() => { // Event handler for beforeunload 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 } }; - + // Add event listener - window.addEventListener('beforeunload', handleBeforeUnload); - + window.addEventListener("beforeunload", handleBeforeUnload); + // Clean up return () => { - window.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener("beforeunload", handleBeforeUnload); }; }, [hasUnsavedChanges]); - + // Initialize video event listeners useEffect(() => { const video = videoRef.current; if (!video) return; - + const handleLoadedMetadata = () => { setDuration(video.duration); setTrimEnd(video.duration); - + // Generate placeholders and create initial segment const initializeEditor = async () => { // Generate thumbnail for initial segment const segmentThumbnail = await generateThumbnail(video, video.duration / 2); - + // Create an initial segment that spans the entire video const initialSegment: Segment = { id: 1, @@ -111,7 +110,7 @@ const useVideoTrimmer = () => { endTime: video.duration, thumbnail: segmentThumbnail }; - + // Initialize history state with the full-length segment const initialState: EditorState = { trimStart: 0, @@ -119,79 +118,73 @@ const useVideoTrimmer = () => { splitPoints: [], clipSegments: [initialSegment] }; - + setHistory([initialState]); setHistoryPosition(0); setClipSegments([initialSegment]); - + // Generate timeline thumbnails const count = 6; const interval = video.duration / count; const placeholders: string[] = []; - + for (let i = 0; i < count; i++) { const time = interval * i + interval / 2; const thumbnail = await generateThumbnail(video, time); placeholders.push(thumbnail); } - + setThumbnails(placeholders); }; - + initializeEditor(); }; - + const handleTimeUpdate = () => { setCurrentTime(video.currentTime); }; - + const handlePlay = () => { - // Only update isPlaying if we're not in preview mode - if (!isPreviewMode) { - setIsPlaying(true); - setVideoInitialized(true); - } + setIsPlaying(true); + setVideoInitialized(true); }; - + const handlePause = () => { - // Only update isPlaying if we're not in preview mode - if (!isPreviewMode) { - setIsPlaying(false); - } + setIsPlaying(false); }; - + const handleEnded = () => { setIsPlaying(false); video.currentTime = trimStart; }; - + // 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 = () => { const video = videoRef.current; if (!video) return; - + if (isPlaying) { 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) { @@ -202,81 +195,56 @@ const useVideoTrimmer = () => { else if (video.currentTime >= trimEnd) { 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 }); } }; - + // Seek to a specific time const seekVideo = (time: number) => { const video = videoRef.current; if (!video) return; - + // 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); }); } }; - + // Save the current state to history with a debounce buffer // This helps prevent multiple rapid saves for small adjustments const saveState = (action?: string) => { @@ -286,51 +254,54 @@ 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 const lastState = history[historyPosition]; - + // Helper function to compare segments deeply const haveSegmentsChanged = () => { if (!lastState || lastState.clipSegments.length !== newState.clipSegments.length) { return true; // Different length means significant change } - + // Compare each segment's start and end times for (let i = 0; i < newState.clipSegments.length; i++) { const oldSeg = lastState.clipSegments[i]; const newSeg = newState.clipSegments[i]; - + 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; } } - + return false; // No significant changes found }; - - const isSignificantChange = !lastState || - lastState.trimStart !== newState.trimStart || + + const isSignificantChange = + !lastState || + lastState.trimStart !== newState.trimStart || lastState.trimEnd !== newState.trimEnd || lastState.splitPoints.length !== newState.splitPoints.length || haveSegmentsChanged(); - + // Additionally, check if there's an explicit action from a UI event const hasExplicitActionFlag = newState.action !== undefined; - + // Only proceed if this is a significant change or if explicitly requested if (isSignificantChange || hasExplicitActionFlag) { // Get the current position to avoid closure issues 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); @@ -340,9 +311,9 @@ const useVideoTrimmer = () => { return [...prevHistory, newState]; } }); - + // Update position using functional update - setHistoryPosition(prev => { + setHistoryPosition((prev) => { const newPosition = prev + 1; // "Saved state to history position", newPosition) return newPosition; @@ -351,36 +322,36 @@ const useVideoTrimmer = () => { // logger.debug("Skipped non-significant state save"); } }; - + // Listen for trim handle update events useEffect(() => { const handleTrimUpdate = (e: CustomEvent) => { if (e.detail) { const { time, isStart, recordHistory, action } = e.detail; - + if (isStart) { setTrimStart(time); } else { setTrimEnd(time); } - + // Only record in history if explicitly requested 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); }; }, []); - + // Listen for segment update events and split-at-time events useEffect(() => { const handleUpdateSegments = (e: CustomEvent) => { @@ -389,14 +360,16 @@ 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); - + // Always save state to history for non-intermediate actions if (isSignificantChange) { // A slight delay helps avoid race conditions but we need to @@ -404,7 +377,7 @@ const useVideoTrimmer = () => { setTimeout(() => { // Deep clone to ensure state is captured correctly const segmentsClone = JSON.parse(JSON.stringify(e.detail.segments)); - + // Create a complete state snapshot const stateWithAction: EditorState = { trimStart, @@ -413,12 +386,12 @@ const useVideoTrimmer = () => { clipSegments: segmentsClone, action: actionType // Store the action type in the state }; - + // Get the current history position to ensure we're using the latest value 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); @@ -428,90 +401,95 @@ const useVideoTrimmer = () => { return [...prevHistory, stateWithAction]; } }); - + // 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; - + // Move the current time to the split position 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 if (timeToSplit <= segmentToSplit.startTime || timeToSplit >= segmentToSplit.endTime) { return; // Can't split outside segment boundaries } - + // Create two new segments from the split 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); - + // Create first half of the split segment - no thumbnail needed const firstHalf: Segment = { id: Date.now(), 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 const secondHalf: Segment = { id: Date.now() + 1, 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 newSegments.push(firstHalf, secondHalf); - + // Sort segments by start time newSegments.sort((a, b) => a.startTime - b.startTime); - + // 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 if (newSegments.length === 0 && videoRef.current) { @@ -522,9 +500,9 @@ 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 setTrimStart(0); setTrimEnd(videoRef.current.duration); @@ -534,197 +512,50 @@ 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 const handleSplit = async () => { if (!videoRef.current) return; - + // Add current time to split points if not already present if (!splitPoints.includes(currentTime)) { const newSplitPoints = [...splitPoints, currentTime].sort((a, b) => a - b); setSplitPoints(newSplitPoints); - + // Generate segments based on split points const newSegments: Segment[] = []; let startTime = 0; - + for (let i = 0; i <= newSplitPoints.length; i++) { const endTime = i < newSplitPoints.length ? newSplitPoints[i] : duration; - + if (startTime < endTime) { // No need to generate thumbnails - we'll use dynamic colors newSegments.push({ @@ -732,51 +563,57 @@ 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; } } - + setClipSegments(newSegments); - saveState('create_split_points'); + saveState("create_split_points"); } }; - + // Handle reset of all edits const handleReset = async () => { setTrimStart(0); setTrimEnd(duration); setSplitPoints([]); - + // Create a new default segment that spans the entire video if (!videoRef.current) return; - + // No need to generate thumbnails - we'll use dynamic colors const defaultSegment: Segment = { id: Date.now(), 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 => - `ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}` - )); - + 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); setTrimEnd(previousState.trimEnd); @@ -787,18 +624,24 @@ const useVideoTrimmer = () => { logger.debug("Cannot undo: at earliest history position"); } }; - + // Handle redo 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 => - `ID: ${seg.id}, Time: ${formatDetailedTime(seg.startTime)} - ${formatDetailedTime(seg.endTime)}` - )); - + 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); setTrimEnd(nextState.trimEnd); @@ -809,161 +652,152 @@ const useVideoTrimmer = () => { logger.debug("Cannot redo: at latest history position"); } }; - + // Handle zoom level change const handleZoomChange = (level: number) => { setZoomLevel(level); }; - + // Handle play/pause of the full video const handlePlay = () => { 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); video.currentTime = window.lastSeekedPosition; } } - + // 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 }); } }; - + // Toggle mute state const toggleMute = () => { const video = videoRef.current; if (!video) return; - + video.muted = !video.muted; setIsMuted(!isMuted); }; - + // Handle save action const handleSave = () => { // Sort segments chronologically by start time before saving const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - + // 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); } - + // Mark as saved - no unsaved changes 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); }; - + // Handle save a copy action const handleSaveACopy = () => { // Sort segments chronologically by start time before saving const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - + // 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); } - + // Mark as saved - no unsaved changes 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 const handleSaveSegments = () => { // Sort segments chronologically by start time before saving const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - + // 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) })) }; - + // 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); } - + // Mark as saved - no unsaved changes setHasUnsavedChanges(false); - + // Debug message 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 const handleMobileSafeSeek = (time: number) => { // Only allow seeking if not on mobile or if video has been played @@ -971,20 +805,24 @@ const useVideoTrimmer = () => { seekVideo(time); } }; - + // 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); - + // Effect to handle segments playback useEffect(() => { if (!isPlayingSegments || !videoRef.current) return; const video = videoRef.current; const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); - + const handleSegmentsPlayback = () => { const currentSegment = orderedSegments[currentSegmentIndex]; if (!currentSegment) return; @@ -1004,11 +842,11 @@ const useVideoTrimmer = () => { const nextSegment = orderedSegments[currentSegmentIndex + 1]; video.currentTime = nextSegment.startTime; setCurrentSegmentIndex(currentSegmentIndex + 1); - + // 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]); @@ -1066,28 +909,25 @@ const useVideoTrimmer = () => { // Start segments playback 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); - + // Start from the first segment 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); }); - + logger.debug("Starting playback of all segments continuously"); } }; - + return { videoRef, currentTime, @@ -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 }; }; diff --git a/frontend-tools/video-editor/client/src/index.css b/frontend-tools/video-editor/client/src/index.css index 71eddbc0..e635a6ae 100644 --- a/frontend-tools/video-editor/client/src/index.css +++ b/frontend-tools/video-editor/client/src/index.css @@ -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%; } diff --git a/frontend-tools/video-editor/client/src/lib/logger.ts b/frontend-tools/video-editor/client/src/lib/logger.ts index 982655c1..f204c26d 100644 --- a/frontend-tools/video-editor/client/src/lib/logger.ts +++ b/frontend-tools/video-editor/client/src/lib/logger.ts @@ -7,25 +7,25 @@ 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); } }, - + /** * Always logs error messages */ error: (...args: any[]) => console.error(...args), - + /** * Always logs warning messages */ warn: (...args: any[]) => console.warn(...args), - + /** * Always logs info messages */ info: (...args: any[]) => console.info(...args) }; -export default logger; \ No newline at end of file +export default logger; diff --git a/frontend-tools/video-editor/client/src/lib/queryClient.ts b/frontend-tools/video-editor/client/src/lib/queryClient.ts index a8b3fc1d..892f099a 100644 --- a/frontend-tools/video-editor/client/src/lib/queryClient.ts +++ b/frontend-tools/video-editor/client/src/lib/queryClient.ts @@ -10,13 +10,13 @@ async function throwIfResNotOk(res: Response) { export async function apiRequest( method: string, url: string, - data?: unknown | undefined, + data?: unknown | undefined ): Promise { 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: (options: { - on401: UnauthorizedBehavior; -}) => QueryFunction = +export const getQueryFn: (options: { on401: UnauthorizedBehavior }) => QueryFunction = ({ 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 + } + } }); diff --git a/frontend-tools/video-editor/client/src/lib/timeUtils.ts b/frontend-tools/video-editor/client/src/lib/timeUtils.ts index d33862a9..14fef1ba 100644 --- a/frontend-tools/video-editor/client/src/lib/timeUtils.ts +++ b/frontend-tools/video-editor/client/src/lib/timeUtils.ts @@ -3,17 +3,17 @@ */ export const formatDetailedTime = (seconds: number): string => { 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"); - + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; }; diff --git a/frontend-tools/video-editor/client/src/lib/utils.ts b/frontend-tools/video-editor/client/src/lib/utils.ts index bd0c391d..a5ef1935 100644 --- a/frontend-tools/video-editor/client/src/lib/utils.ts +++ b/frontend-tools/video-editor/client/src/lib/utils.ts @@ -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)); } diff --git a/frontend-tools/video-editor/client/src/lib/videoUtils.ts b/frontend-tools/video-editor/client/src/lib/videoUtils.ts index affb9d18..0586e031 100644 --- a/frontend-tools/video-editor/client/src/lib/videoUtils.ts +++ b/frontend-tools/video-editor/client/src/lib/videoUtils.ts @@ -2,20 +2,17 @@ * 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); - + // Calculate color based on position // Use an extremely light blue-based color palette const hue = 210; // Blue base const saturation = 40 + Math.floor(position * 20); // 40-60% (less saturated) const lightness = 85 + Math.floor(position * 8); // 85-93% (extremely light) - + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; }; @@ -24,27 +21,27 @@ export const generateSolidColor = ( * Now returns a data URL for a solid color square instead of a video thumbnail */ export const generateThumbnail = async ( - videoElement: HTMLVideoElement, + videoElement: HTMLVideoElement, time: number ): Promise => { 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); - + // Fill with solid color ctx.fillStyle = color; ctx.fillRect(0, 0, canvas.width, canvas.height); } - + // 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); }); }; diff --git a/frontend-tools/video-editor/client/src/main.tsx b/frontend-tools/video-editor/client/src/main.tsx index 780c763a..044e1cd2 100644 --- a/frontend-tools/video-editor/client/src/main.tsx +++ b/frontend-tools/video-editor/client/src/main.tsx @@ -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(); -} \ No newline at end of file +} diff --git a/frontend-tools/video-editor/client/src/services/videoApi.ts b/frontend-tools/video-editor/client/src/services/videoApi.ts index 7e3e5175..88389907 100644 --- a/frontend-tools/video-editor/client/src/services/videoApi.ts +++ b/frontend-tools/video-editor/client/src/services/videoApi.ts @@ -4,36 +4,36 @@ interface TrimVideoRequest { segments: { startTime: string; endTime: string; - name?: string; + name?: string; }[]; saveAsCopy?: boolean; - saveIndividualSegments?: boolean; + saveIndividualSegments?: boolean; } interface TrimVideoResponse { msg: string; url_redirect: string; - status?: number; // HTTP status code for success/error - error?: string; // Error message if status is not 200 + status?: number; // HTTP status code for success/error + error?: string; // Error message if status is not 200 } // 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 export const trimVideo = async ( - mediaId: string, + mediaId: string, data: TrimVideoRequest ): Promise => { 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) }); - + if (!response.ok) { // For error responses, return with error status and message if (response.status === 400) { @@ -86,7 +86,7 @@ export const trimVideo = async ( }; } } - + // Successful response const jsonResponse = await response.json(); return { @@ -104,7 +104,7 @@ export const trimVideo = async ( url_redirect: `./view?m=${mediaId}` }; } - + /* Mock implementation that simulates network latency return new Promise((resolve) => { setTimeout(() => { @@ -115,4 +115,4 @@ export const trimVideo = async ( }, 1500); // Simulate 1.5 second server delay }); */ -}; \ No newline at end of file +}; diff --git a/frontend-tools/video-editor/client/src/styles/ClipSegments.css b/frontend-tools/video-editor/client/src/styles/ClipSegments.css index 49d55474..3b71925e 100644 --- a/frontend-tools/video-editor/client/src/styles/ClipSegments.css +++ b/frontend-tools/video-editor/client/src/styles/ClipSegments.css @@ -4,7 +4,7 @@ [data-tooltip] { position: relative; } - + [data-tooltip]:before { content: attr(data-tooltip); position: absolute; @@ -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,17 +39,19 @@ 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; } - + [data-tooltip]:hover:before, [data-tooltip]:hover:after { opacity: 1; visibility: visible; } } - + /* Hide button tooltips on touch devices */ @media (pointer: coarse) { [data-tooltip]:before, @@ -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); } -} \ No newline at end of file + .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); + } +} diff --git a/frontend-tools/video-editor/client/src/styles/EditingTools.css b/frontend-tools/video-editor/client/src/styles/EditingTools.css index 29cb00a7..06f611bd 100644 --- a/frontend-tools/video-editor/client/src/styles/EditingTools.css +++ b/frontend-tools/video-editor/client/src/styles/EditingTools.css @@ -1,11 +1,10 @@ #video-editor-trim-root { - /* Tooltip styles - only on desktop where hover is available */ @media (hover: hover) and (pointer: fine) { [data-tooltip] { position: relative; } - + [data-tooltip]:before { content: attr(data-tooltip); position: absolute; @@ -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,17 +39,19 @@ 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; } - + [data-tooltip]:hover:before, [data-tooltip]:hover:after { opacity: 1; visibility: visible; } } - + /* Hide button tooltips on touch devices */ @media (pointer: coarse) { [data-tooltip]:before, @@ -86,7 +89,7 @@ .full-text { display: inline; } - + .short-text { display: none; } @@ -99,20 +102,20 @@ .button-group { display: flex; align-items: center; - + &.play-buttons-group { gap: 0.75rem; justify-content: flex-start; flex: 0 0 auto; /* Don't expand to fill space */ } - + &.secondary { gap: 0.75rem; align-items: center; justify-content: flex-end; margin-left: auto; /* Push to right edge */ } - + button { display: flex; align-items: center; @@ -121,17 +124,16 @@ border: none; cursor: pointer; min-width: auto; - - /* Disabled hover effect as requested */ + &:hover:not(:disabled) { color: inherit; } - + &:disabled { opacity: 0.5; cursor: not-allowed; } - + svg { height: 1.25rem; width: 1.25rem; @@ -144,10 +146,11 @@ border-right: 1px solid #d1d5db; height: 1.5rem; margin: 0 0.5rem; - } + } /* Style for play buttons with highlight effect */ - .play-button, .preview-button { + .play-button, + .preview-button { font-weight: 600; display: flex; align-items: center; @@ -157,13 +160,13 @@ justify-content: center; font-size: 0.875rem !important; } - + /* Greyed out play button when segments are playing */ .play-button.greyed-out { opacity: 0.5; cursor: not-allowed; } - + /* Highlighted stop button with blue pulse on small screens */ .segments-button.highlighted-stop { background-color: rgba(59, 130, 246, 0.1); @@ -171,7 +174,7 @@ border: 1px solid #3b82f6; animation: bluePulse 2s infinite; } - + @keyframes bluePulse { 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); @@ -183,9 +186,10 @@ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } } - + /* 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; @@ -193,27 +197,15 @@ width: auto !important; 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; @@ -225,19 +217,12 @@ opacity: 0.8; } } - - .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; } - + /* Media queries for the editing tools */ @media (max-width: 992px) { /* Hide text for undo/redo buttons on medium screens */ @@ -245,76 +230,77 @@ display: none; } } - + @media (max-width: 768px) { /* Keep all buttons in a single row, make them more compact */ .flex-container.single-row { justify-content: space-between; } - + .button-group { gap: 0.5rem; } - + /* Keep font size consistent regardless of screen size */ - .preview-button, .play-button { + .preview-button, + .play-button { font-size: 0.875rem !important; } } - + @media (max-width: 640px) { /* Prevent container overflow on mobile */ .editing-tools-container { padding: 0.75rem; overflow-x: hidden; } - + /* At this breakpoint, make preview button text shorter */ .preview-button { min-width: auto; } - + /* Switch to short text versions */ .full-text { display: none; } - + .short-text { display: inline; margin-left: 0.15rem; } - + /* Hide reset text */ .reset-text { display: none; } - + /* Ensure buttons stay in correct position */ .button-group.play-buttons-group { flex: initial; justify-content: flex-start; flex-shrink: 0; } - + .button-group.secondary { flex: initial; justify-content: flex-end; flex-shrink: 0; } - + /* Reduce button sizes on mobile */ .button-group button { padding: 0.375rem; min-width: auto; } - + .button-group button svg { height: 1.125rem; width: 1.125rem; margin-right: 0.125rem; } } - + @media (max-width: 576px) { /* Keep single row, left-align play buttons, right-align controls */ .flex-container.single-row { @@ -322,94 +308,88 @@ flex-wrap: nowrap; gap: 10px; } - + /* Fix left-align for play buttons */ .button-group.play-buttons-group { justify-content: flex-start; flex: 0 0 auto; } - + /* Fix right-align for editing controls */ .button-group.secondary { justify-content: flex-end; margin-left: auto; } - + /* Reduce button padding to fit more easily */ .button-group button { padding: 0.25rem; } - - /* Smaller preview mode message */ - .preview-mode-message { - font-size: 0.8rem; - padding: 4px 8px; - } - + .divider { margin: 0 0.25rem; } } - + /* Very small screens - maintain layout but reduce further */ @media (max-width: 480px) { .editing-tools-container { padding: 0.5rem; } - + .flex-container.single-row { gap: 8px; } - + .button-group.play-buttons-group, .button-group.secondary { gap: 0.25rem; } - + .divider { display: none; /* Hide divider on very small screens */ } - + /* Even smaller buttons on very small screens */ .button-group button { padding: 0.125rem; } - + .button-group button svg { height: 1rem; width: 1rem; margin-right: 0; } - + /* Hide all button text on very small screens */ .button-text, .reset-text { display: none; } } - + /* Portrait orientation specific fixes */ @media (max-width: 640px) and (orientation: portrait) { .editing-tools-container { width: 100%; box-sizing: border-box; } - + .flex-container.single-row { width: 100%; padding: 0; margin: 0; } - + /* Ensure button groups don't overflow */ .button-group { max-width: 50%; } - + .button-group.play-buttons-group { max-width: 60%; } - + .button-group.secondary { max-width: 40%; } diff --git a/frontend-tools/video-editor/client/src/styles/IOSNotification.css b/frontend-tools/video-editor/client/src/styles/IOSNotification.css index 3a0c9a96..5e3af434 100644 --- a/frontend-tools/video-editor/client/src/styles/IOSNotification.css +++ b/frontend-tools/video-editor/client/src/styles/IOSNotification.css @@ -132,7 +132,7 @@ .ios-notification { padding-top: env(safe-area-inset-top); } - + .ios-notification-close { padding: 10px; } @@ -143,11 +143,11 @@ .ios-notification-content { padding: 5px; } - + .ios-notification-message h3 { font-size: 15px; } - + .ios-notification-message p, .ios-notification-message ol { font-size: 13px; @@ -164,4 +164,4 @@ html.ios-device { html.ios-device .ios-control-btn { /* Make buttons easier to tap in desktop mode */ min-height: 44px; -} \ No newline at end of file +} diff --git a/frontend-tools/video-editor/client/src/styles/IOSPlayPrompt.css b/frontend-tools/video-editor/client/src/styles/IOSPlayPrompt.css index 438cfd4e..9fa7d707 100644 --- a/frontend-tools/video-editor/client/src/styles/IOSPlayPrompt.css +++ b/frontend-tools/video-editor/client/src/styles/IOSPlayPrompt.css @@ -93,4 +93,4 @@ /* Extra spacing for mobile */ padding: 14px 25px; } -} \ No newline at end of file +} diff --git a/frontend-tools/video-editor/client/src/styles/IOSVideoPlayer.css b/frontend-tools/video-editor/client/src/styles/IOSVideoPlayer.css index 3b671b34..8d8dbf92 100644 --- a/frontend-tools/video-editor/client/src/styles/IOSVideoPlayer.css +++ b/frontend-tools/video-editor/client/src/styles/IOSVideoPlayer.css @@ -36,13 +36,13 @@ .ios-video-player-container video { max-height: 50vh; /* Use viewport height on iOS */ } - + /* Improve controls visibility on iOS */ video::-webkit-media-controls { opacity: 1 !important; visibility: visible !important; } - + /* Ensure controls don't disappear too quickly */ video::-webkit-media-controls-panel { transition-duration: 3s !important; @@ -76,19 +76,19 @@ /* Prevent text selection on buttons */ .no-select { -webkit-touch-callout: none; /* iOS Safari */ - -webkit-user-select: none; /* Safari */ - -khtml-user-select: none; /* Konqueror HTML */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* Internet Explorer/Edge */ - user-select: none; /* Non-prefixed version, supported by Chrome and Opera */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, supported by Chrome and Opera */ cursor: default; } /* Specifically prevent default behavior on fine controls */ -.ios-fine-controls button, +.ios-fine-controls button, .ios-external-controls .no-select { touch-action: manipulation; -webkit-touch-callout: none; -webkit-user-select: none; pointer-events: auto; -} \ No newline at end of file +} diff --git a/frontend-tools/video-editor/client/src/styles/Modal.css b/frontend-tools/video-editor/client/src/styles/Modal.css index f5d51349..0d67c342 100644 --- a/frontend-tools/video-editor/client/src/styles/Modal.css +++ b/frontend-tools/video-editor/client/src/styles/Modal.css @@ -1,302 +1,306 @@ #video-editor-trim-root { -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-container { - background-color: white; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - width: 90%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; - animation: modal-fade-in 0.3s ease-out; -} - -@keyframes modal-fade-in { - from { - opacity: 0; - transform: translateY(-20px); + .modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; } - to { - opacity: 1; - transform: translateY(0); - } -} -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - border-bottom: 1px solid #eee; -} - -.modal-title { - margin: 0; - font-size: 1.25rem; - font-weight: 600; - color: #333; -} - -.modal-close-button { - background: none; - border: none; - cursor: pointer; - color: #666; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s; -} - -.modal-close-button:hover { - color: #000; -} - -.modal-content { - padding: 20px; - color: #333; - font-size: 1rem; - line-height: 1.5; - max-height: 400px; - overflow-y: auto; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - padding: 16px 20px; - border-top: 1px solid #eee; - gap: 12px; -} - -.modal-button { - padding: 8px 16px; - border-radius: 4px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; - border: none; -} - -.modal-button-primary { - background-color: #0066cc; - color: white; -} - -.modal-button-primary:hover { - background-color: #0055aa; -} - -.modal-button-secondary { - background-color: #f0f0f0; - color: #333; -} - -.modal-button-secondary:hover { - background-color: #e0e0e0; -} - -.modal-button-danger { - background-color: #dc3545; - color: white; -} - -.modal-button-danger:hover { - background-color: #bd2130; -} - -/* Modal content styles */ -.modal-message { - margin-bottom: 16px; - font-size: 1rem; -} - -.text-center { - text-align: center; -} - -.modal-spinner { - display: flex; - align-items: center; - justify-content: center; - margin: 20px 0; -} - -.spinner { - border: 4px solid rgba(0, 0, 0, 0.1); - border-radius: 50%; - border-top: 4px solid #0066cc; - width: 30px; - height: 30px; - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.modal-success-icon { - display: flex; - justify-content: center; - margin-bottom: 16px; - color: #28a745; - font-size: 2rem; -} - -.modal-success-icon svg { - width: 60px; - height: 60px; - color: #4CAF50; - animation: success-pop 0.5s ease-out; -} - -@keyframes success-pop { - 0% { - transform: scale(0); - opacity: 0; - } - 70% { - transform: scale(1.1); - opacity: 1; - } - 100% { - transform: scale(1); - opacity: 1; - } -} - -.modal-error-icon { - display: flex; - justify-content: center; - margin-bottom: 16px; - color: #dc3545; - font-size: 2rem; -} - -.modal-error-icon svg { - width: 60px; - height: 60px; - color: #F44336; - animation: error-pop 0.5s ease-out; -} - -@keyframes error-pop { - 0% { - transform: scale(0); - opacity: 0; - } - 70% { - transform: scale(1.1); - opacity: 1; - } - 100% { - transform: scale(1); - opacity: 1; - } -} - -.modal-choices { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 20px; -} - -.modal-choice-button { - padding: 12px 16px; - border: none; - border-radius: 4px; - background-color: #0066cc; - text-align: center; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - font-weight: 500; - text-decoration: none; - color: white; -} - -.modal-choice-button:hover { - background-color: #0055aa; - transform: translateY(-1px); - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); -} - -.modal-choice-button svg { - margin-right: 8px; -} - -.success-link { - background-color: #4CAF50; -} - -.success-link:hover { - background-color: #3d8b40; -} - -.centered-choice { - margin: 0 auto; - width: auto; - min-width: 220px; - background-color: #0066cc; - color: white; -} - -.centered-choice:hover { - background-color: #0055aa; -} - -@media (max-width: 480px) { .modal-container { - width: 95%; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + width: 90%; + max-width: 500px; + max-height: 90vh; + overflow-y: auto; + animation: modal-fade-in 0.3s ease-out; } - + + @keyframes modal-fade-in { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #eee; + } + + .modal-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: #333; + } + + .modal-close-button { + background: none; + border: none; + cursor: pointer; + color: #666; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s; + } + + .modal-close-button:hover { + color: #000; + } + + .modal-content { + padding: 20px; + color: #333; + font-size: 1rem; + line-height: 1.5; + max-height: 400px; + overflow-y: auto; + } + .modal-actions { - flex-direction: column; + display: flex; + justify-content: flex-end; + padding: 16px 20px; + border-top: 1px solid #eee; + gap: 12px; } - + .modal-button { - width: 100%; + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + border: none; + } + + .modal-button-primary { + background-color: #0066cc; + color: white; + } + + .modal-button-primary:hover { + background-color: #0055aa; + } + + .modal-button-secondary { + background-color: #f0f0f0; + color: #333; + } + + .modal-button-secondary:hover { + background-color: #e0e0e0; + } + + .modal-button-danger { + background-color: #dc3545; + color: white; + } + + .modal-button-danger:hover { + background-color: #bd2130; + } + + /* Modal content styles */ + .modal-message { + margin-bottom: 16px; + font-size: 1rem; + } + + .text-center { + text-align: center; + } + + .modal-spinner { + display: flex; + align-items: center; + justify-content: center; + margin: 20px 0; + } + + .spinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top: 4px solid #0066cc; + width: 30px; + height: 30px; + animation: spin 1s linear infinite; + } + + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + .modal-success-icon { + display: flex; + justify-content: center; + margin-bottom: 16px; + color: #28a745; + font-size: 2rem; + } + + .modal-success-icon svg { + width: 60px; + height: 60px; + color: #4caf50; + animation: success-pop 0.5s ease-out; + } + + @keyframes success-pop { + 0% { + transform: scale(0); + opacity: 0; + } + 70% { + transform: scale(1.1); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } + } + + .modal-error-icon { + display: flex; + justify-content: center; + margin-bottom: 16px; + color: #dc3545; + font-size: 2rem; + } + + .modal-error-icon svg { + width: 60px; + height: 60px; + color: #f44336; + animation: error-pop 0.5s ease-out; + } + + @keyframes error-pop { + 0% { + transform: scale(0); + opacity: 0; + } + 70% { + transform: scale(1.1); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 1; + } + } + + .modal-choices { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 20px; + } + + .modal-choice-button { + padding: 12px 16px; + border: none; + border-radius: 4px; + background-color: #0066cc; + text-align: center; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + text-decoration: none; + color: white; + } + + .modal-choice-button:hover { + background-color: #0055aa; + transform: translateY(-1px); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + } + + .modal-choice-button svg { + margin-right: 8px; + } + + .success-link { + background-color: #4caf50; + } + + .success-link:hover { + background-color: #3d8b40; + } + + .centered-choice { + margin: 0 auto; + width: auto; + min-width: 220px; + background-color: #0066cc; + color: white; + } + + .centered-choice:hover { + background-color: #0055aa; + } + + @media (max-width: 480px) { + .modal-container { + width: 95%; + } + + .modal-actions { + flex-direction: column; + } + + .modal-button { + width: 100%; + } + } + + .error-message { + color: #f44336; + font-weight: 500; + background-color: rgba(244, 67, 54, 0.1); + padding: 10px; + border-radius: 4px; + border-left: 4px solid #f44336; + margin-top: 10px; + } + + .redirect-message { + margin-top: 20px; + color: #555; + font-size: 0.95rem; + padding: 0; + margin: 0; + } + + .countdown { + font-weight: bold; + color: #0066cc; + font-size: 1.1rem; } } - -.error-message { - color: #F44336; - font-weight: 500; - background-color: rgba(244, 67, 54, 0.1); - padding: 10px; - border-radius: 4px; - border-left: 4px solid #F44336; - margin-top: 10px; -} - -.redirect-message { - margin-top: 20px; - color: #555; - font-size: 0.95rem; - padding: 0; - margin: 0; -} - -.countdown { - font-weight: bold; - color: #0066cc; - font-size: 1.1rem; -} -} \ No newline at end of file diff --git a/frontend-tools/video-editor/client/src/styles/TimelineControls.css b/frontend-tools/video-editor/client/src/styles/TimelineControls.css index a030b0e9..ac38e61d 100644 --- a/frontend-tools/video-editor/client/src/styles/TimelineControls.css +++ b/frontend-tools/video-editor/client/src/styles/TimelineControls.css @@ -56,7 +56,7 @@ .timeline-marker { position: absolute; - height: 82px; /* Increased height to extend below timeline */ + height: 82px; /* Increased height to extend below timeline */ width: 2px; background-color: #000; transform: translateX(-50%); @@ -83,7 +83,7 @@ .timeline-marker-drag { position: absolute; - bottom: -12px; /* Changed from -6px to -12px to move it further down */ + bottom: -12px; /* Changed from -6px to -12px to move it further down */ left: 50%; transform: translateX(-50%); width: 16px; @@ -248,14 +248,14 @@ right: 0; border-radius: 0 2px 2px 0; } - + /* Enhanced handles for touch devices */ @media (pointer: coarse) { .clip-segment-handle { width: 14px; /* Wider target for touch devices */ background-color: rgba(0, 0, 0, 0.4); /* Darker by default for better visibility */ } - + .clip-segment-handle:after { content: ""; position: absolute; @@ -267,15 +267,15 @@ background-color: rgba(255, 255, 255, 0.8); border-radius: 1px; } - + .clip-segment-handle.left:after { box-shadow: -2px 0 0 rgba(0, 0, 0, 0.5); } - + .clip-segment-handle.right:after { box-shadow: 2px 0 0 rgba(0, 0, 0, 0.5); } - + /* Active state for touch feedback */ .clip-segment-handle:active { background-color: rgba(0, 0, 0, 0.6); @@ -284,19 +284,19 @@ .timeline-marker { height: 85px; } - + .timeline-marker-head { width: 24px; height: 24px; top: -13px; } - + .timeline-marker-drag { width: 24px; height: 24px; bottom: -18px; } - + .timeline-marker-head.dragging { width: 28px; height: 28px; @@ -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 { @@ -532,8 +532,8 @@ } /* General styles for all save buttons */ - .save-button, - .save-copy-button, + .save-button, + .save-copy-button, .save-segments-button { color: #ffffff; background: #0066cc; @@ -548,8 +548,8 @@ } /* Shared hover effect */ - .save-button:hover, - .save-copy-button:hover, + .save-button:hover, + .save-copy-button:hover, .save-segments-button:hover { background-color: #0056b3; } @@ -561,30 +561,30 @@ justify-content: space-between; gap: 0.5rem; } - - .save-button, - .save-copy-button, + + .save-button, + .save-copy-button, .save-segments-button { flex: 1; font-size: 0.7rem; padding: 0.25rem 0.35rem; } } - + /* Very small screens - adjust save buttons */ @media (max-width: 480px) { - .save-button, - .save-copy-button, + .save-button, + .save-copy-button, .save-segments-button { font-size: 0.675rem; padding: 0.25rem; } - + /* Remove margins for controls-right buttons */ .controls-right { margin: 0; } - + .controls-right button { margin: 0; } @@ -595,7 +595,7 @@ [data-tooltip] { position: relative; } - + [data-tooltip]:before { content: attr(data-tooltip); position: absolute; @@ -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,17 +630,19 @@ 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; } - + [data-tooltip]:hover:before, [data-tooltip]:hover:after { opacity: 1; visibility: visible; } } - + /* Hide button tooltips on touch devices */ @media (pointer: coarse) { [data-tooltip]:before, @@ -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; @@ -889,4 +871,4 @@ width: 1.25rem; margin-right: 0.5rem; color: #3b82f6; -} \ No newline at end of file +} diff --git a/frontend-tools/video-editor/client/src/styles/TwoRowTooltip.css b/frontend-tools/video-editor/client/src/styles/TwoRowTooltip.css index 50a58404..74bb2160 100644 --- a/frontend-tools/video-editor/client/src/styles/TwoRowTooltip.css +++ b/frontend-tools/video-editor/client/src/styles/TwoRowTooltip.css @@ -23,7 +23,7 @@ } .tooltip-row:first-child { - margin-bottom: 6px; + margin-bottom: 6px; } .tooltip-time-btn { @@ -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; @@ -69,13 +89,13 @@ background-color: #f3f4f6; border: none; border-radius: 4px; - padding: 5px; + padding: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #4b5563; - width: 26px; + width: 26px; height: 26px; min-width: 20px !important; position: relative; /* Add relative positioning for tooltips */ @@ -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; } @@ -175,7 +199,7 @@ } .tooltip-action-btn.play-from-start { - color: #4f46e5; + color: #4f46e5; } .tooltip-action-btn.play-from-start:hover { @@ -194,7 +218,7 @@ padding: 6px 10px; display: flex; flex-direction: row; - color: #10b981; + color: #10b981; } .tooltip-action-btn.new-segment:hover { @@ -227,43 +251,80 @@ 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 { - padding: 4px; + padding: 4px; } - + .tooltip-row:first-child { - margin-bottom: 4px; + margin-bottom: 4px; } - + .tooltip-time-btn { min-width: 20px !important; font-size: 0.7rem !important; padding: 3px 6px !important; } - + .tooltip-time-display { font-size: 0.8rem !important; padding: 3px 4px !important; min-width: 90px !important; } - + .tooltip-action-btn { width: 24px; height: 24px; padding: 4px; } - + .tooltip-action-btn.new-segment { padding: 4px 8px; } - + .tooltip-action-btn svg { width: 14px; height: 14px; } - + /* Adjust tooltip position for small screens - maintain the same position but adjust size */ .tooltip-action-btn[data-tooltip]:before { min-width: 100px; @@ -272,7 +333,7 @@ height: 24px; top: 33px; /* Maintain the same relative distance on mobile */ } - + .tooltip-action-btn[data-tooltip]:after { top: 33px; /* Match the tooltip position */ } diff --git a/frontend-tools/video-editor/client/src/styles/VideoPlayer.css b/frontend-tools/video-editor/client/src/styles/VideoPlayer.css index bbe84d67..11c7ed82 100644 --- a/frontend-tools/video-editor/client/src/styles/VideoPlayer.css +++ b/frontend-tools/video-editor/client/src/styles/VideoPlayer.css @@ -4,7 +4,7 @@ [data-tooltip] { position: relative; } - + [data-tooltip]:before { content: attr(data-tooltip); position: absolute; @@ -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,17 +39,19 @@ 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; } - + [data-tooltip]:hover:before, [data-tooltip]:hover:after { opacity: 1; visibility: visible; } } - + /* Hide button tooltips on touch devices */ @media (pointer: coarse) { [data-tooltip]:before, @@ -71,7 +75,7 @@ -webkit-user-select: none; user-select: none; } - + .video-player-container video { width: 100%; height: 100%; @@ -83,7 +87,7 @@ -webkit-user-select: none; user-select: none; } - + /* iOS-specific styles */ @supports (-webkit-touch-callout: none) { .video-player-container video { @@ -92,7 +96,7 @@ -webkit-touch-callout: none; } } - + .play-pause-indicator { position: absolute; top: 50%; @@ -106,19 +110,19 @@ transition: opacity 0.3s; pointer-events: none; } - + .video-player-container:hover .play-pause-indicator { opacity: 1; } - + .play-pause-indicator::before { - content: ''; + content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } - + .play-pause-indicator.play-icon::before { width: 0; height: 0; @@ -127,14 +131,14 @@ border-left: 25px solid white; margin-left: 3px; } - + .play-pause-indicator.pause-icon::before { width: 20px; height: 25px; border-left: 6px solid white; border-right: 6px solid white; } - + /* iOS First-play indicator */ .ios-first-play-indicator { position: absolute; @@ -148,7 +152,7 @@ justify-content: center; z-index: 10; } - + .ios-play-message { color: white; font-size: 1.2rem; @@ -158,13 +162,22 @@ border-radius: 0.5rem; animation: pulse 2s infinite; } - + @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 { position: absolute; bottom: 0; @@ -175,21 +188,21 @@ opacity: 0; transition: opacity 0.3s; } - + .video-player-container:hover .video-controls { opacity: 1; } - + .video-current-time { color: white; font-size: 0.875rem; } - + .video-duration { color: white; font-size: 0.875rem; } - + .video-time-display { display: flex; justify-content: space-between; @@ -197,7 +210,7 @@ color: white; font-size: 0.875rem; } - + .video-progress { position: relative; height: 6px; @@ -208,11 +221,11 @@ touch-action: none; /* Prevent browser handling of drag gestures */ flex-grow: 1; } - + .video-progress.dragging { height: 8px; } - + .video-progress-fill { position: absolute; top: 0; @@ -222,7 +235,7 @@ border-radius: 3px; pointer-events: none; } - + .video-scrubber { position: absolute; top: 50%; @@ -232,9 +245,12 @@ 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 */ .video-progress.dragging .video-scrubber { transform: translate(-50%, -50%) scale(1.2); @@ -243,22 +259,22 @@ cursor: grabbing; box-shadow: 0 0 8px rgba(255, 0, 0, 0.6); } - + /* Enhance for touch devices */ @media (pointer: coarse) { .video-scrubber { width: 20px; height: 20px; } - + .video-progress.dragging .video-scrubber { width: 24px; height: 24px; } - + /* Create a larger invisible touch target */ .video-scrubber:before { - content: ''; + content: ""; position: absolute; top: -10px; left: -10px; @@ -266,14 +282,14 @@ bottom: -10px; } } - + .video-controls-buttons { display: flex; align-items: center; justify-content: flex-end; gap: 0.75rem; } - + .mute-button, .fullscreen-button { min-width: auto; @@ -283,17 +299,17 @@ cursor: pointer; padding: 0.25rem; transition: transform 0.2s; - + &:hover { transform: scale(1.1); } - + svg { width: 1.25rem; height: 1.25rem; } } - + /* Time tooltip that appears when dragging */ .video-time-tooltip { position: absolute; @@ -309,10 +325,10 @@ white-space: nowrap; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); } - + /* Add a small arrow to the tooltip */ .video-time-tooltip:after { - content: ''; + content: ""; position: absolute; bottom: -4px; left: 50%; @@ -323,4 +339,4 @@ border-right: 4px solid transparent; border-top: 4px solid rgba(0, 0, 0, 0.7); } -} \ No newline at end of file +} diff --git a/frontend-tools/video-editor/package.json b/frontend-tools/video-editor/package.json index f0ccee76..6da3b785 100644 --- a/frontend-tools/video-editor/package.json +++ b/frontend-tools/video-editor/package.json @@ -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" diff --git a/frontend-tools/video-editor/yarn.lock b/frontend-tools/video-editor/yarn.lock index 1ac7c43e..b4347cd5 100644 --- a/frontend-tools/video-editor/yarn.lock +++ b/frontend-tools/video-editor/yarn.lock @@ -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== diff --git a/static/video_editor/video-editor.css b/static/video_editor/video-editor.css index 0187b3b9..0b913b92 100644 --- a/static/video_editor/video-editor.css +++ b/static/video_editor/video-editor.css @@ -1 +1 @@ -#video-editor-trim-root{@keyframes pulse{0%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.05)}to{opacity:.7;transform:scale(1)}}}#video-editor-trim-root .video-player-container{position:relative;width:100%;background:#000;border-radius:.5rem;overflow:hidden;margin-bottom:1rem;aspect-ratio:16/9;-webkit-user-select:none;-moz-user-select:none;user-select:none}#video-editor-trim-root .video-player-container video{width:100%;height:100%;cursor:pointer;transform:translateZ(0);-webkit-transform:translateZ(0);-webkit-user-select:none;-moz-user-select:none;user-select:none}@supports (-webkit-touch-callout: none){#video-editor-trim-root .video-player-container video{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}}#video-editor-trim-root .play-pause-indicator{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:60px;height:60px;background-color:#0009;border-radius:50%;opacity:0;transition:opacity .3s;pointer-events:none}#video-editor-trim-root .video-player-container:hover .play-pause-indicator{opacity:1}#video-editor-trim-root .play-pause-indicator:before{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#video-editor-trim-root .play-pause-indicator.play-icon:before{width:0;height:0;border-top:15px solid transparent;border-bottom:15px solid transparent;border-left:25px solid white;margin-left:3px}#video-editor-trim-root .play-pause-indicator.pause-icon:before{width:20px;height:25px;border-left:6px solid white;border-right:6px solid white}#video-editor-trim-root .ios-first-play-indicator{position:absolute;top:0;left:0;width:100%;height:100%;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:10}#video-editor-trim-root .ios-play-message{color:#fff;font-size:1.2rem;text-align:center;padding:1rem;background:#000c;border-radius:.5rem;animation:pulse 2s infinite}#video-editor-trim-root .video-controls{position:absolute;bottom:0;left:0;right:0;padding:.75rem;background:linear-gradient(transparent,#000000b3);opacity:0;transition:opacity .3s}#video-editor-trim-root .video-player-container:hover .video-controls{opacity:1}#video-editor-trim-root .video-current-time,#video-editor-trim-root .video-duration{color:#fff;font-size:.875rem}#video-editor-trim-root .video-time-display{display:flex;justify-content:space-between;margin-bottom:.5rem;color:#fff;font-size:.875rem}#video-editor-trim-root .video-progress{position:relative;height:6px;background-color:#ffffff4d;border-radius:3px;cursor:pointer;margin:0 10px;touch-action:none;flex-grow:1}#video-editor-trim-root .video-progress.dragging{height:8px}#video-editor-trim-root .video-progress-fill{position:absolute;top:0;left:0;height:100%;background-color:red;border-radius:3px;pointer-events:none}#video-editor-trim-root .video-scrubber{position:absolute;top:50%;transform:translate(-50%,-50%);width:16px;height:16px;background-color:red;border-radius:50%;cursor:grab;transition:transform .1s ease,width .1s ease,height .1s ease}#video-editor-trim-root .video-progress.dragging .video-scrubber{transform:translate(-50%,-50%) scale(1.2);width:18px;height:18px;cursor:grabbing;box-shadow:0 0 8px #f009}@media (pointer: coarse){#video-editor-trim-root .video-scrubber{width:20px;height:20px}#video-editor-trim-root .video-progress.dragging .video-scrubber{width:24px;height:24px}#video-editor-trim-root .video-scrubber:before{content:"";position:absolute;top:-10px;left:-10px;right:-10px;bottom:-10px}}#video-editor-trim-root .video-controls-buttons{display:flex;align-items:center;justify-content:flex-end;gap:.75rem}#video-editor-trim-root .mute-button,#video-editor-trim-root .fullscreen-button{min-width:auto;color:#fff;background:none;border:none;cursor:pointer;padding:.25rem;transition:transform .2s}#video-editor-trim-root .mute-button:hover,#video-editor-trim-root .fullscreen-button:hover{transform:scale(1.1)}#video-editor-trim-root .mute-button svg,#video-editor-trim-root .fullscreen-button svg{width:1.25rem;height:1.25rem}#video-editor-trim-root .video-time-tooltip{position:absolute;top:-30px;background-color:#000000b3;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;font-family:monospace;pointer-events:none;z-index:1000;white-space:nowrap;box-shadow:0 2px 4px #0000004d}#video-editor-trim-root .video-time-tooltip:after{content:"";position:absolute;bottom:-4px;left:50%;transform:translate(-50%);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(0,0,0,.7)}#video-editor-trim-root{@keyframes modal-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes success-pop{0%{transform:scale(0);opacity:0}70%{transform:scale(1.1);opacity:1}to{transform:scale(1);opacity:1}}@keyframes error-pop{0%{transform:scale(0);opacity:0}70%{transform:scale(1.1);opacity:1}to{transform:scale(1);opacity:1}}}#video-editor-trim-root .modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}#video-editor-trim-root .modal-container{background-color:#fff;border-radius:8px;box-shadow:0 4px 12px #00000026;width:90%;max-width:500px;max-height:90vh;overflow-y:auto;animation:modal-fade-in .3s ease-out}#video-editor-trim-root .modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #eee}#video-editor-trim-root .modal-title{margin:0;font-size:1.25rem;font-weight:600;color:#333}#video-editor-trim-root .modal-close-button{background:none;border:none;cursor:pointer;color:#666;padding:4px;display:flex;align-items:center;justify-content:center;transition:color .2s}#video-editor-trim-root .modal-close-button:hover{color:#000}#video-editor-trim-root .modal-content{padding:20px;color:#333;font-size:1rem;line-height:1.5;max-height:400px;overflow-y:auto}#video-editor-trim-root .modal-actions{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid #eee;gap:12px}#video-editor-trim-root .modal-button{padding:8px 16px;border-radius:4px;font-weight:500;cursor:pointer;transition:all .2s;border:none}#video-editor-trim-root .modal-button-primary{background-color:#06c;color:#fff}#video-editor-trim-root .modal-button-primary:hover{background-color:#05a}#video-editor-trim-root .modal-button-secondary{background-color:#f0f0f0;color:#333}#video-editor-trim-root .modal-button-secondary:hover{background-color:#e0e0e0}#video-editor-trim-root .modal-button-danger{background-color:#dc3545;color:#fff}#video-editor-trim-root .modal-button-danger:hover{background-color:#bd2130}#video-editor-trim-root .modal-message{margin-bottom:16px;font-size:1rem}#video-editor-trim-root .modal-spinner{display:flex;align-items:center;justify-content:center;margin:20px 0}#video-editor-trim-root .spinner{border:4px solid rgba(0,0,0,.1);border-radius:50%;border-top:4px solid #0066cc;width:30px;height:30px;animation:spin 1s linear infinite}#video-editor-trim-root .modal-success-icon{display:flex;justify-content:center;margin-bottom:16px;color:#28a745;font-size:2rem}#video-editor-trim-root .modal-success-icon svg{width:60px;height:60px;color:#4caf50;animation:success-pop .5s ease-out}#video-editor-trim-root .modal-error-icon{display:flex;justify-content:center;margin-bottom:16px;color:#dc3545;font-size:2rem}#video-editor-trim-root .modal-error-icon svg{width:60px;height:60px;color:#f44336;animation:error-pop .5s ease-out}#video-editor-trim-root .modal-choices{display:flex;flex-direction:column;gap:10px;margin-top:20px}#video-editor-trim-root .modal-choice-button{padding:12px 16px;border:none;border-radius:4px;background-color:#06c;text-align:center;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;font-weight:500;text-decoration:none;color:#fff}#video-editor-trim-root .modal-choice-button:hover{background-color:#05a;transform:translateY(-1px);box-shadow:0 2px 5px #0000001a}#video-editor-trim-root .modal-choice-button svg{margin-right:8px}#video-editor-trim-root .success-link{background-color:#4caf50}#video-editor-trim-root .success-link:hover{background-color:#3d8b40}#video-editor-trim-root .centered-choice{margin:0 auto;width:auto;min-width:220px;background-color:#06c;color:#fff}#video-editor-trim-root .centered-choice:hover{background-color:#05a}@media (max-width: 480px){#video-editor-trim-root .modal-container{width:95%}#video-editor-trim-root .modal-actions{flex-direction:column}#video-editor-trim-root .modal-button{width:100%}}#video-editor-trim-root .error-message{color:#f44336;font-weight:500;background-color:#f443361a;padding:10px;border-radius:4px;border-left:4px solid #F44336;margin-top:10px}#video-editor-trim-root .redirect-message{color:#555;font-size:.95rem;padding:0;margin:0}#video-editor-trim-root .countdown{font-weight:700;color:#06c;font-size:1.1rem}#video-editor-trim-root{@keyframes spin{to{transform:rotate(360deg)}}@keyframes fadeIn{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}}#video-editor-trim-root .timeline-container-card{background-color:#fff;border-radius:.5rem;padding:1rem;box-shadow:0 1px 2px #0000000d}#video-editor-trim-root .timeline-header{margin-bottom:.75rem;display:flex;justify-content:space-between;align-items:center}#video-editor-trim-root .timeline-title{font-size:.875rem;font-weight:500;color:var(--foreground, #333)}#video-editor-trim-root .timeline-title-text{font-weight:700}#video-editor-trim-root .current-time{font-size:.875rem;color:var(--foreground, #333)}#video-editor-trim-root .time-code{font-family:monospace;background-color:#f3f4f6;padding:0 .5rem;border-radius:.25rem}#video-editor-trim-root .duration-time{font-size:.875rem;color:var(--foreground, #333)}#video-editor-trim-root .timeline-scroll-container{position:relative;overflow:visible!important}#video-editor-trim-root .timeline-container{position:relative;min-width:100%;background-color:#fafbfc;height:70px;border-radius:.25rem;overflow:visible!important}#video-editor-trim-root .timeline-marker{position:absolute;height:82px;width:2px;background-color:#000;transform:translate(-50%);z-index:50;pointer-events:none}#video-editor-trim-root .timeline-marker-head{position:absolute;top:-6px;left:50%;transform:translate(-50%);width:16px;height:16px;background-color:#ef4444;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;pointer-events:auto;z-index:51}#video-editor-trim-root .timeline-marker-drag{position:absolute;bottom:-12px;left:50%;transform:translate(-50%);width:16px;height:16px;background-color:#4b5563;border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;pointer-events:auto;z-index:51}#video-editor-trim-root .timeline-marker-drag.dragging{cursor:grabbing;background-color:#374151}#video-editor-trim-root .timeline-marker-head-icon{color:#fff;font-size:14px;font-weight:700;line-height:1;-webkit-user-select:none;-moz-user-select:none;user-select:none}#video-editor-trim-root .timeline-marker-drag-icon{color:#fff;font-size:12px;line-height:1;-webkit-user-select:none;-moz-user-select:none;user-select:none;transform:rotate(90deg);display:inline-block}#video-editor-trim-root .trim-line-marker{position:absolute;top:0;bottom:0;width:1px;background-color:#00000080;z-index:20}#video-editor-trim-root .trim-handle{position:absolute;width:10px;height:20px;background-color:#000;cursor:ew-resize}#video-editor-trim-root .trim-handle.left{right:0;top:10px;border-radius:3px 0 0 3px}#video-editor-trim-root .trim-handle.right{left:0;top:10px;border-radius:0 3px 3px 0}#video-editor-trim-root .timeline-thumbnail{display:inline-block;height:70px;border-right:1px solid rgba(0,0,0,.03)}#video-editor-trim-root .split-point{position:absolute;top:0;bottom:0;width:1px;background-color:#ff000080;z-index:15}#video-editor-trim-root .clip-segment{position:absolute;height:70px;border-radius:4px;z-index:10;border:2px solid rgba(0,0,0,.15);cursor:pointer}#video-editor-trim-root .clip-segment:hover{box-shadow:0 0 0 2px #0000004d;border-color:#0006;background-color:#f0f0f0cc!important}#video-editor-trim-root .clip-segment.selected{box-shadow:0 0 0 2px #3b82f6b3;border-color:#3b82f6e6}#video-editor-trim-root .clip-segment.selected:hover{background-color:#f0f8ffd9!important}#video-editor-trim-root .clip-segment-info{position:absolute;bottom:0;left:0;right:0;padding:.4rem;background-color:#0006;color:#fff;opacity:1;transition:background-color .2s;line-height:1.3}#video-editor-trim-root .clip-segment:hover .clip-segment-info{background-color:#00000080}#video-editor-trim-root .clip-segment.selected .clip-segment-info{background-color:#3b82f680}#video-editor-trim-root .clip-segment.selected:hover .clip-segment-info{background-color:#3b82f666}#video-editor-trim-root .clip-segment-name{font-weight:700;font-size:12px}#video-editor-trim-root .clip-segment-time,#video-editor-trim-root .clip-segment-duration{font-size:10px}#video-editor-trim-root .clip-segment-handle{position:absolute;top:0;bottom:0;width:6px;background-color:#0003;cursor:ew-resize}#video-editor-trim-root .clip-segment-handle:hover{background-color:#0006}#video-editor-trim-root .clip-segment-handle.left{left:0;border-radius:2px 0 0 2px}#video-editor-trim-root .clip-segment-handle.right{right:0;border-radius:0 2px 2px 0}@media (pointer: coarse){#video-editor-trim-root .clip-segment-handle{width:14px;background-color:#0006}#video-editor-trim-root .clip-segment-handle:after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:2px;height:20px;background-color:#fffc;border-radius:1px}#video-editor-trim-root .clip-segment-handle.left:after{box-shadow:-2px 0 #00000080}#video-editor-trim-root .clip-segment-handle.right:after{box-shadow:2px 0 #00000080}#video-editor-trim-root .clip-segment-handle:active{background-color:#0009}#video-editor-trim-root .timeline-marker{height:85px}#video-editor-trim-root .timeline-marker-head{width:24px;height:24px;top:-13px}#video-editor-trim-root .timeline-marker-drag{width:24px;height:24px;bottom:-18px}#video-editor-trim-root .timeline-marker-head.dragging{width:28px;height:28px;top:-15px}}#video-editor-trim-root .segment-tooltip,#video-editor-trim-root .empty-space-tooltip{position:absolute;background-color:#fff;border-radius:4px;box-shadow:0 2px 8px #0000004d;padding:.5rem;z-index:1000;min-width:150px;text-align:center;pointer-events:auto;top:-100px!important;transform:translateY(-10px)}#video-editor-trim-root .segment-tooltip:after,#video-editor-trim-root .empty-space-tooltip:after{content:"";position:absolute;bottom:-5px;left:50%;transform:translate(-50%);width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid white}#video-editor-trim-root .segment-tooltip:before,#video-editor-trim-root .empty-space-tooltip:before{content:"";position:absolute;bottom:-6px;left:50%;transform:translate(-50%);width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid rgba(0,0,0,.1);z-index:-1}#video-editor-trim-root .tooltip-time{font-weight:600;font-size:.875rem;margin-bottom:.5rem;color:#333}#video-editor-trim-root .tooltip-actions{display:flex;justify-content:center;gap:.5rem}#video-editor-trim-root .tooltip-action-btn{background-color:#f3f4f6;border:none;border-radius:.25rem;padding:.375rem;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#4b5563;min-width:20px!important}#video-editor-trim-root .tooltip-action-btn:hover{background-color:#e5e7eb;color:#111827}#video-editor-trim-root .tooltip-action-btn.delete{color:#ef4444}#video-editor-trim-root .tooltip-action-btn.delete:hover{background-color:#fee2e2}#video-editor-trim-root .tooltip-action-btn.new-segment{padding:.375rem .5rem}#video-editor-trim-root .tooltip-action-btn.new-segment .tooltip-btn-text{margin-left:.25rem;font-size:.75rem}#video-editor-trim-root .tooltip-action-btn svg{width:1rem;height:1rem}#video-editor-trim-root .timeline-controls{display:flex;align-items:center;justify-content:space-between;margin-top:.75rem}#video-editor-trim-root .time-navigation{display:none;align-items:center;gap:.5rem}#video-editor-trim-root .time-nav-label{font-size:.875rem;font-weight:500}#video-editor-trim-root .time-input{border:1px solid #d1d5db;border-radius:.25rem;padding:.25rem .5rem;width:8rem;font-size:.875rem}#video-editor-trim-root .time-button-group{display:flex}#video-editor-trim-root .time-button{background-color:#e5e7eb;color:#000;padding:.25rem .5rem;font-size:.875rem;border:none;cursor:pointer;margin-right:.5rem}#video-editor-trim-root .time-button:hover{background-color:#d1d5db}#video-editor-trim-root .time-button:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}#video-editor-trim-root .time-button:last-child{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}#video-editor-trim-root .controls-right{display:flex;align-items:center;gap:.5rem;margin-left:auto}#video-editor-trim-root .zoom-dropdown-container{position:relative;z-index:100;display:none}#video-editor-trim-root .zoom-button{background-color:#374151;color:#fff;border:none;border-radius:.25rem;padding:.25rem .75rem;font-size:.875rem;display:flex;align-items:center;cursor:pointer}#video-editor-trim-root .zoom-button:hover{background-color:#1f2937}#video-editor-trim-root .zoom-button svg{margin-left:.25rem}#video-editor-trim-root .zoom-dropdown{position:absolute;top:100%;left:0;margin-top:.25rem;width:9rem;background-color:#374151;color:#fff;border-radius:.25rem;box-shadow:0 4px 6px -1px #0000001a;z-index:50;max-height:300px;overflow-y:auto}#video-editor-trim-root .zoom-option{padding:.25rem .75rem;cursor:pointer}#video-editor-trim-root .zoom-option:hover{background-color:#4b5563}#video-editor-trim-root .zoom-option.selected{background-color:#6b7280;display:flex;align-items:center}#video-editor-trim-root .zoom-option svg{margin-right:.25rem}#video-editor-trim-root .save-buttons-row{display:flex;align-items:center;gap:.5rem;margin:0;flex-wrap:nowrap}#video-editor-trim-root .save-button,#video-editor-trim-root .save-copy-button,#video-editor-trim-root .save-segments-button{color:#fff;background:#06c;border-radius:.25rem;font-size:.75rem;padding:.25rem .5rem;cursor:pointer;border:none;white-space:nowrap;transition:background-color .2s;min-width:-moz-fit-content;min-width:fit-content}#video-editor-trim-root .save-button:hover,#video-editor-trim-root .save-copy-button:hover,#video-editor-trim-root .save-segments-button:hover{background-color:#0056b3}@media (max-width: 576px){#video-editor-trim-root .save-buttons-row{width:100%;justify-content:space-between;gap:.5rem}#video-editor-trim-root .save-button,#video-editor-trim-root .save-copy-button,#video-editor-trim-root .save-segments-button{flex:1;font-size:.7rem;padding:.25rem .35rem}}@media (max-width: 480px){#video-editor-trim-root .save-button,#video-editor-trim-root .save-copy-button,#video-editor-trim-root .save-segments-button{font-size:.675rem;padding:.25rem}#video-editor-trim-root .controls-right,#video-editor-trim-root .controls-right button{margin:0}}#video-editor-trim-root .modal-success-content,#video-editor-trim-root .modal-error-content{display:flex;flex-direction:column;align-items:center;padding:1rem;text-align:center;padding:0;margin:0}#video-editor-trim-root .modal-success-icon,#video-editor-trim-root .modal-error-icon{margin-bottom:1rem}#video-editor-trim-root .modal-success-icon svg{color:#4caf50;animation:fadeIn .5s ease-in-out}#video-editor-trim-root .modal-error-icon svg{color:#f44336;animation:fadeIn .5s ease-in-out}#video-editor-trim-root .success-link{background-color:#4caf50;color:#fff;transition:background-color .3s}#video-editor-trim-root .success-link:hover{background-color:#388e3c}#video-editor-trim-root .error-message{color:#f44336;font-weight:500}#video-editor-trim-root .modal-spinner{display:flex;justify-content:center;margin:2rem 0}#video-editor-trim-root .spinner{width:50px;height:50px;border:5px solid rgba(0,0,0,.1);border-radius:50%;border-top-color:#06c;animation:spin 1s ease-in-out infinite}#video-editor-trim-root .text-center{text-align:center}#video-editor-trim-root .modal-message{margin-bottom:1rem;line-height:1.5}#video-editor-trim-root .modal-choice-button{display:flex;align-items:center;justify-content:center;padding:.75rem 1.25rem;background-color:#06c;color:#fff;border-radius:4px;text-decoration:none;margin:0 auto;cursor:pointer;font-weight:500;gap:.5rem;border:none;transition:background-color .3s}#video-editor-trim-root .modal-choice-button:hover{background-color:#0056b3}#video-editor-trim-root .modal-choice-button svg{flex-shrink:0}#video-editor-trim-root .centered-choice{margin:0 auto;min-width:180px}.mobile-timeline-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#00000080;z-index:50;display:flex;justify-content:center;align-items:center;border-radius:.5rem;pointer-events:none}.mobile-timeline-message{background-color:#000c;border-radius:8px;padding:15px 25px;text-align:center;max-width:80%;animation:pulse 2s infinite}.mobile-timeline-message p{color:#fff;font-size:16px;margin:0 0 15px;font-weight:500}.mobile-play-icon{width:0;height:0;border-top:15px solid transparent;border-bottom:15px solid transparent;border-left:25px solid white;margin:0 auto}@keyframes pulse{0%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.05)}to{opacity:.7;transform:scale(1)}}.preview-mode .tooltip-action-btn,.preview-mode .tooltip-time-btn{opacity:.5;pointer-events:none;cursor:not-allowed}.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:.5;pointer-events:none;cursor:not-allowed}.timeline-container-card.preview-mode .clip-segment:hover{box-shadow:none;border-color:#00000026;background-color:inherit!important}.segments-playback-mode .tooltip-time-btn{opacity:1;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:.5;pointer-events:none}.segments-playback-mode .tooltip-action-btn.play,.segments-playback-mode .tooltip-action-btn.pause{opacity:1;cursor:pointer}.segments-playback-message{display:flex;align-items:center;background-color:#3b82f61a;color:#3b82f6;padding:6px 12px;border-radius:4px;font-weight:600;font-size:.875rem;animation:pulse 2s infinite}.segments-playback-message svg{height:1.25rem;width:1.25rem;margin-right:.5rem;color:#3b82f6}.two-row-tooltip{display:flex;flex-direction:column;background-color:#fff;padding:6px;border-radius:4px;box-shadow:0 2px 8px #00000026;position:relative;z-index:3000}.tooltip-time-btn[data-tooltip="Decrease by 100ms"],.tooltip-time-btn[data-tooltip="Increase by 100ms"]{display:none!important}.tooltip-row{display:flex;justify-content:space-between;align-items:center;gap:3px}.tooltip-row:first-child{margin-bottom:6px}.tooltip-time-btn{background-color:#f0f0f0!important;border:none!important;border-radius:4px!important;padding:4px 8px!important;font-size:.75rem!important;font-weight:500!important;color:#333!important;cursor:pointer!important;transition:background-color .2s!important;min-width:20px!important}.tooltip-time-btn:hover{background-color:#e0e0e0!important}.tooltip-time-display{font-family:monospace!important;font-size:.875rem!important;font-weight:600!important;color:#333!important;padding:4px 6px!important;background-color:#f7f7f7!important;border-radius:4px!important;min-width:100px!important;text-align:center!important;overflow:hidden!important}.tooltip-actions{display:flex;justify-content:space-between;align-items:center;gap:3px;position:relative;z-index:2500}.tooltip-action-btn{background-color:#f3f4f6;border:none;border-radius:4px;padding:5px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#4b5563;width:26px;height:26px;min-width:20px!important;position:relative}.tooltip-action-btn[data-tooltip]:before{content:attr(data-tooltip);position:absolute;height:30px;top:35px;left:50%;transform:translate(-50%);margin-left:0;background-color:#000000d9;color:#fff;text-align:left;padding:6px 12px;border-radius:4px;box-shadow:0 2px 8px #0003;font-size:12px;white-space:nowrap;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;z-index:2500;pointer-events:none}.tooltip-action-btn[data-tooltip]:after{content:"";position:absolute;top:35px;left:50%;transform:translate(-50%);border-width:4px;border-style:solid;border-color:rgba(0,0,0,.85) transparent transparent transparent;margin-left:0;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;z-index:2500;pointer-events:none}@media (hover: hover) and (pointer: fine){.tooltip-action-btn[data-tooltip]:hover:before,.tooltip-action-btn[data-tooltip]:hover:after{opacity:1;visibility:visible}}@media (pointer: coarse){.tooltip-action-btn[data-tooltip]:before,.tooltip-action-btn[data-tooltip]:after{display:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important;content:none!important}}.tooltip-action-btn:hover{background-color:#e5e7eb;color:#111827}.tooltip-action-btn.delete{color:#ef4444}.tooltip-action-btn.delete:hover{background-color:#fee2e2}.tooltip-action-btn.play{color:#10b981}.tooltip-action-btn.play:hover{background-color:#d1fae5}.tooltip-action-btn.pause{color:#3b82f6}.tooltip-action-btn.pause:hover{background-color:#dbeafe}.tooltip-action-btn.play-from-start{color:#4f46e5}.tooltip-action-btn.play-from-start:hover{background-color:#e0e7ff}.tooltip-action-btn svg{width:16px;height:16px}.tooltip-action-btn.new-segment{width:auto;height:auto;padding:6px 10px;display:flex;flex-direction:row;color:#10b981}.tooltip-action-btn.new-segment:hover{background-color:#d1fae5}.tooltip-action-btn.new-segment .tooltip-btn-text{margin-left:6px;font-size:.75rem;white-space:nowrap}.tooltip-action-btn.disabled{opacity:.5;cursor:not-allowed;background-color:#f3f4f6}.tooltip-action-btn.disabled:hover{background-color:#f3f4f6;color:#9ca3af}.tooltip-action-btn.disabled svg{color:#9ca3af}.tooltip-action-btn.disabled .tooltip-btn-text{color:#9ca3af}@media (max-width: 768px){.two-row-tooltip{padding:4px}.tooltip-row:first-child{margin-bottom:4px}.tooltip-time-btn{min-width:20px!important;font-size:.7rem!important;padding:3px 6px!important}.tooltip-time-display{font-size:.8rem!important;padding:3px 4px!important;min-width:90px!important}.tooltip-action-btn{width:24px;height:24px;padding:4px}.tooltip-action-btn.new-segment{padding:4px 8px}.tooltip-action-btn svg{width:14px;height:14px}.tooltip-action-btn[data-tooltip]:before{min-width:100px;font-size:11px;padding:4px 8px;height:24px;top:33px}.tooltip-action-btn[data-tooltip]:after{top:33px}}#video-editor-trim-root{@keyframes bluePulse{0%{box-shadow:0 0 #3b82f666}50%{box-shadow:0 0 0 8px #3b82f600}to{box-shadow:0 0 #3b82f600}}@keyframes pulse{0%{opacity:.8}50%{opacity:1}to{opacity:.8}}}#video-editor-trim-root .editing-tools-container{background-color:#fff;border-radius:.5rem;padding:1rem;margin-bottom:2.5rem;box-shadow:0 1px 2px #0000000d}#video-editor-trim-root .flex-container{display:flex;justify-content:space-between;align-items:center;position:relative;gap:15px;width:100%}#video-editor-trim-root .flex-container.single-row{flex-wrap:nowrap}#video-editor-trim-root .full-text{display:inline}#video-editor-trim-root .short-text{display:none}#video-editor-trim-root .reset-text{display:inline}#video-editor-trim-root .button-group{display:flex;align-items:center}#video-editor-trim-root .button-group.play-buttons-group{gap:.75rem;justify-content:flex-start;flex:0 0 auto}#video-editor-trim-root .button-group.secondary{gap:.75rem;align-items:center;justify-content:flex-end;margin-left:auto}#video-editor-trim-root .button-group button{display:flex;align-items:center;color:#333;background:none;border:none;cursor:pointer;min-width:auto}#video-editor-trim-root .button-group button:hover:not(:disabled){color:inherit}#video-editor-trim-root .button-group button:disabled{opacity:.5;cursor:not-allowed}#video-editor-trim-root .button-group button svg{height:1.25rem;width:1.25rem;margin-right:.25rem}#video-editor-trim-root .divider{border-right:1px solid #d1d5db;height:1.5rem;margin:0 .5rem}#video-editor-trim-root .play-button,#video-editor-trim-root .preview-button{font-weight:600;display:flex;align-items:center;position:relative;overflow:hidden;min-width:80px;justify-content:center;font-size:.875rem!important}#video-editor-trim-root .play-button.greyed-out{opacity:.5;cursor:not-allowed}#video-editor-trim-root .segments-button.highlighted-stop{background-color:#3b82f61a;color:#3b82f6;border:1px solid #3b82f6;animation:bluePulse 2s infinite}#video-editor-trim-root .play-button:hover:not(:disabled),#video-editor-trim-root .preview-button:hover:not(:disabled){color:inherit!important;transform:none!important;font-size:.875rem!important;width:auto!important;background:none!important}#video-editor-trim-root .play-button svg,#video-editor-trim-root .preview-button svg{height:1.5rem;width:1.5rem;flex-shrink:0}#video-editor-trim-root .preview-mode-message{display:flex;align-items:center;background-color:#3b82f61a;color:#3b82f6;padding:6px 12px;border-radius:4px;font-weight:600;font-size:.875rem;animation:pulse 2s infinite}#video-editor-trim-root .preview-mode-message svg{height:1.25rem;width:1.25rem;margin-right:.5rem;color:#3b82f6}#video-editor-trim-root .button-text{margin-left:.25rem}@media (max-width: 992px){#video-editor-trim-root .button-group.secondary .button-text{display:none}}@media (max-width: 768px){#video-editor-trim-root .flex-container.single-row{justify-content:space-between}#video-editor-trim-root .button-group{gap:.5rem}#video-editor-trim-root .preview-button,#video-editor-trim-root .play-button{font-size:.875rem!important}}@media (max-width: 640px){#video-editor-trim-root .editing-tools-container{padding:.75rem;overflow-x:hidden}#video-editor-trim-root .preview-button{min-width:auto}#video-editor-trim-root .full-text{display:none}#video-editor-trim-root .short-text{display:inline;margin-left:.15rem}#video-editor-trim-root .reset-text{display:none}#video-editor-trim-root .button-group.play-buttons-group{flex:initial;justify-content:flex-start;flex-shrink:0}#video-editor-trim-root .button-group.secondary{flex:initial;justify-content:flex-end;flex-shrink:0}#video-editor-trim-root .button-group button{padding:.375rem;min-width:auto}#video-editor-trim-root .button-group button svg{height:1.125rem;width:1.125rem;margin-right:.125rem}}@media (max-width: 576px){#video-editor-trim-root .flex-container.single-row{justify-content:space-between;flex-wrap:nowrap;gap:10px}#video-editor-trim-root .button-group.play-buttons-group{justify-content:flex-start;flex:0 0 auto}#video-editor-trim-root .button-group.secondary{justify-content:flex-end;margin-left:auto}#video-editor-trim-root .button-group button{padding:.25rem}#video-editor-trim-root .preview-mode-message{font-size:.8rem;padding:4px 8px}#video-editor-trim-root .divider{margin:0 .25rem}}@media (max-width: 480px){#video-editor-trim-root .editing-tools-container{padding:.5rem}#video-editor-trim-root .flex-container.single-row{gap:8px}#video-editor-trim-root .button-group.play-buttons-group,#video-editor-trim-root .button-group.secondary{gap:.25rem}#video-editor-trim-root .divider{display:none}#video-editor-trim-root .button-group button{padding:.125rem}#video-editor-trim-root .button-group button svg{height:1rem;width:1rem;margin-right:0}#video-editor-trim-root .button-text,#video-editor-trim-root .reset-text{display:none}}@media (max-width: 640px) and (orientation: portrait){#video-editor-trim-root .editing-tools-container{width:100%;box-sizing:border-box}#video-editor-trim-root .flex-container.single-row{width:100%;padding:0;margin:0}#video-editor-trim-root .button-group{max-width:50%}#video-editor-trim-root .button-group.play-buttons-group{max-width:60%}#video-editor-trim-root .button-group.secondary{max-width:40%}}@media (hover: hover) and (pointer: fine){#video-editor-trim-root [data-tooltip]{position:relative}#video-editor-trim-root [data-tooltip]:before{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translate(-50%);margin-bottom:5px;background-color:#000c;color:#fff;text-align:center;padding:5px 10px;border-radius:3px;font-size:12px;white-space:nowrap;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;z-index:1000;pointer-events:none}#video-editor-trim-root [data-tooltip]:after{content:"";position:absolute;bottom:100%;left:50%;transform:translate(-50%);border-width:5px;border-style:solid;border-color:rgba(0,0,0,.8) transparent transparent transparent;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;pointer-events:none}#video-editor-trim-root [data-tooltip]:hover:before,#video-editor-trim-root [data-tooltip]:hover:after{opacity:1;visibility:visible}}@media (pointer: coarse){#video-editor-trim-root [data-tooltip]:before,#video-editor-trim-root [data-tooltip]:after{display:none!important;content:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important}}#video-editor-trim-root .clip-segments-container{margin-top:1rem;background-color:#fff;border-radius:.5rem;padding:1rem;box-shadow:0 1px 2px #0000000d}#video-editor-trim-root .clip-segments-title{font-size:.875rem;font-weight:500;color:var(--foreground, #333);margin-bottom:.75rem}#video-editor-trim-root .segment-item{display:flex;align-items:center;justify-content:space-between;padding:.5rem;border:1px solid #e5e7eb;border-radius:.25rem;margin-bottom:.5rem;transition:box-shadow .2s ease}#video-editor-trim-root .segment-item:hover{box-shadow:0 4px 6px -1px #0000001a}#video-editor-trim-root .segment-content{display:flex;align-items:center}#video-editor-trim-root .segment-thumbnail{width:4rem;height:2.25rem;background-size:cover;background-position:center;border-radius:.25rem;margin-right:.75rem;box-shadow:0 0 0 1px #ffffff4d}#video-editor-trim-root .segment-info{display:flex;flex-direction:column}#video-editor-trim-root .segment-title{font-weight:500;font-size:.875rem;color:#000}#video-editor-trim-root .segment-time{font-size:.75rem;color:#000}#video-editor-trim-root .segment-duration{font-size:.75rem;margin-top:.25rem;display:inline-block;background-color:#f3f4f6;padding:0 .5rem;border-radius:.25rem;color:#000}#video-editor-trim-root .segment-actions{display:flex;align-items:center;gap:.5rem}#video-editor-trim-root .delete-button{padding:.375rem;color:#4b5563;background-color:#e5e7eb;border-radius:9999px;border:none;cursor:pointer;transition:background-color .2s,color .2s;min-width:auto}#video-editor-trim-root .delete-button:hover{color:#000;background-color:#d1d5db}#video-editor-trim-root .delete-button svg{height:1rem;width:1rem}#video-editor-trim-root .empty-message{padding:1rem;text-align:center;color:#333333b3}#video-editor-trim-root .segment-color-1{background-color:#3b82f626}#video-editor-trim-root .segment-color-2{background-color:#10b98126}#video-editor-trim-root .segment-color-3{background-color:#f59e0b26}#video-editor-trim-root .segment-color-4{background-color:#ef444426}#video-editor-trim-root .segment-color-5{background-color:#8b5cf626}#video-editor-trim-root .segment-color-6{background-color:#ec489926}#video-editor-trim-root .segment-color-7{background-color:#06b6d426}#video-editor-trim-root .segment-color-8{background-color:#facc1526}.mobile-play-prompt-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000000b3;display:flex;justify-content:center;align-items:center;z-index:1000;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)}.mobile-play-prompt{background-color:#fff;width:90%;max-width:400px;border-radius:12px;padding:25px;box-shadow:0 4px 20px #00000040;text-align:center}.mobile-play-prompt h3{margin:0 0 15px;font-size:20px;color:#333;font-weight:600}.mobile-play-prompt p{margin:0 0 15px;font-size:16px;color:#444;line-height:1.5}.mobile-prompt-instructions{margin:20px 0;text-align:left;background-color:#f8f9fa;padding:15px;border-radius:8px}.mobile-prompt-instructions p{margin:0 0 8px;font-size:15px;font-weight:500}.mobile-prompt-instructions ol{margin:0;padding-left:22px}.mobile-prompt-instructions li{margin-bottom:8px;font-size:14px;color:#333}.mobile-play-button{background-color:#007bff;color:#fff;border:none;border-radius:8px;padding:12px 25px;font-size:16px;font-weight:500;cursor:pointer;transition:background-color .2s;margin-top:5px;min-height:44px;min-width:200px}.mobile-play-button:hover{background-color:#0069d9}.mobile-play-button:active{background-color:#0062cc;transform:scale(.98)}@supports (-webkit-touch-callout: none){.mobile-play-button{padding:14px 25px}}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}*{border-color:hsl(var(--border))}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.inline{display:inline}.flex{display:flex}.hidden{display:none}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-6xl{max-width:72rem}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.resize{resize:both}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-md{border-radius:calc(var(--radius) - 2px)}.border{border-width:1px}.bg-background{background-color:hsl(var(--background))}.bg-indigo-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity, 1))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.text-center{text-align:center}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.paused{animation-play-state:paused}:root{--foreground: 20 14.3% 4.1%;--muted: 60 4.8% 95.9%;--muted-foreground: 25 5.3% 44.7%;--popover: 0 0% 100%;--popover-foreground: 20 14.3% 4.1%;--card: 0 0% 100%;--card-foreground: 20 14.3% 4.1%;--border: 20 5.9% 90%;--input: 20 5.9% 90%;--primary: 207 90% 54%;--primary-foreground: 211 100% 99%;--secondary: 30 84% 54%;--secondary-foreground: 60 9.1% 97.8%;--accent: 60 4.8% 95.9%;--accent-foreground: 24 9.8% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 60 9.1% 97.8%;--ring: 20 14.3% 4.1%;--radius: .5rem}.video-player{position:relative;width:100%;background-color:#000;overflow:hidden;border-radius:.5rem}.video-controls{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:1rem;display:flex;flex-direction:column}.video-current-time{color:#fff;font-weight:500}.video-progress{position:relative;height:4px;background-color:#ffffff4d;border-radius:2px;margin-bottom:1rem}.video-progress-fill{position:absolute;left:0;top:0;height:100%;background-color:hsl(var(--primary));border-radius:2px}.video-scrubber{position:absolute;width:12px;height:12px;margin-left:-6px;background-color:#fff;border-radius:50%;top:-4px}.video-player-container{position:relative;overflow:hidden}.play-pause-indicator{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70px;height:70px;border-radius:50%;background-color:#00000080;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none;background-position:center;background-repeat:no-repeat}.play-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='36' height='36' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E")}.pause-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='36' height='36' fill='white'%3E%3Cpath d='M6 19h4V5H6v14zm8-14v14h4V5h-4z'/%3E%3C/svg%3E")}.video-player-container:hover .play-pause-indicator{opacity:1}.timeline-scroll-container{height:6rem;border-radius:.375rem;overflow-x:auto;overflow-y:hidden;margin-bottom:.75rem;background-color:#eee;position:relative}.timeline-container{position:relative;background-color:#eee;height:6rem;width:100%;cursor:pointer;transition:width .3s ease}.timeline-marker{position:absolute;top:-10px;height:calc(100% + 10px);width:2px;background-color:red;z-index:100;pointer-events:none;box-shadow:0 0 4px #ff000080}.trim-line-marker{position:absolute;top:0;bottom:0;width:2px;background-color:#007bffe6;z-index:10}.trim-handle{width:8px;background-color:#6c757de6;position:absolute;top:0;bottom:0;cursor:ew-resize;z-index:15}.trim-handle.left{left:-4px}.trim-handle.right{right:-4px}.timeline-thumbnail{height:100%;border-right:1px solid rgba(0,0,0,.1);position:relative;display:inline-block;background-size:cover;background-position:center}.split-point{position:absolute;width:2px;background-color:#6c757de6;top:0;bottom:0;z-index:5}.clip-segment{position:absolute;height:95%;top:0;border-radius:4px;background-size:cover;background-position:center;background-blend-mode:soft-light;box-shadow:0 2px 8px #0003;overflow:hidden;cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:box-shadow .2s,transform .1s;z-index:15}.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,.9)}.clip-segment:nth-child(2n),.segment-color-2,.segment-color-4,.segment-color-6,.segment-color-8{background-color:transparent;border:2px solid rgba(108,117,125,.9)}.clip-segment:hover{box-shadow:0 4px 12px #0000004d;transform:translateY(-1px);filter:brightness(1.1)}.clip-segment:active{cursor:grabbing;box-shadow:0 2px 6px #0000004d;transform:translateY(0)}.clip-segment.selected{border-width:3px;box-shadow:0 4px 12px #0006;z-index:25;filter:brightness(1.2)}.clip-segment-info{background-color:#e2e6eae6;color:#000;padding:6px 8px;font-size:.7rem;position:absolute;top:0;left:0;width:100%;border-radius:4px 4px 0 0;z-index:2;display:flex;flex-direction:column;gap:2px}.clip-segment-name{font-weight:700;color:#000}.clip-segment-time{font-size:.65rem;color:#000}.clip-segment-duration{font-size:.65rem;color:#000;background:#b3d9ff66;padding:1px 4px;border-radius:2px;display:inline-block;margin-top:2px}.clip-segment-handle{position:absolute;width:8px;top:0;bottom:0;background-color:#6c757de6;cursor:ew-resize;z-index:20;display:flex;align-items:center;justify-content:center}.clip-segment-handle:after{content:"↔";color:#fff;font-size:12px;text-shadow:0 0 2px rgba(0,0,0,.8)}.clip-segment-handle.left{left:0}.clip-segment-handle.right{right:0}.clip-segment-handle:hover{background-color:#007bffe6;width:10px}input[type=range]{-webkit-appearance:none;height:6px;background:#e0e0e0;border-radius:3px}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;height:16px;width:16px;border-radius:50%;background:#007bffe6;cursor:pointer}[data-tooltip]{position:relative;cursor:pointer}[data-tooltip]:before{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translate(-50%);margin-bottom:8px;background-color:#000c;color:#fff;padding:5px 10px;border-radius:4px;font-size:.8rem;white-space:nowrap;z-index:1000;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;pointer-events:none}[data-tooltip]:after{content:"";position:absolute;bottom:100%;left:50%;transform:translate(-50%);border-width:5px;border-style:solid;border-color:rgba(0,0,0,.8) transparent transparent transparent;margin-bottom:0;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;pointer-events:none}@media (hover: hover) and (pointer: fine){[data-tooltip]:hover:before,[data-tooltip]:hover:after{opacity:1;visibility:visible}}@media (pointer: coarse){[data-tooltip]:before,[data-tooltip]:after{display:none!important;content:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important}}button[disabled][data-tooltip]:before,button[disabled][data-tooltip]:after{opacity:.5}.tooltip-action-btn{position:relative}.tooltip-action-btn[data-tooltip]:before,.tooltip-action-btn[data-tooltip]:after{opacity:0;visibility:hidden;position:absolute;pointer-events:none;transition:all .3s ease}.tooltip-action-btn[data-tooltip]:before{content:attr(data-tooltip);background-color:#000c;color:#fff;font-size:12px;padding:4px 8px;border-radius:3px;white-space:nowrap;bottom:-35px;left:50%;transform:translate(-50%);z-index:9999}.tooltip-action-btn[data-tooltip]:after{content:"";border-width:5px;border-style:solid;border-color:transparent transparent rgba(0,0,0,.8) transparent;bottom:-15px;left:50%;transform:translate(-50%);z-index:9999}@media (hover: hover) and (pointer: fine){.tooltip-action-btn:hover[data-tooltip]:before,.tooltip-action-btn:hover[data-tooltip]:after{opacity:1;visibility:visible}}.segment-tooltip{background-color:#b3d9fff2;color:#000;border-radius:4px;padding:6px;min-width:140px;z-index:1000;box-shadow:0 3px 10px #0003}.segment-tooltip:after{content:"";position:absolute;bottom:-6px;left:50%;transform:translate(-50%);width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid rgba(179,217,255,.95)}.tooltip-time{font-size:.85rem;font-weight:700;text-align:center;margin-bottom:6px;color:#000}.tooltip-actions{display:flex;justify-content:space-between;gap:5px;position:relative}.tooltip-action-btn{background-color:#007bff33;border:none;border-radius:3px;width:30px;height:30px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:6px;transition:background-color .2s;min-width:20px!important}.tooltip-action-btn:hover{background-color:#007bff66}.tooltip-action-btn svg{width:100%;height:100%;stroke:currentColor}.tooltip-action-btn.set-in svg,.tooltip-action-btn.set-out svg{width:100%;height:100%;margin:0 auto;fill:currentColor;stroke:none}.empty-space-tooltip{background-color:#fff;border-radius:6px;box-shadow:0 2px 8px #00000026;padding:8px;z-index:50;min-width:120px;text-align:center;position:relative}.empty-space-tooltip:after{content:"";position:absolute;bottom:-8px;left:50%;transform:translate(-50%);border-width:8px 8px 0;border-style:solid;border-color:white transparent transparent}.tooltip-action-btn.new-segment{width:auto;padding:6px 10px;display:flex;align-items:center;gap:5px}.tooltip-btn-text{font-size:.8rem;white-space:nowrap;color:#000}.icon-new-segment{width:20px;height:20px}.zoom-dropdown-container{position:relative}.zoom-button{display:flex;align-items:center;gap:6px;background-color:#6c757dcc;color:#fff;border:none;border-radius:4px;padding:8px 12px;font-weight:500;cursor:pointer;transition:background-color .2s}.zoom-button:hover{background-color:#6c757d}.zoom-dropdown{background-color:#fff;border-radius:4px;box-shadow:0 2px 10px #00000026;max-height:300px;overflow-y:auto}.zoom-option{padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:5px}.zoom-option:hover{background-color:#007bff1a}.zoom-option.selected{background-color:#007bff33;font-weight:500}.save-button,.save-copy-button,.save-segments-button{background-color:#007bffcc;color:#fff;border:none;border-radius:4px;padding:8px 12px;font-weight:500;cursor:pointer;transition:background-color .2s}.save-button:hover,.save-copy-button:hover{background-color:#007bff}.save-copy-button{background-color:#6c757dcc}.save-copy-button:hover{background-color:#6c757d}.time-nav-label{font-weight:500;font-size:.9rem}.time-input{padding:6px 10px;border-radius:4px;border:1px solid #ccc;width:150px;font-family:monospace}.time-button-group{display:flex;gap:5px}.time-button{background-color:#6c757dcc;color:#fff;border:none;border-radius:4px;padding:6px 8px;font-size:.8rem;cursor:pointer;transition:background-color .2s}.time-button:hover{background-color:#6c757d}.timeline-controls{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;padding:12px;background-color:#f5f5f5;border-radius:6px;margin-top:15px}.time-navigation{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.controls-right{display:flex;align-items:center;gap:10px}@media (max-width: 768px){.timeline-controls{flex-direction:column;align-items:flex-start;gap:15px}.controls-right{margin-top:10px;width:100%;justify-content:flex-start;text-align:center;align-items:center;justify-content:center}}.timeline-header{display:flex;align-items:center;gap:20px;margin-bottom:10px;flex-wrap:wrap}.timeline-title{font-weight:700;margin-right:20px}.timeline-title-text{font-size:1.1rem}.current-time,.duration-time{white-space:nowrap}.time-code{font-family:monospace;font-weight:500}@media (max-width: 480px){.timeline-header{flex-direction:column;align-items:flex-start;gap:8px}.time-navigation{width:100%;flex-direction:column;align-items:flex-start;gap:10px}.time-button-group{width:100%;display:flex;justify-content:space-between;margin-top:10px}.controls-right{flex-wrap:wrap;gap:8px}.save-button,.save-copy-button{margin-top:8px;width:100%}.zoom-dropdown-container{width:100%}.zoom-button{width:100%;justify-content:center}} +#video-editor-trim-root{@keyframes pulse{0%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.05)}to{opacity:.7;transform:scale(1)}}}#video-editor-trim-root .video-player-container{position:relative;width:100%;background:#000;border-radius:.5rem;overflow:hidden;margin-bottom:1rem;aspect-ratio:16/9;-webkit-user-select:none;-moz-user-select:none;user-select:none}#video-editor-trim-root .video-player-container video{width:100%;height:100%;cursor:pointer;transform:translateZ(0);-webkit-transform:translateZ(0);-webkit-user-select:none;-moz-user-select:none;user-select:none}@supports (-webkit-touch-callout: none){#video-editor-trim-root .video-player-container video{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}}#video-editor-trim-root .play-pause-indicator{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:60px;height:60px;background-color:#0009;border-radius:50%;opacity:0;transition:opacity .3s;pointer-events:none}#video-editor-trim-root .video-player-container:hover .play-pause-indicator{opacity:1}#video-editor-trim-root .play-pause-indicator:before{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#video-editor-trim-root .play-pause-indicator.play-icon:before{width:0;height:0;border-top:15px solid transparent;border-bottom:15px solid transparent;border-left:25px solid white;margin-left:3px}#video-editor-trim-root .play-pause-indicator.pause-icon:before{width:20px;height:25px;border-left:6px solid white;border-right:6px solid white}#video-editor-trim-root .ios-first-play-indicator{position:absolute;top:0;left:0;width:100%;height:100%;background:#000000b3;display:flex;align-items:center;justify-content:center;z-index:10}#video-editor-trim-root .ios-play-message{color:#fff;font-size:1.2rem;text-align:center;padding:1rem;background:#000c;border-radius:.5rem;animation:pulse 2s infinite}#video-editor-trim-root .video-controls{position:absolute;bottom:0;left:0;right:0;padding:.75rem;background:linear-gradient(transparent,#000000b3);opacity:0;transition:opacity .3s}#video-editor-trim-root .video-player-container:hover .video-controls{opacity:1}#video-editor-trim-root .video-current-time,#video-editor-trim-root .video-duration{color:#fff;font-size:.875rem}#video-editor-trim-root .video-time-display{display:flex;justify-content:space-between;margin-bottom:.5rem;color:#fff;font-size:.875rem}#video-editor-trim-root .video-progress{position:relative;height:6px;background-color:#ffffff4d;border-radius:3px;cursor:pointer;margin:0 10px;touch-action:none;flex-grow:1}#video-editor-trim-root .video-progress.dragging{height:8px}#video-editor-trim-root .video-progress-fill{position:absolute;top:0;left:0;height:100%;background-color:red;border-radius:3px;pointer-events:none}#video-editor-trim-root .video-scrubber{position:absolute;top:50%;transform:translate(-50%,-50%);width:16px;height:16px;background-color:red;border-radius:50%;cursor:grab;transition:transform .1s ease,width .1s ease,height .1s ease}#video-editor-trim-root .video-progress.dragging .video-scrubber{transform:translate(-50%,-50%) scale(1.2);width:18px;height:18px;cursor:grabbing;box-shadow:0 0 8px #f009}@media (pointer: coarse){#video-editor-trim-root .video-scrubber{width:20px;height:20px}#video-editor-trim-root .video-progress.dragging .video-scrubber{width:24px;height:24px}#video-editor-trim-root .video-scrubber:before{content:"";position:absolute;top:-10px;left:-10px;right:-10px;bottom:-10px}}#video-editor-trim-root .video-controls-buttons{display:flex;align-items:center;justify-content:flex-end;gap:.75rem}#video-editor-trim-root .mute-button,#video-editor-trim-root .fullscreen-button{min-width:auto;color:#fff;background:none;border:none;cursor:pointer;padding:.25rem;transition:transform .2s}#video-editor-trim-root .mute-button:hover,#video-editor-trim-root .fullscreen-button:hover{transform:scale(1.1)}#video-editor-trim-root .mute-button svg,#video-editor-trim-root .fullscreen-button svg{width:1.25rem;height:1.25rem}#video-editor-trim-root .video-time-tooltip{position:absolute;top:-30px;background-color:#000000b3;color:#fff;padding:4px 8px;border-radius:4px;font-size:12px;font-family:monospace;pointer-events:none;z-index:1000;white-space:nowrap;box-shadow:0 2px 4px #0000004d}#video-editor-trim-root .video-time-tooltip:after{content:"";position:absolute;bottom:-4px;left:50%;transform:translate(-50%);width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid rgba(0,0,0,.7)}#video-editor-trim-root{@keyframes modal-fade-in{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes success-pop{0%{transform:scale(0);opacity:0}70%{transform:scale(1.1);opacity:1}to{transform:scale(1);opacity:1}}@keyframes error-pop{0%{transform:scale(0);opacity:0}70%{transform:scale(1.1);opacity:1}to{transform:scale(1);opacity:1}}}#video-editor-trim-root .modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}#video-editor-trim-root .modal-container{background-color:#fff;border-radius:8px;box-shadow:0 4px 12px #00000026;width:90%;max-width:500px;max-height:90vh;overflow-y:auto;animation:modal-fade-in .3s ease-out}#video-editor-trim-root .modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #eee}#video-editor-trim-root .modal-title{margin:0;font-size:1.25rem;font-weight:600;color:#333}#video-editor-trim-root .modal-close-button{background:none;border:none;cursor:pointer;color:#666;padding:4px;display:flex;align-items:center;justify-content:center;transition:color .2s}#video-editor-trim-root .modal-close-button:hover{color:#000}#video-editor-trim-root .modal-content{padding:20px;color:#333;font-size:1rem;line-height:1.5;max-height:400px;overflow-y:auto}#video-editor-trim-root .modal-actions{display:flex;justify-content:flex-end;padding:16px 20px;border-top:1px solid #eee;gap:12px}#video-editor-trim-root .modal-button{padding:8px 16px;border-radius:4px;font-weight:500;cursor:pointer;transition:all .2s;border:none}#video-editor-trim-root .modal-button-primary{background-color:#06c;color:#fff}#video-editor-trim-root .modal-button-primary:hover{background-color:#05a}#video-editor-trim-root .modal-button-secondary{background-color:#f0f0f0;color:#333}#video-editor-trim-root .modal-button-secondary:hover{background-color:#e0e0e0}#video-editor-trim-root .modal-button-danger{background-color:#dc3545;color:#fff}#video-editor-trim-root .modal-button-danger:hover{background-color:#bd2130}#video-editor-trim-root .modal-message{margin-bottom:16px;font-size:1rem}#video-editor-trim-root .modal-spinner{display:flex;align-items:center;justify-content:center;margin:20px 0}#video-editor-trim-root .spinner{border:4px solid rgba(0,0,0,.1);border-radius:50%;border-top:4px solid #0066cc;width:30px;height:30px;animation:spin 1s linear infinite}#video-editor-trim-root .modal-success-icon{display:flex;justify-content:center;margin-bottom:16px;color:#28a745;font-size:2rem}#video-editor-trim-root .modal-success-icon svg{width:60px;height:60px;color:#4caf50;animation:success-pop .5s ease-out}#video-editor-trim-root .modal-error-icon{display:flex;justify-content:center;margin-bottom:16px;color:#dc3545;font-size:2rem}#video-editor-trim-root .modal-error-icon svg{width:60px;height:60px;color:#f44336;animation:error-pop .5s ease-out}#video-editor-trim-root .modal-choices{display:flex;flex-direction:column;gap:10px;margin-top:20px}#video-editor-trim-root .modal-choice-button{padding:12px 16px;border:none;border-radius:4px;background-color:#06c;text-align:center;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;font-weight:500;text-decoration:none;color:#fff}#video-editor-trim-root .modal-choice-button:hover{background-color:#05a;transform:translateY(-1px);box-shadow:0 2px 5px #0000001a}#video-editor-trim-root .modal-choice-button svg{margin-right:8px}#video-editor-trim-root .success-link{background-color:#4caf50}#video-editor-trim-root .success-link:hover{background-color:#3d8b40}#video-editor-trim-root .centered-choice{margin:0 auto;width:auto;min-width:220px;background-color:#06c;color:#fff}#video-editor-trim-root .centered-choice:hover{background-color:#05a}@media (max-width: 480px){#video-editor-trim-root .modal-container{width:95%}#video-editor-trim-root .modal-actions{flex-direction:column}#video-editor-trim-root .modal-button{width:100%}}#video-editor-trim-root .error-message{color:#f44336;font-weight:500;background-color:#f443361a;padding:10px;border-radius:4px;border-left:4px solid #f44336;margin-top:10px}#video-editor-trim-root .redirect-message{color:#555;font-size:.95rem;padding:0;margin:0}#video-editor-trim-root .countdown{font-weight:700;color:#06c;font-size:1.1rem}#video-editor-trim-root{@keyframes spin{to{transform:rotate(360deg)}}@keyframes fadeIn{0%{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}}#video-editor-trim-root .timeline-container-card{background-color:#fff;border-radius:.5rem;padding:1rem;box-shadow:0 1px 2px #0000000d}#video-editor-trim-root .timeline-header{margin-bottom:.75rem;display:flex;justify-content:space-between;align-items:center}#video-editor-trim-root .timeline-title{font-size:.875rem;font-weight:500;color:var(--foreground, #333)}#video-editor-trim-root .timeline-title-text{font-weight:700}#video-editor-trim-root .current-time{font-size:.875rem;color:var(--foreground, #333)}#video-editor-trim-root .time-code{font-family:monospace;background-color:#f3f4f6;padding:0 .5rem;border-radius:.25rem}#video-editor-trim-root .duration-time{font-size:.875rem;color:var(--foreground, #333)}#video-editor-trim-root .timeline-scroll-container{position:relative;overflow:visible!important}#video-editor-trim-root .timeline-container{position:relative;min-width:100%;background-color:#fafbfc;height:70px;border-radius:.25rem;overflow:visible!important}#video-editor-trim-root .timeline-marker{position:absolute;height:82px;width:2px;background-color:#000;transform:translate(-50%);z-index:50;pointer-events:none}#video-editor-trim-root .timeline-marker-head{position:absolute;top:-6px;left:50%;transform:translate(-50%);width:16px;height:16px;background-color:#ef4444;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;pointer-events:auto;z-index:51}#video-editor-trim-root .timeline-marker-drag{position:absolute;bottom:-12px;left:50%;transform:translate(-50%);width:16px;height:16px;background-color:#4b5563;border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;pointer-events:auto;z-index:51}#video-editor-trim-root .timeline-marker-drag.dragging{cursor:grabbing;background-color:#374151}#video-editor-trim-root .timeline-marker-head-icon{color:#fff;font-size:14px;font-weight:700;line-height:1;-webkit-user-select:none;-moz-user-select:none;user-select:none}#video-editor-trim-root .timeline-marker-drag-icon{color:#fff;font-size:12px;line-height:1;-webkit-user-select:none;-moz-user-select:none;user-select:none;transform:rotate(90deg);display:inline-block}#video-editor-trim-root .trim-line-marker{position:absolute;top:0;bottom:0;width:1px;background-color:#00000080;z-index:20}#video-editor-trim-root .trim-handle{position:absolute;width:10px;height:20px;background-color:#000;cursor:ew-resize}#video-editor-trim-root .trim-handle.left{right:0;top:10px;border-radius:3px 0 0 3px}#video-editor-trim-root .trim-handle.right{left:0;top:10px;border-radius:0 3px 3px 0}#video-editor-trim-root .timeline-thumbnail{display:inline-block;height:70px;border-right:1px solid rgba(0,0,0,.03)}#video-editor-trim-root .split-point{position:absolute;top:0;bottom:0;width:1px;background-color:#ff000080;z-index:15}#video-editor-trim-root .clip-segment{position:absolute;height:70px;border-radius:4px;z-index:10;border:2px solid rgba(0,0,0,.15);cursor:pointer}#video-editor-trim-root .clip-segment:hover{box-shadow:0 0 0 2px #0000004d;border-color:#0006;background-color:#f0f0f0cc!important}#video-editor-trim-root .clip-segment.selected{box-shadow:0 0 0 2px #3b82f6b3;border-color:#3b82f6e6}#video-editor-trim-root .clip-segment.selected:hover{background-color:#f0f8ffd9!important}#video-editor-trim-root .clip-segment-info{position:absolute;bottom:0;left:0;right:0;padding:.4rem;background-color:#0006;color:#fff;opacity:1;transition:background-color .2s;line-height:1.3}#video-editor-trim-root .clip-segment:hover .clip-segment-info{background-color:#00000080}#video-editor-trim-root .clip-segment.selected .clip-segment-info{background-color:#3b82f680}#video-editor-trim-root .clip-segment.selected:hover .clip-segment-info{background-color:#3b82f666}#video-editor-trim-root .clip-segment-name{font-weight:700;font-size:12px}#video-editor-trim-root .clip-segment-time,#video-editor-trim-root .clip-segment-duration{font-size:10px}#video-editor-trim-root .clip-segment-handle{position:absolute;top:0;bottom:0;width:6px;background-color:#0003;cursor:ew-resize}#video-editor-trim-root .clip-segment-handle:hover{background-color:#0006}#video-editor-trim-root .clip-segment-handle.left{left:0;border-radius:2px 0 0 2px}#video-editor-trim-root .clip-segment-handle.right{right:0;border-radius:0 2px 2px 0}@media (pointer: coarse){#video-editor-trim-root .clip-segment-handle{width:14px;background-color:#0006}#video-editor-trim-root .clip-segment-handle:after{content:"";position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:2px;height:20px;background-color:#fffc;border-radius:1px}#video-editor-trim-root .clip-segment-handle.left:after{box-shadow:-2px 0 #00000080}#video-editor-trim-root .clip-segment-handle.right:after{box-shadow:2px 0 #00000080}#video-editor-trim-root .clip-segment-handle:active{background-color:#0009}#video-editor-trim-root .timeline-marker{height:85px}#video-editor-trim-root .timeline-marker-head{width:24px;height:24px;top:-13px}#video-editor-trim-root .timeline-marker-drag{width:24px;height:24px;bottom:-18px}#video-editor-trim-root .timeline-marker-head.dragging{width:28px;height:28px;top:-15px}}#video-editor-trim-root .segment-tooltip,#video-editor-trim-root .empty-space-tooltip{position:absolute;background-color:#fff;border-radius:4px;box-shadow:0 2px 8px #0000004d;padding:.5rem;z-index:1000;min-width:150px;text-align:center;pointer-events:auto;top:-100px!important;transform:translateY(-10px)}#video-editor-trim-root .segment-tooltip:after,#video-editor-trim-root .empty-space-tooltip:after{content:"";position:absolute;bottom:-5px;left:50%;transform:translate(-50%);width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid white}#video-editor-trim-root .segment-tooltip:before,#video-editor-trim-root .empty-space-tooltip:before{content:"";position:absolute;bottom:-6px;left:50%;transform:translate(-50%);width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid rgba(0,0,0,.1);z-index:-1}#video-editor-trim-root .tooltip-time{font-weight:600;font-size:.875rem;margin-bottom:.5rem;color:#333}#video-editor-trim-root .tooltip-actions{display:flex;justify-content:center;gap:.5rem}#video-editor-trim-root .tooltip-action-btn{background-color:#f3f4f6;border:none;border-radius:.25rem;padding:.375rem;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#4b5563;min-width:20px!important}#video-editor-trim-root .tooltip-action-btn:hover{background-color:#e5e7eb;color:#111827}#video-editor-trim-root .tooltip-action-btn.delete{color:#ef4444}#video-editor-trim-root .tooltip-action-btn.delete:hover{background-color:#fee2e2}#video-editor-trim-root .tooltip-action-btn.new-segment{padding:.375rem .5rem}#video-editor-trim-root .tooltip-action-btn.new-segment .tooltip-btn-text{margin-left:.25rem;font-size:.75rem}#video-editor-trim-root .tooltip-action-btn svg{width:1rem;height:1rem}#video-editor-trim-root .timeline-controls{display:flex;align-items:center;justify-content:space-between;margin-top:.75rem}#video-editor-trim-root .time-navigation{display:none;align-items:center;gap:.5rem}#video-editor-trim-root .time-nav-label{font-size:.875rem;font-weight:500}#video-editor-trim-root .time-input{border:1px solid #d1d5db;border-radius:.25rem;padding:.25rem .5rem;width:8rem;font-size:.875rem}#video-editor-trim-root .time-button-group{display:flex}#video-editor-trim-root .time-button{background-color:#e5e7eb;color:#000;padding:.25rem .5rem;font-size:.875rem;border:none;cursor:pointer;margin-right:.5rem}#video-editor-trim-root .time-button:hover{background-color:#d1d5db}#video-editor-trim-root .time-button:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}#video-editor-trim-root .time-button:last-child{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}#video-editor-trim-root .controls-right{display:flex;align-items:center;gap:.5rem;margin-left:auto}#video-editor-trim-root .zoom-dropdown-container{position:relative;z-index:100;display:none}#video-editor-trim-root .zoom-button{background-color:#374151;color:#fff;border:none;border-radius:.25rem;padding:.25rem .75rem;font-size:.875rem;display:flex;align-items:center;cursor:pointer}#video-editor-trim-root .zoom-button:hover{background-color:#1f2937}#video-editor-trim-root .zoom-button svg{margin-left:.25rem}#video-editor-trim-root .zoom-dropdown{position:absolute;top:100%;left:0;margin-top:.25rem;width:9rem;background-color:#374151;color:#fff;border-radius:.25rem;box-shadow:0 4px 6px -1px #0000001a;z-index:50;max-height:300px;overflow-y:auto}#video-editor-trim-root .zoom-option{padding:.25rem .75rem;cursor:pointer}#video-editor-trim-root .zoom-option:hover{background-color:#4b5563}#video-editor-trim-root .zoom-option.selected{background-color:#6b7280;display:flex;align-items:center}#video-editor-trim-root .zoom-option svg{margin-right:.25rem}#video-editor-trim-root .save-buttons-row{display:flex;align-items:center;gap:.5rem;margin:0;flex-wrap:nowrap}#video-editor-trim-root .save-button,#video-editor-trim-root .save-copy-button,#video-editor-trim-root .save-segments-button{color:#fff;background:#06c;border-radius:.25rem;font-size:.75rem;padding:.25rem .5rem;cursor:pointer;border:none;white-space:nowrap;transition:background-color .2s;min-width:-moz-fit-content;min-width:fit-content}#video-editor-trim-root .save-button:hover,#video-editor-trim-root .save-copy-button:hover,#video-editor-trim-root .save-segments-button:hover{background-color:#0056b3}@media (max-width: 576px){#video-editor-trim-root .save-buttons-row{width:100%;justify-content:space-between;gap:.5rem}#video-editor-trim-root .save-button,#video-editor-trim-root .save-copy-button,#video-editor-trim-root .save-segments-button{flex:1;font-size:.7rem;padding:.25rem .35rem}}@media (max-width: 480px){#video-editor-trim-root .save-button,#video-editor-trim-root .save-copy-button,#video-editor-trim-root .save-segments-button{font-size:.675rem;padding:.25rem}#video-editor-trim-root .controls-right,#video-editor-trim-root .controls-right button{margin:0}}#video-editor-trim-root .modal-success-content,#video-editor-trim-root .modal-error-content{display:flex;flex-direction:column;align-items:center;padding:1rem;text-align:center;padding:0;margin:0}#video-editor-trim-root .modal-success-icon,#video-editor-trim-root .modal-error-icon{margin-bottom:1rem}#video-editor-trim-root .modal-success-icon svg{color:#4caf50;animation:fadeIn .5s ease-in-out}#video-editor-trim-root .modal-error-icon svg{color:#f44336;animation:fadeIn .5s ease-in-out}#video-editor-trim-root .success-link{background-color:#4caf50;color:#fff;transition:background-color .3s}#video-editor-trim-root .success-link:hover{background-color:#388e3c}#video-editor-trim-root .error-message{color:#f44336;font-weight:500}#video-editor-trim-root .modal-spinner{display:flex;justify-content:center;margin:2rem 0}#video-editor-trim-root .spinner{width:50px;height:50px;border:5px solid rgba(0,0,0,.1);border-radius:50%;border-top-color:#06c;animation:spin 1s ease-in-out infinite}#video-editor-trim-root .text-center{text-align:center}#video-editor-trim-root .modal-message{margin-bottom:1rem;line-height:1.5}#video-editor-trim-root .modal-choice-button{display:flex;align-items:center;justify-content:center;padding:.75rem 1.25rem;background-color:#06c;color:#fff;border-radius:4px;text-decoration:none;margin:0 auto;cursor:pointer;font-weight:500;gap:.5rem;border:none;transition:background-color .3s}#video-editor-trim-root .modal-choice-button:hover{background-color:#0056b3}#video-editor-trim-root .modal-choice-button svg{flex-shrink:0}#video-editor-trim-root .centered-choice{margin:0 auto;min-width:180px}.mobile-timeline-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#00000080;z-index:50;display:flex;justify-content:center;align-items:center;border-radius:.5rem;pointer-events:none}.mobile-timeline-message{background-color:#000c;border-radius:8px;padding:15px 25px;text-align:center;max-width:80%;animation:pulse 2s infinite}.mobile-timeline-message p{color:#fff;font-size:16px;margin:0 0 15px;font-weight:500}.mobile-play-icon{width:0;height:0;border-top:15px solid transparent;border-bottom:15px solid transparent;border-left:25px solid white;margin:0 auto}@keyframes pulse{0%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.05)}to{opacity:.7;transform:scale(1)}}.segments-playback-mode .tooltip-time-btn,.segments-playback-mode .tooltip-action-btn.play,.segments-playback-mode .tooltip-action-btn.pause{opacity:1;cursor:pointer}.segments-playback-mode .tooltip-time-btn[disabled],.segments-playback-mode .tooltip-action-btn[disabled]{opacity:.5!important;cursor:not-allowed!important}.segments-playback-mode [data-tooltip][disabled]:hover:before,.segments-playback-mode [data-tooltip][disabled]:hover:after{opacity:1!important;visibility:visible!important}.segments-playback-message{display:flex;align-items:center;background-color:#3b82f61a;color:#3b82f6;padding:6px 12px;border-radius:4px;font-weight:600;font-size:.875rem;animation:pulse 2s infinite}.segments-playback-message svg{height:1.25rem;width:1.25rem;margin-right:.5rem;color:#3b82f6}.two-row-tooltip{display:flex;flex-direction:column;background-color:#fff;padding:6px;border-radius:4px;box-shadow:0 2px 8px #00000026;position:relative;z-index:3000}.tooltip-time-btn[data-tooltip="Decrease by 100ms"],.tooltip-time-btn[data-tooltip="Increase by 100ms"]{display:none!important}.tooltip-row{display:flex;justify-content:space-between;align-items:center;gap:3px}.tooltip-row:first-child{margin-bottom:6px}.tooltip-time-btn{background-color:#f0f0f0!important;border:none!important;border-radius:4px!important;padding:4px 8px!important;font-size:.75rem!important;font-weight:500!important;color:#333!important;cursor:pointer!important;transition:background-color .2s!important;min-width:20px!important}.tooltip-time-btn:hover{background-color:#e0e0e0!important}.tooltip-time-display{font-family:monospace!important;font-size:.875rem!important;font-weight:600!important;color:#333!important;padding:4px 6px!important;background-color:#f7f7f7!important;border-radius:4px!important;min-width:100px!important;text-align:center!important;overflow:hidden!important}.tooltip-time-display.disabled{pointer-events:none!important;cursor:not-allowed!important;opacity:.6!important;user-select:none!important;-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important}.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;align-items:center;gap:3px;position:relative;z-index:2500}.tooltip-action-btn{background-color:#f3f4f6;border:none;border-radius:4px;padding:5px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#4b5563;width:26px;height:26px;min-width:20px!important;position:relative}.tooltip-action-btn[data-tooltip]:before{content:attr(data-tooltip);position:absolute;height:30px;top:35px;left:50%;transform:translate(-50%);margin-left:0;background-color:#000000d9;color:#fff;text-align:left;padding:6px 12px;border-radius:4px;box-shadow:0 2px 8px #0003;font-size:12px;white-space:nowrap;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;z-index:2500;pointer-events:none}.tooltip-action-btn[data-tooltip]:after{content:"";position:absolute;top:35px;left:50%;transform:translate(-50%);border-width:4px;border-style:solid;border-color:rgba(0,0,0,.85) transparent transparent transparent;margin-left:0;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;z-index:2500;pointer-events:none}@media (hover: hover) and (pointer: fine){.tooltip-action-btn[data-tooltip]:hover:before,.tooltip-action-btn[data-tooltip]:hover:after{opacity:1;visibility:visible}}@media (pointer: coarse){.tooltip-action-btn[data-tooltip]:before,.tooltip-action-btn[data-tooltip]:after{display:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important;content:none!important}}.tooltip-action-btn:hover{background-color:#e5e7eb;color:#111827}.tooltip-action-btn.delete{color:#ef4444}.tooltip-action-btn.delete:hover{background-color:#fee2e2}.tooltip-action-btn.play{color:#10b981}.tooltip-action-btn.play:hover{background-color:#d1fae5}.tooltip-action-btn.pause{color:#3b82f6}.tooltip-action-btn.pause:hover{background-color:#dbeafe}.tooltip-action-btn.play-from-start{color:#4f46e5}.tooltip-action-btn.play-from-start:hover{background-color:#e0e7ff}.tooltip-action-btn svg{width:16px;height:16px}.tooltip-action-btn.new-segment{width:auto;height:auto;padding:6px 10px;display:flex;flex-direction:row;color:#10b981}.tooltip-action-btn.new-segment:hover{background-color:#d1fae5}.tooltip-action-btn.new-segment .tooltip-btn-text{margin-left:6px;font-size:.75rem;white-space:nowrap}.tooltip-action-btn.disabled{opacity:.5;cursor:not-allowed;background-color:#f3f4f6}.tooltip-action-btn.disabled:hover{background-color:#f3f4f6;color:#9ca3af}.tooltip-action-btn.disabled svg{color:#9ca3af}.tooltip-action-btn.disabled .tooltip-btn-text{color:#9ca3af}.tooltip-action-btn.pause.disabled{color:#9ca3af!important;opacity:.5;cursor:not-allowed}.tooltip-action-btn.pause.disabled:hover{background-color:#f3f4f6!important;color:#9ca3af!important}.tooltip-action-btn.play.disabled{color:#9ca3af!important;opacity:.5;cursor:not-allowed}.tooltip-action-btn.play.disabled:hover{background-color:#f3f4f6!important;color:#9ca3af!important}.tooltip-time-btn.disabled{opacity:.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}@media (max-width: 768px){.two-row-tooltip{padding:4px}.tooltip-row:first-child{margin-bottom:4px}.tooltip-time-btn{min-width:20px!important;font-size:.7rem!important;padding:3px 6px!important}.tooltip-time-display{font-size:.8rem!important;padding:3px 4px!important;min-width:90px!important}.tooltip-action-btn{width:24px;height:24px;padding:4px}.tooltip-action-btn.new-segment{padding:4px 8px}.tooltip-action-btn svg{width:14px;height:14px}.tooltip-action-btn[data-tooltip]:before{min-width:100px;font-size:11px;padding:4px 8px;height:24px;top:33px}.tooltip-action-btn[data-tooltip]:after{top:33px}}#video-editor-trim-root{@keyframes bluePulse{0%{box-shadow:0 0 #3b82f666}50%{box-shadow:0 0 0 8px #3b82f600}to{box-shadow:0 0 #3b82f600}}@keyframes pulse{0%{opacity:.8}50%{opacity:1}to{opacity:.8}}}#video-editor-trim-root .editing-tools-container{background-color:#fff;border-radius:.5rem;padding:1rem;margin-bottom:2.5rem;box-shadow:0 1px 2px #0000000d}#video-editor-trim-root .flex-container{display:flex;justify-content:space-between;align-items:center;position:relative;gap:15px;width:100%}#video-editor-trim-root .flex-container.single-row{flex-wrap:nowrap}#video-editor-trim-root .full-text{display:inline}#video-editor-trim-root .short-text{display:none}#video-editor-trim-root .reset-text{display:inline}#video-editor-trim-root .button-group{display:flex;align-items:center}#video-editor-trim-root .button-group.play-buttons-group{gap:.75rem;justify-content:flex-start;flex:0 0 auto}#video-editor-trim-root .button-group.secondary{gap:.75rem;align-items:center;justify-content:flex-end;margin-left:auto}#video-editor-trim-root .button-group button{display:flex;align-items:center;color:#333;background:none;border:none;cursor:pointer;min-width:auto}#video-editor-trim-root .button-group button:hover:not(:disabled){color:inherit}#video-editor-trim-root .button-group button:disabled{opacity:.5;cursor:not-allowed}#video-editor-trim-root .button-group button svg{height:1.25rem;width:1.25rem;margin-right:.25rem}#video-editor-trim-root .divider{border-right:1px solid #d1d5db;height:1.5rem;margin:0 .5rem}#video-editor-trim-root .play-button,#video-editor-trim-root .preview-button{font-weight:600;display:flex;align-items:center;position:relative;overflow:hidden;min-width:80px;justify-content:center;font-size:.875rem!important}#video-editor-trim-root .play-button.greyed-out{opacity:.5;cursor:not-allowed}#video-editor-trim-root .segments-button.highlighted-stop{background-color:#3b82f61a;color:#3b82f6;border:1px solid #3b82f6;animation:bluePulse 2s infinite}#video-editor-trim-root .play-button:hover:not(:disabled),#video-editor-trim-root .preview-button:hover:not(:disabled){color:inherit!important;transform:none!important;font-size:.875rem!important;width:auto!important;background:none!important}#video-editor-trim-root .play-button svg,#video-editor-trim-root .preview-button svg{height:1.5rem;width:1.5rem;flex-shrink:0}#video-editor-trim-root .button-text{margin-left:.25rem}@media (max-width: 992px){#video-editor-trim-root .button-group.secondary .button-text{display:none}}@media (max-width: 768px){#video-editor-trim-root .flex-container.single-row{justify-content:space-between}#video-editor-trim-root .button-group{gap:.5rem}#video-editor-trim-root .preview-button,#video-editor-trim-root .play-button{font-size:.875rem!important}}@media (max-width: 640px){#video-editor-trim-root .editing-tools-container{padding:.75rem;overflow-x:hidden}#video-editor-trim-root .preview-button{min-width:auto}#video-editor-trim-root .full-text{display:none}#video-editor-trim-root .short-text{display:inline;margin-left:.15rem}#video-editor-trim-root .reset-text{display:none}#video-editor-trim-root .button-group.play-buttons-group{flex:initial;justify-content:flex-start;flex-shrink:0}#video-editor-trim-root .button-group.secondary{flex:initial;justify-content:flex-end;flex-shrink:0}#video-editor-trim-root .button-group button{padding:.375rem;min-width:auto}#video-editor-trim-root .button-group button svg{height:1.125rem;width:1.125rem;margin-right:.125rem}}@media (max-width: 576px){#video-editor-trim-root .flex-container.single-row{justify-content:space-between;flex-wrap:nowrap;gap:10px}#video-editor-trim-root .button-group.play-buttons-group{justify-content:flex-start;flex:0 0 auto}#video-editor-trim-root .button-group.secondary{justify-content:flex-end;margin-left:auto}#video-editor-trim-root .button-group button{padding:.25rem}#video-editor-trim-root .divider{margin:0 .25rem}}@media (max-width: 480px){#video-editor-trim-root .editing-tools-container{padding:.5rem}#video-editor-trim-root .flex-container.single-row{gap:8px}#video-editor-trim-root .button-group.play-buttons-group,#video-editor-trim-root .button-group.secondary{gap:.25rem}#video-editor-trim-root .divider{display:none}#video-editor-trim-root .button-group button{padding:.125rem}#video-editor-trim-root .button-group button svg{height:1rem;width:1rem;margin-right:0}#video-editor-trim-root .button-text,#video-editor-trim-root .reset-text{display:none}}@media (max-width: 640px) and (orientation: portrait){#video-editor-trim-root .editing-tools-container{width:100%;box-sizing:border-box}#video-editor-trim-root .flex-container.single-row{width:100%;padding:0;margin:0}#video-editor-trim-root .button-group{max-width:50%}#video-editor-trim-root .button-group.play-buttons-group{max-width:60%}#video-editor-trim-root .button-group.secondary{max-width:40%}}@media (hover: hover) and (pointer: fine){#video-editor-trim-root [data-tooltip]{position:relative}#video-editor-trim-root [data-tooltip]:before{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translate(-50%);margin-bottom:5px;background-color:#000c;color:#fff;text-align:center;padding:5px 10px;border-radius:3px;font-size:12px;white-space:nowrap;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;z-index:1000;pointer-events:none}#video-editor-trim-root [data-tooltip]:after{content:"";position:absolute;bottom:100%;left:50%;transform:translate(-50%);border-width:5px;border-style:solid;border-color:rgba(0,0,0,.8) transparent transparent transparent;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;pointer-events:none}#video-editor-trim-root [data-tooltip]:hover:before,#video-editor-trim-root [data-tooltip]:hover:after{opacity:1;visibility:visible}}@media (pointer: coarse){#video-editor-trim-root [data-tooltip]:before,#video-editor-trim-root [data-tooltip]:after{display:none!important;content:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important}}#video-editor-trim-root .clip-segments-container{margin-top:1rem;background-color:#fff;border-radius:.5rem;padding:1rem;box-shadow:0 1px 2px #0000000d}#video-editor-trim-root .clip-segments-title{font-size:.875rem;font-weight:500;color:var(--foreground, #333);margin-bottom:.75rem}#video-editor-trim-root .segment-item{display:flex;align-items:center;justify-content:space-between;padding:.5rem;border:1px solid #e5e7eb;border-radius:.25rem;margin-bottom:.5rem;transition:box-shadow .2s ease}#video-editor-trim-root .segment-item:hover{box-shadow:0 4px 6px -1px #0000001a}#video-editor-trim-root .segment-content{display:flex;align-items:center}#video-editor-trim-root .segment-thumbnail{width:4rem;height:2.25rem;background-size:cover;background-position:center;border-radius:.25rem;margin-right:.75rem;box-shadow:0 0 0 1px #ffffff4d}#video-editor-trim-root .segment-info{display:flex;flex-direction:column}#video-editor-trim-root .segment-title{font-weight:500;font-size:.875rem;color:#000}#video-editor-trim-root .segment-time{font-size:.75rem;color:#000}#video-editor-trim-root .segment-duration{font-size:.75rem;margin-top:.25rem;display:inline-block;background-color:#f3f4f6;padding:0 .5rem;border-radius:.25rem;color:#000}#video-editor-trim-root .segment-actions{display:flex;align-items:center;gap:.5rem}#video-editor-trim-root .delete-button{padding:.375rem;color:#4b5563;background-color:#e5e7eb;border-radius:9999px;border:none;cursor:pointer;transition:background-color .2s,color .2s;min-width:auto}#video-editor-trim-root .delete-button:hover{color:#000;background-color:#d1d5db}#video-editor-trim-root .delete-button svg{height:1rem;width:1rem}#video-editor-trim-root .empty-message{padding:1rem;text-align:center;color:#333333b3}#video-editor-trim-root .segment-color-1{background-color:#3b82f626}#video-editor-trim-root .segment-color-2{background-color:#10b98126}#video-editor-trim-root .segment-color-3{background-color:#f59e0b26}#video-editor-trim-root .segment-color-4{background-color:#ef444426}#video-editor-trim-root .segment-color-5{background-color:#8b5cf626}#video-editor-trim-root .segment-color-6{background-color:#ec489926}#video-editor-trim-root .segment-color-7{background-color:#06b6d426}#video-editor-trim-root .segment-color-8{background-color:#facc1526}.mobile-play-prompt-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#000000b3;display:flex;justify-content:center;align-items:center;z-index:1000;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)}.mobile-play-prompt{background-color:#fff;width:90%;max-width:400px;border-radius:12px;padding:25px;box-shadow:0 4px 20px #00000040;text-align:center}.mobile-play-prompt h3{margin:0 0 15px;font-size:20px;color:#333;font-weight:600}.mobile-play-prompt p{margin:0 0 15px;font-size:16px;color:#444;line-height:1.5}.mobile-prompt-instructions{margin:20px 0;text-align:left;background-color:#f8f9fa;padding:15px;border-radius:8px}.mobile-prompt-instructions p{margin:0 0 8px;font-size:15px;font-weight:500}.mobile-prompt-instructions ol{margin:0;padding-left:22px}.mobile-prompt-instructions li{margin-bottom:8px;font-size:14px;color:#333}.mobile-play-button{background-color:#007bff;color:#fff;border:none;border-radius:8px;padding:12px 25px;font-size:16px;font-weight:500;cursor:pointer;transition:background-color .2s;margin-top:5px;min-height:44px;min-width:200px}.mobile-play-button:hover{background-color:#0069d9}.mobile-play-button:active{background-color:#0062cc;transform:scale(.98)}@supports (-webkit-touch-callout: none){.mobile-play-button{padding:14px 25px}}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}*{border-color:hsl(var(--border))}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.inline{display:inline}.flex{display:flex}.hidden{display:none}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-6xl{max-width:72rem}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.resize{resize:both}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded-md{border-radius:calc(var(--radius) - 2px)}.border{border-width:1px}.bg-background{background-color:hsl(var(--background))}.bg-indigo-600{--tw-bg-opacity: 1;background-color:rgb(79 70 229 / var(--tw-bg-opacity, 1))}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.text-center{text-align:center}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.paused{animation-play-state:paused}:root{--foreground: 20 14.3% 4.1%;--muted: 60 4.8% 95.9%;--muted-foreground: 25 5.3% 44.7%;--popover: 0 0% 100%;--popover-foreground: 20 14.3% 4.1%;--card: 0 0% 100%;--card-foreground: 20 14.3% 4.1%;--border: 20 5.9% 90%;--input: 20 5.9% 90%;--primary: 207 90% 54%;--primary-foreground: 211 100% 99%;--secondary: 30 84% 54%;--secondary-foreground: 60 9.1% 97.8%;--accent: 60 4.8% 95.9%;--accent-foreground: 24 9.8% 10%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 60 9.1% 97.8%;--ring: 20 14.3% 4.1%;--radius: .5rem}.video-player{position:relative;width:100%;background-color:#000;overflow:hidden;border-radius:.5rem}.video-controls{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(0,0,0,.8),transparent);padding:1rem;display:flex;flex-direction:column}.video-current-time{color:#fff;font-weight:500}.video-progress{position:relative;height:4px;background-color:#ffffff4d;border-radius:2px;margin-bottom:1rem}.video-progress-fill{position:absolute;left:0;top:0;height:100%;background-color:hsl(var(--primary));border-radius:2px}.video-scrubber{position:absolute;width:12px;height:12px;margin-left:-6px;background-color:#fff;border-radius:50%;top:-4px}.video-player-container{position:relative;overflow:hidden}.play-pause-indicator{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70px;height:70px;border-radius:50%;background-color:#00000080;z-index:20;opacity:0;transition:opacity .2s ease;pointer-events:none;background-position:center;background-repeat:no-repeat}.play-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='36' height='36' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E")}.pause-icon{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='36' height='36' fill='white'%3E%3Cpath d='M6 19h4V5H6v14zm8-14v14h4V5h-4z'/%3E%3C/svg%3E")}.video-player-container:hover .play-pause-indicator{opacity:1}.timeline-scroll-container{height:6rem;border-radius:.375rem;overflow-x:auto;overflow-y:hidden;margin-bottom:.75rem;background-color:#eee;position:relative}.timeline-container{position:relative;background-color:#eee;height:6rem;width:100%;cursor:pointer;transition:width .3s ease}.timeline-marker{position:absolute;top:-10px;height:calc(100% + 10px);width:2px;background-color:red;z-index:100;pointer-events:none;box-shadow:0 0 4px #ff000080}.trim-line-marker{position:absolute;top:0;bottom:0;width:2px;background-color:#007bffe6;z-index:10}.trim-handle{width:8px;background-color:#6c757de6;position:absolute;top:0;bottom:0;cursor:ew-resize;z-index:15}.trim-handle.left{left:-4px}.trim-handle.right{right:-4px}.timeline-thumbnail{height:100%;border-right:1px solid rgba(0,0,0,.1);position:relative;display:inline-block;background-size:cover;background-position:center}.split-point{position:absolute;width:2px;background-color:#6c757de6;top:0;bottom:0;z-index:5}.clip-segment{position:absolute;height:95%;top:0;border-radius:4px;background-size:cover;background-position:center;background-blend-mode:soft-light;box-shadow:0 2px 8px #0003;overflow:hidden;cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:box-shadow .2s,transform .1s;z-index:15}.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,.9)}.clip-segment:nth-child(2n),.segment-color-2,.segment-color-4,.segment-color-6,.segment-color-8{background-color:transparent;border:2px solid rgba(108,117,125,.9)}.clip-segment:hover{box-shadow:0 4px 12px #0000004d;transform:translateY(-1px);filter:brightness(1.1)}.clip-segment:active{cursor:grabbing;box-shadow:0 2px 6px #0000004d;transform:translateY(0)}.clip-segment.selected{border-width:3px;box-shadow:0 4px 12px #0006;z-index:25;filter:brightness(1.2)}.clip-segment-info{background-color:#e2e6eae6;color:#000;padding:6px 8px;font-size:.7rem;position:absolute;top:0;left:0;width:100%;border-radius:4px 4px 0 0;z-index:2;display:flex;flex-direction:column;gap:2px}.clip-segment-name{font-weight:700;color:#000}.clip-segment-time{font-size:.65rem;color:#000}.clip-segment-duration{font-size:.65rem;color:#000;background:#b3d9ff66;padding:1px 4px;border-radius:2px;display:inline-block;margin-top:2px}.clip-segment-handle{position:absolute;width:8px;top:0;bottom:0;background-color:#6c757de6;cursor:ew-resize;z-index:20;display:flex;align-items:center;justify-content:center}.clip-segment-handle:after{content:"↔";color:#fff;font-size:12px;text-shadow:0 0 2px rgba(0,0,0,.8)}.clip-segment-handle.left{left:0}.clip-segment-handle.right{right:0}.clip-segment-handle:hover{background-color:#007bffe6;width:10px}input[type=range]{-webkit-appearance:none;height:6px;background:#e0e0e0;border-radius:3px}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;height:16px;width:16px;border-radius:50%;background:#007bffe6;cursor:pointer}[data-tooltip]{position:relative;cursor:pointer}[data-tooltip]:before{content:attr(data-tooltip);position:absolute;bottom:100%;left:50%;transform:translate(-50%);margin-bottom:8px;background-color:#000c;color:#fff;padding:5px 10px;border-radius:4px;font-size:.8rem;white-space:nowrap;z-index:1000;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;pointer-events:none}[data-tooltip]:after{content:"";position:absolute;bottom:100%;left:50%;transform:translate(-50%);border-width:5px;border-style:solid;border-color:rgba(0,0,0,.8) transparent transparent transparent;margin-bottom:0;opacity:0;visibility:hidden;transition:opacity .2s,visibility .2s;pointer-events:none}@media (hover: hover) and (pointer: fine){[data-tooltip]:hover:before,[data-tooltip]:hover:after{opacity:1;visibility:visible}}@media (pointer: coarse){[data-tooltip]:before,[data-tooltip]:after{display:none!important;content:none!important;opacity:0!important;visibility:hidden!important;pointer-events:none!important}}button[disabled][data-tooltip]:before,button[disabled][data-tooltip]:after{opacity:.5}.tooltip-action-btn{position:relative}.tooltip-action-btn[data-tooltip]:before,.tooltip-action-btn[data-tooltip]:after{opacity:0;visibility:hidden;position:absolute;pointer-events:none;transition:all .3s ease}.tooltip-action-btn[data-tooltip]:before{content:attr(data-tooltip);background-color:#000c;color:#fff;font-size:12px;padding:4px 8px;border-radius:3px;white-space:nowrap;bottom:-35px;left:50%;transform:translate(-50%);z-index:9999}.tooltip-action-btn[data-tooltip]:after{content:"";border-width:5px;border-style:solid;border-color:transparent transparent rgba(0,0,0,.8) transparent;bottom:-15px;left:50%;transform:translate(-50%);z-index:9999}@media (hover: hover) and (pointer: fine){.tooltip-action-btn:hover[data-tooltip]:before,.tooltip-action-btn:hover[data-tooltip]:after{opacity:1;visibility:visible}}.segment-tooltip{background-color:#b3d9fff2;color:#000;border-radius:4px;padding:6px;min-width:140px;z-index:1000;box-shadow:0 3px 10px #0003}.segment-tooltip:after{content:"";position:absolute;bottom:-6px;left:50%;transform:translate(-50%);width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid rgba(179,217,255,.95)}.tooltip-time{font-size:.85rem;font-weight:700;text-align:center;margin-bottom:6px;color:#000}.tooltip-actions{display:flex;justify-content:space-between;gap:5px;position:relative}.tooltip-action-btn{background-color:#007bff33;border:none;border-radius:3px;width:30px;height:30px;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:6px;transition:background-color .2s;min-width:20px!important}.tooltip-action-btn:hover{background-color:#007bff66}.tooltip-action-btn svg{width:100%;height:100%;stroke:currentColor}.tooltip-action-btn.set-in svg,.tooltip-action-btn.set-out svg{width:100%;height:100%;margin:0 auto;fill:currentColor;stroke:none}.empty-space-tooltip{background-color:#fff;border-radius:6px;box-shadow:0 2px 8px #00000026;padding:8px;z-index:50;min-width:120px;text-align:center;position:relative}.empty-space-tooltip:after{content:"";position:absolute;bottom:-8px;left:50%;transform:translate(-50%);border-width:8px 8px 0;border-style:solid;border-color:white transparent transparent}.tooltip-action-btn.new-segment{width:auto;padding:6px 10px;display:flex;align-items:center;gap:5px}.tooltip-btn-text{font-size:.8rem;white-space:nowrap;color:#000}.icon-new-segment{width:20px;height:20px}.zoom-dropdown-container{position:relative}.zoom-button{display:flex;align-items:center;gap:6px;background-color:#6c757dcc;color:#fff;border:none;border-radius:4px;padding:8px 12px;font-weight:500;cursor:pointer;transition:background-color .2s}.zoom-button:hover{background-color:#6c757d}.zoom-dropdown{background-color:#fff;border-radius:4px;box-shadow:0 2px 10px #00000026;max-height:300px;overflow-y:auto}.zoom-option{padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:5px}.zoom-option:hover{background-color:#007bff1a}.zoom-option.selected{background-color:#007bff33;font-weight:500}.save-button,.save-copy-button,.save-segments-button{background-color:#007bffcc;color:#fff;border:none;border-radius:4px;padding:8px 12px;font-weight:500;cursor:pointer;transition:background-color .2s}.save-button:hover,.save-copy-button:hover{background-color:#007bff}.save-copy-button{background-color:#6c757dcc}.save-copy-button:hover{background-color:#6c757d}.time-nav-label{font-weight:500;font-size:.9rem}.time-input{padding:6px 10px;border-radius:4px;border:1px solid #ccc;width:150px;font-family:monospace}.time-button-group{display:flex;gap:5px}.time-button{background-color:#6c757dcc;color:#fff;border:none;border-radius:4px;padding:6px 8px;font-size:.8rem;cursor:pointer;transition:background-color .2s}.time-button:hover{background-color:#6c757d}.timeline-controls{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;padding:12px;background-color:#f5f5f5;border-radius:6px;margin-top:15px}.time-navigation{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.controls-right{display:flex;align-items:center;gap:10px}@media (max-width: 768px){.timeline-controls{flex-direction:column;align-items:flex-start;gap:15px}.controls-right{margin-top:10px;width:100%;justify-content:flex-start;text-align:center;align-items:center;justify-content:center}}.timeline-header{display:flex;align-items:center;gap:20px;margin-bottom:10px;flex-wrap:wrap}.timeline-title{font-weight:700;margin-right:20px}.timeline-title-text{font-size:1.1rem}.current-time,.duration-time{white-space:nowrap}.time-code{font-family:monospace;font-weight:500}@media (max-width: 480px){.timeline-header{flex-direction:column;align-items:flex-start;gap:8px}.time-navigation{width:100%;flex-direction:column;align-items:flex-start;gap:10px}.time-button-group{width:100%;display:flex;justify-content:space-between;margin-top:10px}.controls-right{flex-wrap:wrap;gap:8px}.save-button,.save-copy-button{margin-top:8px;width:100%}.zoom-dropdown-container{width:100%}.zoom-button{width:100%;justify-content:center}} diff --git a/static/video_editor/video-editor.js b/static/video_editor/video-editor.js index a5003ec9..40d8020f 100644 --- a/static/video_editor/video-editor.js +++ b/static/video_editor/video-editor.js @@ -1,4 +1,4 @@ -(function(){"use strict";var dm={exports:{}},Kc={exports:{}},rl={exports:{}};rl.exports;var vm;function HS(){return vm||(vm=1,function(C,k){/** +(function(){"use strict";var dm={exports:{}},Kc={exports:{}},al={exports:{}};al.exports;var vm;function z0(){return vm||(vm=1,function(w,L){/** * @license React * react.development.js * @@ -6,29 +6,29 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var me="18.3.1",Se=Symbol.for("react.element"),ve=Symbol.for("react.portal"),Ee=Symbol.for("react.fragment"),d=Symbol.for("react.strict_mode"),Z=Symbol.for("react.profiler"),ye=Symbol.for("react.provider"),oe=Symbol.for("react.context"),rt=Symbol.for("react.forward_ref"),G=Symbol.for("react.suspense"),j=Symbol.for("react.suspense_list"),ne=Symbol.for("react.memo"),Ue=Symbol.for("react.lazy"),kt=Symbol.for("react.offscreen"),xt=Symbol.iterator,Rt="@@iterator";function Xe(s){if(s===null||typeof s!="object")return null;var h=xt&&s[xt]||s[Rt];return typeof h=="function"?h:null}var xe={current:null},mt={transition:null},Re={current:null,isBatchingLegacy:!1,didScheduleLegacyUpdate:!1},et={current:null},B={},Ot=null;function at(s){Ot=s}B.setExtraStackFrame=function(s){Ot=s},B.getCurrentStack=null,B.getStackAddendum=function(){var s="";Ot&&(s+=Ot);var h=B.getCurrentStack;return h&&(s+=h()||""),s};var Fe=!1,ee=!1,Je=!1,X=!1,At=!1,Be={ReactCurrentDispatcher:xe,ReactCurrentBatchConfig:mt,ReactCurrentOwner:et};Be.ReactDebugCurrentFrame=B,Be.ReactCurrentActQueue=Re;function I(s){{for(var h=arguments.length,M=new Array(h>1?h-1:0),O=1;O1?h-1:0),O=1;O1){for(var Wt=Array(jt),qt=0;qt1){for(var lt=Array(qt),nn=0;nn is not supported and will be removed in a future major release. Did you mean to render instead?")),h.Provider},set:function(be){h.Provider=be}},_currentValue:{get:function(){return h._currentValue},set:function(be){h._currentValue=be}},_currentValue2:{get:function(){return h._currentValue2},set:function(be){h._currentValue2=be}},_threadCount:{get:function(){return h._threadCount},set:function(be){h._threadCount=be}},Consumer:{get:function(){return M||(M=!0,$("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?")),h.Consumer}},displayName:{get:function(){return h.displayName},set:function(be){K||(I("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.",be),K=!0)}}}),h.Consumer=Oe}return h._currentRenderer=null,h._currentRenderer2=null,h}var ya=-1,Kn=0,y=1,F=2;function _(s){if(s._status===ya){var h=s._result,M=h();if(M.then(function(Oe){if(s._status===Kn||s._status===ya){var be=s;be._status=y,be._result=Oe}},function(Oe){if(s._status===Kn||s._status===ya){var be=s;be._status=F,be._result=Oe}}),s._status===ya){var O=s;O._status=Kn,O._result=M}}if(s._status===y){var K=s._result;return K===void 0&&$(`lazy: Expected the result of a dynamic import() call. Instead received: %s + */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var me="18.3.1",Ee=Symbol.for("react.element"),fe=Symbol.for("react.portal"),Re=Symbol.for("react.fragment"),d=Symbol.for("react.strict_mode"),Q=Symbol.for("react.profiler"),he=Symbol.for("react.provider"),te=Symbol.for("react.context"),bt=Symbol.for("react.forward_ref"),I=Symbol.for("react.suspense"),H=Symbol.for("react.suspense_list"),K=Symbol.for("react.memo"),Oe=Symbol.for("react.lazy"),zt=Symbol.for("react.offscreen"),qt=Symbol.iterator,St="@@iterator";function Pe(s){if(s===null||typeof s!="object")return null;var m=qt&&s[qt]||s[St];return typeof m=="function"?m:null}var _e={current:null},ot={transition:null},_={current:null,isBatchingLegacy:!1,didScheduleLegacyUpdate:!1},ne={current:null},De={},Vt=null;function Me(s){Vt=s}De.setExtraStackFrame=function(s){Vt=s},De.getCurrentStack=null,De.getStackAddendum=function(){var s="";Vt&&(s+=Vt);var m=De.getCurrentStack;return m&&(s+=m()||""),s};var pe=!1,nt=!1,oe=!1,Le=!1,Ke=!1,le={ReactCurrentDispatcher:_e,ReactCurrentBatchConfig:ot,ReactCurrentOwner:ne};le.ReactDebugCurrentFrame=De,le.ReactCurrentActQueue=_;function W(s){{for(var m=arguments.length,k=new Array(m>1?m-1:0),N=1;N1?m-1:0),N=1;N1){for(var Yt=Array(At),Wt=0;Wt1){for(var lt=Array(Wt),en=0;en is not supported and will be removed in a future major release. Did you mean to render instead?")),m.Provider},set:function(Se){m.Provider=Se}},_currentValue:{get:function(){return m._currentValue},set:function(Se){m._currentValue=Se}},_currentValue2:{get:function(){return m._currentValue2},set:function(Se){m._currentValue2=Se}},_threadCount:{get:function(){return m._threadCount},set:function(Se){m._threadCount=Se}},Consumer:{get:function(){return k||(k=!0,q("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?")),m.Consumer}},displayName:{get:function(){return m.displayName},set:function(Se){X||(W("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.",Se),X=!0)}}}),m.Consumer=Ne}return m._currentRenderer=null,m._currentRenderer2=null,m}var ma=-1,h=0,B=1,D=2;function T(s){if(s._status===ma){var m=s._result,k=m();if(k.then(function(Ne){if(s._status===h||s._status===ma){var Se=s;Se._status=B,Se._result=Ne}},function(Ne){if(s._status===h||s._status===ma){var Se=s;Se._status=D,Se._result=Ne}}),s._status===ma){var N=s;N._status=h,N._result=k}}if(s._status===B){var X=s._result;return X===void 0&&q(`lazy: Expected the result of a dynamic import() call. Instead received: %s Your code should look like: const MyComponent = lazy(() => import('./MyComponent')) -Did you accidentally put curly braces around the import?`,K),"default"in K||$(`lazy: Expected the result of a dynamic import() call. Instead received: %s +Did you accidentally put curly braces around the import?`,X),"default"in X||q(`lazy: Expected the result of a dynamic import() call. Instead received: %s Your code should look like: - const MyComponent = lazy(() => import('./MyComponent'))`,K),K.default}else throw s._result}function S(s){var h={_status:ya,_result:s},M={$$typeof:Ue,_payload:h,_init:_};{var O,K;Object.defineProperties(M,{defaultProps:{configurable:!0,get:function(){return O},set:function(Oe){$("React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),O=Oe,Object.defineProperty(M,"defaultProps",{enumerable:!0})}},propTypes:{configurable:!0,get:function(){return K},set:function(Oe){$("React.lazy(...): It is not supported to assign `propTypes` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),K=Oe,Object.defineProperty(M,"propTypes",{enumerable:!0})}}})}return M}function w(s){s!=null&&s.$$typeof===ne?$("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof s!="function"?$("forwardRef requires a render function but was given %s.",s===null?"null":typeof s):s.length!==0&&s.length!==2&&$("forwardRef render functions accept exactly two parameters: props and ref. %s",s.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),s!=null&&(s.defaultProps!=null||s.propTypes!=null)&&$("forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?");var h={$$typeof:rt,render:s};{var M;Object.defineProperty(h,"displayName",{enumerable:!1,configurable:!0,get:function(){return M},set:function(O){M=O,!s.name&&!s.displayName&&(s.displayName=O)}})}return h}var c;c=Symbol.for("react.module.reference");function p(s){return!!(typeof s=="string"||typeof s=="function"||s===Ee||s===Z||At||s===d||s===G||s===j||X||s===kt||Fe||ee||Je||typeof s=="object"&&s!==null&&(s.$$typeof===Ue||s.$$typeof===ne||s.$$typeof===ye||s.$$typeof===oe||s.$$typeof===rt||s.$$typeof===c||s.getModuleId!==void 0))}function T(s,h){p(s)||$("memo: The first argument must be a component. Instead received: %s",s===null?"null":typeof s);var M={$$typeof:ne,type:s,compare:h===void 0?null:h};{var O;Object.defineProperty(M,"displayName",{enumerable:!1,configurable:!0,get:function(){return O},set:function(K){O=K,!s.name&&!s.displayName&&(s.displayName=K)}})}return M}function E(){var s=xe.current;return s===null&&$(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: + const MyComponent = lazy(() => import('./MyComponent'))`,X),X.default}else throw s._result}function x(s){var m={_status:ma,_result:s},k={$$typeof:Oe,_payload:m,_init:T};{var N,X;Object.defineProperties(k,{defaultProps:{configurable:!0,get:function(){return N},set:function(Ne){q("React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),N=Ne,Object.defineProperty(k,"defaultProps",{enumerable:!0})}},propTypes:{configurable:!0,get:function(){return X},set:function(Ne){q("React.lazy(...): It is not supported to assign `propTypes` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),X=Ne,Object.defineProperty(k,"propTypes",{enumerable:!0})}}})}return k}function O(s){s!=null&&s.$$typeof===K?q("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof s!="function"?q("forwardRef requires a render function but was given %s.",s===null?"null":typeof s):s.length!==0&&s.length!==2&&q("forwardRef render functions accept exactly two parameters: props and ref. %s",s.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),s!=null&&(s.defaultProps!=null||s.propTypes!=null)&&q("forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?");var m={$$typeof:bt,render:s};{var k;Object.defineProperty(m,"displayName",{enumerable:!1,configurable:!0,get:function(){return k},set:function(N){k=N,!s.name&&!s.displayName&&(s.displayName=N)}})}return m}var c;c=Symbol.for("react.module.reference");function y(s){return!!(typeof s=="string"||typeof s=="function"||s===Re||s===Q||Ke||s===d||s===I||s===H||Le||s===zt||pe||nt||oe||typeof s=="object"&&s!==null&&(s.$$typeof===Oe||s.$$typeof===K||s.$$typeof===he||s.$$typeof===te||s.$$typeof===bt||s.$$typeof===c||s.getModuleId!==void 0))}function S(s,m){y(s)||q("memo: The first argument must be a component. Instead received: %s",s===null?"null":typeof s);var k={$$typeof:K,type:s,compare:m===void 0?null:m};{var N;Object.defineProperty(k,"displayName",{enumerable:!1,configurable:!0,get:function(){return N},set:function(X){N=X,!s.name&&!s.displayName&&(s.displayName=X)}})}return k}function A(){var s=_e.current;return s===null&&q(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 1. You might have mismatching versions of React and the renderer (such as React DOM) 2. You might be breaking the Rules of Hooks 3. You might have more than one copy of React in the same app -See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.`),s}function P(s){var h=E();if(s._context!==void 0){var M=s._context;M.Consumer===s?$("Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be removed in a future major release. Did you mean to call useContext(Context) instead?"):M.Provider===s&&$("Calling useContext(Context.Provider) is not supported. Did you mean to call useContext(Context) instead?")}return h.useContext(s)}function ae(s){var h=E();return h.useState(s)}function H(s,h,M){var O=E();return O.useReducer(s,h,M)}function le(s){var h=E();return h.useRef(s)}function Qe(s,h){var M=E();return M.useEffect(s,h)}function _e(s,h){var M=E();return M.useInsertionEffect(s,h)}function ot(s,h){var M=E();return M.useLayoutEffect(s,h)}function fn(s,h){var M=E();return M.useCallback(s,h)}function _n(s,h){var M=E();return M.useMemo(s,h)}function en(s,h,M){var O=E();return O.useImperativeHandle(s,h,M)}function Ft(s,h){{var M=E();return M.useDebugValue(s,h)}}function Me(){var s=E();return s.useTransition()}function Dt(s){var h=E();return h.useDeferredValue(s)}function Zn(){var s=E();return s.useId()}function yr(s,h,M){var O=E();return O.useSyncExternalStore(s,h,M)}var Va=0,Br,ga,ba,$r,gi,dn,_t;function Sa(){}Sa.__reactDisabledLog=!0;function Ta(){{if(Va===0){Br=console.log,ga=console.info,ba=console.warn,$r=console.error,gi=console.group,dn=console.groupCollapsed,_t=console.groupEnd;var s={configurable:!0,enumerable:!0,value:Sa,writable:!0};Object.defineProperties(console,{info:s,log:s,warn:s,error:s,group:s,groupCollapsed:s,groupEnd:s})}Va++}}function sa(){{if(Va--,Va===0){var s={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:Ze({},s,{value:Br}),info:Ze({},s,{value:ga}),warn:Ze({},s,{value:ba}),error:Ze({},s,{value:$r}),group:Ze({},s,{value:gi}),groupCollapsed:Ze({},s,{value:dn}),groupEnd:Ze({},s,{value:_t})})}Va<0&&$("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var bi=Be.ReactCurrentDispatcher,Yr;function ao(s,h,M){{if(Yr===void 0)try{throw Error()}catch(K){var O=K.stack.trim().match(/\n( *(at )?)/);Yr=O&&O[1]||""}return` -`+Yr+s}}var Si=!1,ro;{var ol=typeof WeakMap=="function"?WeakMap:Map;ro=new ol}function Yu(s,h){if(!s||Si)return"";{var M=ro.get(s);if(M!==void 0)return M}var O;Si=!0;var K=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var Oe;Oe=bi.current,bi.current=null,Ta();try{if(h){var be=function(){throw Error()};if(Object.defineProperty(be.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(be,[])}catch(vn){O=vn}Reflect.construct(s,[],be)}else{try{be.call()}catch(vn){O=vn}s.call(be.prototype)}}else{try{throw Error()}catch(vn){O=vn}s()}}catch(vn){if(vn&&O&&typeof vn.stack=="string"){for(var qe=vn.stack.split(` -`),ft=O.stack.split(` -`),jt=qe.length-1,Wt=ft.length-1;jt>=1&&Wt>=0&&qe[jt]!==ft[Wt];)Wt--;for(;jt>=1&&Wt>=0;jt--,Wt--)if(qe[jt]!==ft[Wt]){if(jt!==1||Wt!==1)do if(jt--,Wt--,Wt<0||qe[jt]!==ft[Wt]){var qt=` -`+qe[jt].replace(" at new "," at ");return s.displayName&&qt.includes("")&&(qt=qt.replace("",s.displayName)),typeof s=="function"&&ro.set(s,qt),qt}while(jt>=1&&Wt>=0);break}}}finally{Si=!1,bi.current=Oe,sa(),Error.prepareStackTrace=K}var lt=s?s.displayName||s.name:"",nn=lt?ao(lt):"";return typeof s=="function"&&ro.set(s,nn),nn}function ll(s,h,M){return Yu(s,!1)}function af(s){var h=s.prototype;return!!(h&&h.isReactComponent)}function Ti(s,h,M){if(s==null)return"";if(typeof s=="function")return Yu(s,af(s));if(typeof s=="string")return ao(s);switch(s){case G:return ao("Suspense");case j:return ao("SuspenseList")}if(typeof s=="object")switch(s.$$typeof){case rt:return ll(s.render);case ne:return Ti(s.type,h,M);case Ue:{var O=s,K=O._payload,Oe=O._init;try{return Ti(Oe(K),h,M)}catch{}}}return""}var Iu={},ul=Be.ReactDebugCurrentFrame;function Et(s){if(s){var h=s._owner,M=Ti(s.type,s._source,h?h.type:null);ul.setExtraStackFrame(M)}else ul.setExtraStackFrame(null)}function rf(s,h,M,O,K){{var Oe=Function.call.bind(ue);for(var be in s)if(Oe(s,be)){var qe=void 0;try{if(typeof s[be]!="function"){var ft=Error((O||"React class")+": "+M+" type `"+be+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof s[be]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw ft.name="Invariant Violation",ft}qe=s[be](h,be,O,M,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(jt){qe=jt}qe&&!(qe instanceof Error)&&(Et(K),$("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",O||"React class",M,be,typeof qe),Et(null)),qe instanceof Error&&!(qe.message in Iu)&&(Iu[qe.message]=!0,Et(K),$("Failed %s type: %s",M,qe.message),Et(null))}}}function gr(s){if(s){var h=s._owner,M=Ti(s.type,s._source,h?h.type:null);at(M)}else at(null)}var Ke;Ke=!1;function sl(){if(et.current){var s=pe(et.current.type);if(s)return` +See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.`),s}function ve(s){var m=A();if(s._context!==void 0){var k=s._context;k.Consumer===s?q("Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be removed in a future major release. Did you mean to call useContext(Context) instead?"):k.Provider===s&&q("Calling useContext(Context.Provider) is not supported. Did you mean to call useContext(Context) instead?")}return m.useContext(s)}function z(s){var m=A();return m.useState(s)}function ue(s,m,k){var N=A();return N.useReducer(s,m,k)}function ye(s){var m=A();return m.useRef(s)}function ze(s,m){var k=A();return k.useEffect(s,m)}function it(s,m){var k=A();return k.useInsertionEffect(s,m)}function Ft(s,m){var k=A();return k.useLayoutEffect(s,m)}function nn(s,m){var k=A();return k.useCallback(s,m)}function Zt(s,m){var k=A();return k.useMemo(s,m)}function Pn(s,m,k){var N=A();return N.useImperativeHandle(s,m,k)}function ft(s,m){{var k=A();return k.useDebugValue(s,m)}}function be(){var s=A();return s.useTransition()}function Rn(s){var m=A();return m.useDeferredValue(s)}function Ua(){var s=A();return s.useId()}function il(s,m,k){var N=A();return N.useSyncExternalStore(s,m,k)}var Ha=0,ha,ya,$r,yi,un,_t,za;function oa(){}oa.__reactDisabledLog=!0;function hr(){{if(Ha===0){ha=console.log,ya=console.info,$r=console.warn,yi=console.error,un=console.group,_t=console.groupCollapsed,za=console.groupEnd;var s={configurable:!0,enumerable:!0,value:oa,writable:!0};Object.defineProperties(console,{info:s,log:s,warn:s,error:s,group:s,groupCollapsed:s,groupEnd:s})}Ha++}}function nr(){{if(Ha--,Ha===0){var s={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:We({},s,{value:ha}),info:We({},s,{value:ya}),warn:We({},s,{value:$r}),error:We({},s,{value:yi}),group:We({},s,{value:un}),groupCollapsed:We({},s,{value:_t}),groupEnd:We({},s,{value:za})})}Ha<0&&q("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var gi=le.ReactCurrentDispatcher,Pr;function no(s,m,k){{if(Pr===void 0)try{throw Error()}catch(X){var N=X.stack.trim().match(/\n( *(at )?)/);Pr=N&&N[1]||""}return` +`+Pr+s}}var bi=!1,ao;{var ol=typeof WeakMap=="function"?WeakMap:Map;ao=new ol}function Yu(s,m){if(!s||bi)return"";{var k=ao.get(s);if(k!==void 0)return k}var N;bi=!0;var X=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var Ne;Ne=gi.current,gi.current=null,hr();try{if(m){var Se=function(){throw Error()};if(Object.defineProperty(Se.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Se,[])}catch(dn){N=dn}Reflect.construct(s,[],Se)}else{try{Se.call()}catch(dn){N=dn}s.call(Se.prototype)}}else{try{throw Error()}catch(dn){N=dn}s()}}catch(dn){if(dn&&N&&typeof dn.stack=="string"){for(var Ie=dn.stack.split(` +`),dt=N.stack.split(` +`),At=Ie.length-1,Yt=dt.length-1;At>=1&&Yt>=0&&Ie[At]!==dt[Yt];)Yt--;for(;At>=1&&Yt>=0;At--,Yt--)if(Ie[At]!==dt[Yt]){if(At!==1||Yt!==1)do if(At--,Yt--,Yt<0||Ie[At]!==dt[Yt]){var Wt=` +`+Ie[At].replace(" at new "," at ");return s.displayName&&Wt.includes("")&&(Wt=Wt.replace("",s.displayName)),typeof s=="function"&&ao.set(s,Wt),Wt}while(At>=1&&Yt>=0);break}}}finally{bi=!1,gi.current=Ne,nr(),Error.prepareStackTrace=X}var lt=s?s.displayName||s.name:"",en=lt?no(lt):"";return typeof s=="function"&&ao.set(s,en),en}function ll(s,m,k){return Yu(s,!1)}function af(s){var m=s.prototype;return!!(m&&m.isReactComponent)}function Si(s,m,k){if(s==null)return"";if(typeof s=="function")return Yu(s,af(s));if(typeof s=="string")return no(s);switch(s){case I:return no("Suspense");case H:return no("SuspenseList")}if(typeof s=="object")switch(s.$$typeof){case bt:return ll(s.render);case K:return Si(s.type,m,k);case Oe:{var N=s,X=N._payload,Ne=N._init;try{return Si(Ne(X),m,k)}catch{}}}return""}var Wu={},ul=le.ReactDebugCurrentFrame;function xt(s){if(s){var m=s._owner,k=Si(s.type,s._source,m?m.type:null);ul.setExtraStackFrame(k)}else ul.setExtraStackFrame(null)}function rf(s,m,k,N,X){{var Ne=Function.call.bind(yt);for(var Se in s)if(Ne(s,Se)){var Ie=void 0;try{if(typeof s[Se]!="function"){var dt=Error((N||"React class")+": "+k+" type `"+Se+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof s[Se]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw dt.name="Invariant Violation",dt}Ie=s[Se](m,Se,N,k,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(At){Ie=At}Ie&&!(Ie instanceof Error)&&(xt(X),q("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",N||"React class",k,Se,typeof Ie),xt(null)),Ie instanceof Error&&!(Ie.message in Wu)&&(Wu[Ie.message]=!0,xt(X),q("Failed %s type: %s",k,Ie.message),xt(null))}}}function yr(s){if(s){var m=s._owner,k=Si(s.type,s._source,m?m.type:null);Me(k)}else Me(null)}var Xe;Xe=!1;function sl(){if(ne.current){var s=Ge(ne.current.type);if(s)return` -Check the render method of \``+s+"`."}return""}function Jn(s){if(s!==void 0){var h=s.fileName.replace(/^.*[\\\/]/,""),M=s.lineNumber;return` +Check the render method of \``+s+"`."}return""}function Qn(s){if(s!==void 0){var m=s.fileName.replace(/^.*[\\\/]/,""),k=s.lineNumber;return` -Check your code at `+h+":"+M+"."}return""}function Ei(s){return s!=null?Jn(s.__source):""}var Ir={};function of(s){var h=sl();if(!h){var M=typeof s=="string"?s:s.displayName||s.name;M&&(h=` +Check your code at `+m+":"+k+"."}return""}function Ti(s){return s!=null?Qn(s.__source):""}var Yr={};function of(s){var m=sl();if(!m){var k=typeof s=="string"?s:s.displayName||s.name;k&&(m=` -Check the top-level render call using <`+M+">.")}return h}function Mn(s,h){if(!(!s._store||s._store.validated||s.key!=null)){s._store.validated=!0;var M=of(h);if(!Ir[M]){Ir[M]=!0;var O="";s&&s._owner&&s._owner!==et.current&&(O=" It was passed a child from "+pe(s._owner.type)+"."),gr(s),$('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',M,O),gr(null)}}}function tn(s,h){if(typeof s=="object"){if(Xt(s))for(var M=0;M",K=" Did you accidentally export a JSX literal instead of a component?"):be=typeof s,$("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",be,K)}var qe=Pe.apply(this,arguments);if(qe==null)return qe;if(O)for(var ft=2;ft10&&I("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table."),O._updatedFibers.clear()}}}var fl=!1,io=null;function uf(s){if(io===null)try{var h=("require"+Math.random()).slice(0,7),M=C&&C[h];io=M.call(C,"timers").setImmediate}catch{io=function(K){fl===!1&&(fl=!0,typeof MessageChannel>"u"&&$("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var Oe=new MessageChannel;Oe.port1.onmessage=K,Oe.port2.postMessage(void 0)}}return io(s)}var Wr=0,Ci=!1;function dl(s){{var h=Wr;Wr++,Re.current===null&&(Re.current=[]);var M=Re.isBatchingLegacy,O;try{if(Re.isBatchingLegacy=!0,O=s(),!M&&Re.didScheduleLegacyUpdate){var K=Re.current;K!==null&&(Re.didScheduleLegacyUpdate=!1,uo(K))}}catch(lt){throw br(h),lt}finally{Re.isBatchingLegacy=M}if(O!==null&&typeof O=="object"&&typeof O.then=="function"){var Oe=O,be=!1,qe={then:function(lt,nn){be=!0,Oe.then(function(vn){br(h),Wr===0?oo(vn,lt,nn):lt(vn)},function(vn){br(h),nn(vn)})}};return!Ci&&typeof Promise<"u"&&Promise.resolve().then(function(){}).then(function(){be||(Ci=!0,$("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),qe}else{var ft=O;if(br(h),Wr===0){var jt=Re.current;jt!==null&&(uo(jt),Re.current=null);var Wt={then:function(lt,nn){Re.current===null?(Re.current=[],oo(ft,lt,nn)):lt(ft)}};return Wt}else{var qt={then:function(lt,nn){lt(ft)}};return qt}}}}function br(s){s!==Wr-1&&$("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "),Wr=s}function oo(s,h,M){{var O=Re.current;if(O!==null)try{uo(O),uf(function(){O.length===0?(Re.current=null,h(s)):oo(s,h,M)})}catch(K){M(K)}else h(s)}}var lo=!1;function uo(s){if(!lo){lo=!0;var h=0;try{for(;h.")}return m}function Dn(s,m){if(!(!s._store||s._store.validated||s.key!=null)){s._store.validated=!0;var k=of(m);if(!Yr[k]){Yr[k]=!0;var N="";s&&s._owner&&s._owner!==ne.current&&(N=" It was passed a child from "+Ge(s._owner.type)+"."),yr(s),q('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',k,N),yr(null)}}}function Jt(s,m){if(typeof s=="object"){if(F(s))for(var k=0;k",X=" Did you accidentally export a JSX literal instead of a component?"):Se=typeof s,q("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",Se,X)}var Ie=Je.apply(this,arguments);if(Ie==null)return Ie;if(N)for(var dt=2;dt10&&W("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table."),N._updatedFibers.clear()}}}var fl=!1,ro=null;function uf(s){if(ro===null)try{var m=("require"+Math.random()).slice(0,7),k=w&&w[m];ro=k.call(w,"timers").setImmediate}catch{ro=function(X){fl===!1&&(fl=!0,typeof MessageChannel>"u"&&q("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var Ne=new MessageChannel;Ne.port1.onmessage=X,Ne.port2.postMessage(void 0)}}return ro(s)}var Wr=0,Ei=!1;function dl(s){{var m=Wr;Wr++,_.current===null&&(_.current=[]);var k=_.isBatchingLegacy,N;try{if(_.isBatchingLegacy=!0,N=s(),!k&&_.didScheduleLegacyUpdate){var X=_.current;X!==null&&(_.didScheduleLegacyUpdate=!1,lo(X))}}catch(lt){throw gr(m),lt}finally{_.isBatchingLegacy=k}if(N!==null&&typeof N=="object"&&typeof N.then=="function"){var Ne=N,Se=!1,Ie={then:function(lt,en){Se=!0,Ne.then(function(dn){gr(m),Wr===0?io(dn,lt,en):lt(dn)},function(dn){gr(m),en(dn)})}};return!Ei&&typeof Promise<"u"&&Promise.resolve().then(function(){}).then(function(){Se||(Ei=!0,q("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),Ie}else{var dt=N;if(gr(m),Wr===0){var At=_.current;At!==null&&(lo(At),_.current=null);var Yt={then:function(lt,en){_.current===null?(_.current=[],io(dt,lt,en)):lt(dt)}};return Yt}else{var Wt={then:function(lt,en){lt(dt)}};return Wt}}}}function gr(s){s!==Wr-1&&q("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "),Wr=s}function io(s,m,k){{var N=_.current;if(N!==null)try{lo(N),uf(function(){N.length===0?(_.current=null,m(s)):io(s,m,k)})}catch(X){k(X)}else m(s)}}var oo=!1;function lo(s){if(!oo){oo=!0;var m=0;try{for(;m.")}return h}function Mn(s,h){if(!( * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */return function(){var C=Zc(),k=Symbol.for("react.element"),me=Symbol.for("react.portal"),Se=Symbol.for("react.fragment"),ve=Symbol.for("react.strict_mode"),Ee=Symbol.for("react.profiler"),d=Symbol.for("react.provider"),Z=Symbol.for("react.context"),ye=Symbol.for("react.forward_ref"),oe=Symbol.for("react.suspense"),rt=Symbol.for("react.suspense_list"),G=Symbol.for("react.memo"),j=Symbol.for("react.lazy"),ne=Symbol.for("react.offscreen"),Ue=Symbol.iterator,kt="@@iterator";function xt(c){if(c===null||typeof c!="object")return null;var p=Ue&&c[Ue]||c[kt];return typeof p=="function"?p:null}var Rt=C.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;function Xe(c){{for(var p=arguments.length,T=new Array(p>1?p-1:0),E=1;E=1&&ot>=0&&le[_e]!==Qe[ot];)ot--;for(;_e>=1&&ot>=0;_e--,ot--)if(le[_e]!==Qe[ot]){if(_e!==1||ot!==1)do if(_e--,ot--,ot<0||le[_e]!==Qe[ot]){var fn=` -`+le[_e].replace(" at new "," at ");return c.displayName&&fn.includes("")&&(fn=fn.replace("",c.displayName)),typeof c=="function"&&We.set(c,fn),fn}while(_e>=1&&ot>=0);break}}}finally{it=!1,$t.current=ae,st(),Error.prepareStackTrace=P}var _n=c?c.displayName||c.name:"",en=_n?Nt(_n):"";return typeof c=="function"&&We.set(c,en),en}function Xt(c,p,T){return xn(c,!1)}function cn(c){var p=c.prototype;return!!(p&&p.isReactComponent)}function Yt(c,p,T){if(c==null)return"";if(typeof c=="function")return xn(c,cn(c));if(typeof c=="string")return Nt(c);switch(c){case oe:return Nt("Suspense");case rt:return Nt("SuspenseList")}if(typeof c=="object")switch(c.$$typeof){case ye:return Xt(c.render);case G:return Yt(c.type,p,T);case j:{var E=c,P=E._payload,ae=E._init;try{return Yt(ae(P),p,T)}catch{}}}return""}var an=Object.prototype.hasOwnProperty,Rn={},U=Rt.ReactDebugCurrentFrame;function Y(c){if(c){var p=c._owner,T=Yt(c.type,c._source,p?p.type:null);U.setExtraStackFrame(T)}else U.setExtraStackFrame(null)}function pe(c,p,T,E,P){{var ae=Function.call.bind(an);for(var H in c)if(ae(c,H)){var le=void 0;try{if(typeof c[H]!="function"){var Qe=Error((E||"React class")+": "+T+" type `"+H+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof c[H]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw Qe.name="Invariant Violation",Qe}le=c[H](p,H,E,T,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(_e){le=_e}le&&!(le instanceof Error)&&(Y(P),Xe("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",E||"React class",T,H,typeof le),Y(null)),le instanceof Error&&!(le.message in Rn)&&(Rn[le.message]=!0,Y(P),Xe("Failed %s type: %s",T,le.message),Y(null))}}}var ue=Array.isArray;function Ce(c){return ue(c)}function De(c){{var p=typeof Symbol=="function"&&Symbol.toStringTag,T=p&&c[Symbol.toStringTag]||c.constructor.name||"Object";return T}}function yt(c){try{return Le(c),!1}catch{return!0}}function Le(c){return""+c}function ct(c){if(yt(c))return Xe("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",De(c)),Le(c)}var wt=Rt.ReactCurrentOwner,un={key:!0,ref:!0,__self:!0,__source:!0},wn,Q;function ge(c){if(an.call(c,"ref")){var p=Object.getOwnPropertyDescriptor(c,"ref").get;if(p&&p.isReactWarning)return!1}return c.ref!==void 0}function Pe(c){if(an.call(c,"key")){var p=Object.getOwnPropertyDescriptor(c,"key").get;if(p&&p.isReactWarning)return!1}return c.key!==void 0}function gt(c,p){typeof c.ref=="string"&&wt.current}function Pt(c,p){{var T=function(){wn||(wn=!0,Xe("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",p))};T.isReactWarning=!0,Object.defineProperty(c,"key",{get:T,configurable:!0})}}function Qt(c,p){{var T=function(){Q||(Q=!0,Xe("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",p))};T.isReactWarning=!0,Object.defineProperty(c,"ref",{get:T,configurable:!0})}}var Kt=function(c,p,T,E,P,ae,H){var le={$$typeof:k,type:c,key:p,ref:T,props:H,_owner:ae};return le._store={},Object.defineProperty(le._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(le,"_self",{configurable:!1,enumerable:!1,writable:!1,value:E}),Object.defineProperty(le,"_source",{configurable:!1,enumerable:!1,writable:!1,value:P}),Object.freeze&&(Object.freeze(le.props),Object.freeze(le)),le};function Dn(c,p,T,E,P){{var ae,H={},le=null,Qe=null;T!==void 0&&(ct(T),le=""+T),Pe(p)&&(ct(p.key),le=""+p.key),ge(p)&&(Qe=p.ref,gt(p,P));for(ae in p)an.call(p,ae)&&!un.hasOwnProperty(ae)&&(H[ae]=p[ae]);if(c&&c.defaultProps){var _e=c.defaultProps;for(ae in _e)H[ae]===void 0&&(H[ae]=_e[ae])}if(le||Qe){var ot=typeof c=="function"?c.displayName||c.name||"Unknown":c;le&&Pt(H,ot),Qe&&Qt(H,ot)}return Kt(c,le,Qe,P,E,wt.current,H)}}var It=Rt.ReactCurrentOwner,Vt=Rt.ReactDebugCurrentFrame;function bt(c){if(c){var p=c._owner,T=Yt(c.type,c._source,p?p.type:null);Vt.setExtraStackFrame(T)}else Vt.setExtraStackFrame(null)}var ua;ua=!1;function ha(c){return typeof c=="object"&&c!==null&&c.$$typeof===k}function Yn(){{if(It.current){var c=X(It.current.type);if(c)return` + */return function(){var w=Zc(),L=Symbol.for("react.element"),me=Symbol.for("react.portal"),Ee=Symbol.for("react.fragment"),fe=Symbol.for("react.strict_mode"),Re=Symbol.for("react.profiler"),d=Symbol.for("react.provider"),Q=Symbol.for("react.context"),he=Symbol.for("react.forward_ref"),te=Symbol.for("react.suspense"),bt=Symbol.for("react.suspense_list"),I=Symbol.for("react.memo"),H=Symbol.for("react.lazy"),K=Symbol.for("react.offscreen"),Oe=Symbol.iterator,zt="@@iterator";function qt(c){if(c===null||typeof c!="object")return null;var y=Oe&&c[Oe]||c[zt];return typeof y=="function"?y:null}var St=w.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;function Pe(c){{for(var y=arguments.length,S=new Array(y>1?y-1:0),A=1;A=1&&Ft>=0&&ye[it]!==ze[Ft];)Ft--;for(;it>=1&&Ft>=0;it--,Ft--)if(ye[it]!==ze[Ft]){if(it!==1||Ft!==1)do if(it--,Ft--,Ft<0||ye[it]!==ze[Ft]){var nn=` +`+ye[it].replace(" at new "," at ");return c.displayName&&nn.includes("")&&(nn=nn.replace("",c.displayName)),typeof c=="function"&&Gt.set(c,nn),nn}while(it>=1&&Ft>=0);break}}}finally{Qe=!1,et.current=z,cn(),Error.prepareStackTrace=ve}var Zt=c?c.displayName||c.name:"",Pn=Zt?gt(Zt):"";return typeof c=="function"&&Gt.set(c,Pn),Pn}function F(c,y,S){return fn(c,!1)}function G(c){var y=c.prototype;return!!(y&&y.isReactComponent)}function Te(c,y,S){if(c==null)return"";if(typeof c=="function")return fn(c,G(c));if(typeof c=="string")return gt(c);switch(c){case te:return gt("Suspense");case bt:return gt("SuspenseList")}if(typeof c=="object")switch(c.$$typeof){case he:return F(c.render);case I:return Te(c.type,y,S);case H:{var A=c,ve=A._payload,z=A._init;try{return Te(z(ve),y,S)}catch{}}}return""}var Ce=Object.prototype.hasOwnProperty,Ze={},st=St.ReactDebugCurrentFrame;function ht(c){if(c){var y=c._owner,S=Te(c.type,c._source,y?y.type:null);st.setExtraStackFrame(S)}else st.setExtraStackFrame(null)}function Ge(c,y,S,A,ve){{var z=Function.call.bind(Ce);for(var ue in c)if(z(c,ue)){var ye=void 0;try{if(typeof c[ue]!="function"){var ze=Error((A||"React class")+": "+S+" type `"+ue+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof c[ue]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw ze.name="Invariant Violation",ze}ye=c[ue](y,ue,A,S,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(it){ye=it}ye&&!(ye instanceof Error)&&(ht(ve),Pe("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",A||"React class",S,ue,typeof ye),ht(null)),ye instanceof Error&&!(ye.message in Ze)&&(Ze[ye.message]=!0,ht(ve),Pe("Failed %s type: %s",S,ye.message),ht(null))}}}var yt=Array.isArray;function rt(c){return yt(c)}function tn(c){{var y=typeof Symbol=="function"&&Symbol.toStringTag,S=y&&c[Symbol.toStringTag]||c.constructor.name||"Object";return S}}function mn(c){try{return Ot(c),!1}catch{return!0}}function Ot(c){return""+c}function Lt(c){if(mn(c))return Pe("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",tn(c)),Ot(c)}var hn=St.ReactCurrentOwner,ia={key:!0,ref:!0,__self:!0,__source:!0},va,Z;function xe(c){if(Ce.call(c,"ref")){var y=Object.getOwnPropertyDescriptor(c,"ref").get;if(y&&y.isReactWarning)return!1}return c.ref!==void 0}function Je(c){if(Ce.call(c,"key")){var y=Object.getOwnPropertyDescriptor(c,"key").get;if(y&&y.isReactWarning)return!1}return c.key!==void 0}function Rt(c,y){typeof c.ref=="string"&&hn.current}function Bt(c,y){{var S=function(){va||(va=!0,Pe("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",y))};S.isReactWarning=!0,Object.defineProperty(c,"key",{get:S,configurable:!0})}}function Xt(c,y){{var S=function(){Z||(Z=!0,Pe("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",y))};S.isReactWarning=!0,Object.defineProperty(c,"ref",{get:S,configurable:!0})}}var Qt=function(c,y,S,A,ve,z,ue){var ye={$$typeof:L,type:c,key:y,ref:S,props:ue,_owner:z};return ye._store={},Object.defineProperty(ye._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(ye,"_self",{configurable:!1,enumerable:!1,writable:!1,value:A}),Object.defineProperty(ye,"_source",{configurable:!1,enumerable:!1,writable:!1,value:ve}),Object.freeze&&(Object.freeze(ye.props),Object.freeze(ye)),ye};function wn(c,y,S,A,ve){{var z,ue={},ye=null,ze=null;S!==void 0&&(Lt(S),ye=""+S),Je(y)&&(Lt(y.key),ye=""+y.key),xe(y)&&(ze=y.ref,Rt(y,ve));for(z in y)Ce.call(y,z)&&!ia.hasOwnProperty(z)&&(ue[z]=y[z]);if(c&&c.defaultProps){var it=c.defaultProps;for(z in it)ue[z]===void 0&&(ue[z]=it[z])}if(ye||ze){var Ft=typeof c=="function"?c.displayName||c.name||"Unknown":c;ye&&Bt(ue,Ft),ze&&Xt(ue,Ft)}return Qt(c,ye,ze,ve,A,hn.current,ue)}}var Pt=St.ReactCurrentOwner,Tt=St.ReactDebugCurrentFrame;function Dt(c){if(c){var y=c._owner,S=Te(c.type,c._source,y?y.type:null);Tt.setExtraStackFrame(S)}else Tt.setExtraStackFrame(null)}var er;er=!1;function $n(c){return typeof c=="object"&&c!==null&&c.$$typeof===L}function pa(){{if(Pt.current){var c=Le(Pt.current.type);if(c)return` -Check the render method of \``+c+"`."}return""}}function nr(c){return""}var Vr={};function Pr(c){{var p=Yn();if(!p){var T=typeof c=="string"?c:c.displayName||c.name;T&&(p=` +Check the render method of \``+c+"`."}return""}}function tr(c){return""}var Fr={};function Vr(c){{var y=pa();if(!y){var S=typeof c=="string"?c:c.displayName||c.name;S&&(y=` -Check the top-level render call using <`+T+">.")}return p}}function hr(c,p){{if(!c._store||c._store.validated||c.key!=null)return;c._store.validated=!0;var T=Pr(p);if(Vr[T])return;Vr[T]=!0;var E="";c&&c._owner&&c._owner!==It.current&&(E=" It was passed a child from "+X(c._owner.type)+"."),bt(c),Xe('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',T,E),bt(null)}}function Ha(c,p){{if(typeof c!="object")return;if(Ce(c))for(var T=0;T",le=" Did you accidentally export a JSX literal instead of a component?"):_e=typeof c,Xe("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",_e,le)}var ot=Dn(c,p,T,P,ae);if(ot==null)return ot;if(H){var fn=p.children;if(fn!==void 0)if(E)if(Ce(fn)){for(var _n=0;_n0?"{key: someKey, "+Ft.join(": ..., ")+": ...}":"{key: someKey}";if(!Kn[en+Me]){var Dt=Ft.length>0?"{"+Ft.join(": ..., ")+": ...}":"{}";Xe(`A props object containing a "key" prop is being spread into JSX: +Check the top-level render call using <`+S+">.")}return y}}function wa(c,y){{if(!c._store||c._store.validated||c.key!=null)return;c._store.validated=!0;var S=Vr(y);if(Fr[S])return;Fr[S]=!0;var A="";c&&c._owner&&c._owner!==Pt.current&&(A=" It was passed a child from "+Le(c._owner.type)+"."),Dt(c),Pe('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',S,A),Dt(null)}}function ja(c,y){{if(typeof c!="object")return;if(rt(c))for(var S=0;S",ye=" Did you accidentally export a JSX literal instead of a component?"):it=typeof c,Pe("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",it,ye)}var Ft=wn(c,y,S,ve,z);if(Ft==null)return Ft;if(ue){var nn=y.children;if(nn!==void 0)if(A)if(rt(nn)){for(var Zt=0;Zt0?"{key: someKey, "+ft.join(": ..., ")+": ...}":"{key: someKey}";if(!h[Pn+be]){var Rn=ft.length>0?"{"+ft.join(": ..., ")+": ...}":"{}";Pe(`A props object containing a "key" prop is being spread into JSX: let props = %s; <%s {...props} /> React keys must be passed directly to JSX without using spread: let props = %s; - <%s key={someKey} {...props} />`,Me,en,Dt,en),Kn[en+Me]=!0}}return c===Se?ya(ot):Fa(ot),ot}}function F(c,p,T){return y(c,p,T,!0)}function _(c,p,T){return y(c,p,T,!1)}var S=_,w=F;il.Fragment=Se,il.jsx=S,il.jsxs=w}(),il}dm.exports=FS();var m=dm.exports,hm={exports:{}},Jc={exports:{}},ef={},ym;function VS(){return ym||(ym=1,function(C){/** + <%s key={someKey} {...props} />`,be,Pn,Rn,Pn),h[Pn+be]=!0}}return c===Ee?ma(Ft):Br(Ft),Ft}}function D(c,y,S){return B(c,y,S,!0)}function T(c,y,S){return B(c,y,S,!1)}var x=T,O=D;rl.Fragment=Ee,rl.jsx=x,rl.jsxs=O}(),rl}dm.exports=F0();var p=dm.exports,hm={exports:{}},Jc={exports:{}},ef={},ym;function V0(){return ym||(ym=1,function(w){/** * @license React * scheduler.development.js * @@ -57,7 +57,7 @@ React keys must be passed directly to JSX without using spread: * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var k=!1,me=5;function Se(Q,ge){var Pe=Q.length;Q.push(ge),d(Q,ge,Pe)}function ve(Q){return Q.length===0?null:Q[0]}function Ee(Q){if(Q.length===0)return null;var ge=Q[0],Pe=Q.pop();return Pe!==ge&&(Q[0]=Pe,Z(Q,Pe,0)),ge}function d(Q,ge,Pe){for(var gt=Pe;gt>0;){var Pt=gt-1>>>1,Qt=Q[Pt];if(ye(Qt,ge)>0)Q[Pt]=ge,Q[gt]=Qt,gt=Pt;else return}}function Z(Q,ge,Pe){for(var gt=Pe,Pt=Q.length,Qt=Pt>>>1;gtPe&&(!Q||U()));){var gt=Je.callback;if(typeof gt=="function"){Je.callback=null,X=Je.priorityLevel;var Pt=Je.expirationTime<=Pe,Qt=gt(Pt);Pe=C.unstable_now(),typeof Qt=="function"?Je.callback=Qt:Je===ve(at)&&Ee(at),nt(Pe)}else Ee(at);Je=ve(at)}if(Je!==null)return!0;var Kt=ve(Fe);return Kt!==null&&ct(ht,Kt.startTime-Pe),!1}function Ne(Q,ge){switch(Q){case oe:case rt:case G:case j:case ne:break;default:Q=G}var Pe=X;X=Q;try{return ge()}finally{X=Pe}}function st(Q){var ge;switch(X){case oe:case rt:case G:ge=G;break;default:ge=X;break}var Pe=X;X=ge;try{return Q()}finally{X=Pe}}function $t(Q){var ge=X;return function(){var Pe=X;X=ge;try{return Q.apply(this,arguments)}finally{X=Pe}}}function Ie(Q,ge,Pe){var gt=C.unstable_now(),Pt;if(typeof Pe=="object"&&Pe!==null){var Qt=Pe.delay;typeof Qt=="number"&&Qt>0?Pt=gt+Qt:Pt=gt}else Pt=gt;var Kt;switch(Q){case oe:Kt=mt;break;case rt:Kt=Re;break;case ne:Kt=Ot;break;case j:Kt=B;break;case G:default:Kt=et;break}var Dn=Pt+Kt,It={id:ee++,callback:ge,priorityLevel:Q,startTime:Pt,expirationTime:Dn,sortIndex:-1};return Pt>gt?(It.sortIndex=Pt,Se(Fe,It),ve(at)===null&&It===ve(Fe)&&(I?wt():I=!0,ct(ht,Pt-gt))):(It.sortIndex=Dn,Se(at,It),!Be&&!At&&(Be=!0,Le(Ze))),It}function Nt(){}function it(){!Be&&!At&&(Be=!0,Le(Ze))}function We(){return ve(at)}function Jt(Q){Q.callback=null}function xn(){return X}var Xt=!1,cn=null,Yt=-1,an=me,Rn=-1;function U(){var Q=C.unstable_now()-Rn;return!(Q125){console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported");return}Q>0?an=Math.floor(1e3/Q):an=me}var ue=function(){if(cn!==null){var Q=C.unstable_now();Rn=Q;var ge=!0,Pe=!0;try{Pe=cn(ge,Q)}finally{Pe?Ce():(Xt=!1,cn=null)}}else Xt=!1},Ce;if(typeof Ve=="function")Ce=function(){Ve(ue)};else if(typeof MessageChannel<"u"){var De=new MessageChannel,yt=De.port2;De.port1.onmessage=ue,Ce=function(){yt.postMessage(null)}}else Ce=function(){$(ue,0)};function Le(Q){cn=Q,Xt||(Xt=!0,Ce())}function ct(Q,ge){Yt=$(function(){Q(C.unstable_now())},ge)}function wt(){He(Yt),Yt=-1}var un=Y,wn=null;C.unstable_IdlePriority=ne,C.unstable_ImmediatePriority=oe,C.unstable_LowPriority=j,C.unstable_NormalPriority=G,C.unstable_Profiling=wn,C.unstable_UserBlockingPriority=rt,C.unstable_cancelCallback=Jt,C.unstable_continueExecution=it,C.unstable_forceFrameRate=pe,C.unstable_getCurrentPriorityLevel=xn,C.unstable_getFirstCallbackNode=We,C.unstable_next=st,C.unstable_pauseExecution=Nt,C.unstable_requestPaint=un,C.unstable_runWithPriority=Ne,C.unstable_scheduleCallback=Ie,C.unstable_shouldYield=U,C.unstable_wrapCallback=$t,typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error)})()}(ef)),ef}var gm;function PS(){return gm||(gm=1,Jc.exports=VS()),Jc.exports}var la={},bm;function BS(){if(bm)return la;bm=1;/** + */(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var L=!1,me=5;function Ee(Z,xe){var Je=Z.length;Z.push(xe),d(Z,xe,Je)}function fe(Z){return Z.length===0?null:Z[0]}function Re(Z){if(Z.length===0)return null;var xe=Z[0],Je=Z.pop();return Je!==xe&&(Z[0]=Je,Q(Z,Je,0)),xe}function d(Z,xe,Je){for(var Rt=Je;Rt>0;){var Bt=Rt-1>>>1,Xt=Z[Bt];if(he(Xt,xe)>0)Z[Bt]=xe,Z[Rt]=Xt,Rt=Bt;else return}}function Q(Z,xe,Je){for(var Rt=Je,Bt=Z.length,Xt=Bt>>>1;RtJe&&(!Z||st()));){var Rt=oe.callback;if(typeof Rt=="function"){oe.callback=null,Le=oe.priorityLevel;var Bt=oe.expirationTime<=Je,Xt=Rt(Bt);Je=w.unstable_now(),typeof Xt=="function"?oe.callback=Xt:oe===fe(Me)&&Re(Me),at(Je)}else Re(Me);oe=fe(Me)}if(oe!==null)return!0;var Qt=fe(pe);return Qt!==null&&Lt(ct,Qt.startTime-Je),!1}function Ue(Z,xe){switch(Z){case te:case bt:case I:case H:case K:break;default:Z=I}var Je=Le;Le=Z;try{return xe()}finally{Le=Je}}function cn(Z){var xe;switch(Le){case te:case bt:case I:xe=I;break;default:xe=Le;break}var Je=Le;Le=xe;try{return Z()}finally{Le=Je}}function et(Z){var xe=Le;return function(){var Je=Le;Le=xe;try{return Z.apply(this,arguments)}finally{Le=Je}}}function Ht(Z,xe,Je){var Rt=w.unstable_now(),Bt;if(typeof Je=="object"&&Je!==null){var Xt=Je.delay;typeof Xt=="number"&&Xt>0?Bt=Rt+Xt:Bt=Rt}else Bt=Rt;var Qt;switch(Z){case te:Qt=ot;break;case bt:Qt=_;break;case K:Qt=Vt;break;case H:Qt=De;break;case I:default:Qt=ne;break}var wn=Bt+Qt,Pt={id:nt++,callback:xe,priorityLevel:Z,startTime:Bt,expirationTime:wn,sortIndex:-1};return Bt>Rt?(Pt.sortIndex=Bt,Ee(pe,Pt),fe(Me)===null&&Pt===fe(pe)&&(W?hn():W=!0,Lt(ct,Bt-Rt))):(Pt.sortIndex=wn,Ee(Me,Pt),!le&&!Ke&&(le=!0,Ot(We))),Pt}function gt(){}function Qe(){!le&&!Ke&&(le=!0,Ot(We))}function Gt(){return fe(Me)}function rn(Z){Z.callback=null}function fn(){return Le}var F=!1,G=null,Te=-1,Ce=me,Ze=-1;function st(){var Z=w.unstable_now()-Ze;return!(Z125){console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported");return}Z>0?Ce=Math.floor(1e3/Z):Ce=me}var yt=function(){if(G!==null){var Z=w.unstable_now();Ze=Z;var xe=!0,Je=!0;try{Je=G(xe,Z)}finally{Je?rt():(F=!1,G=null)}}else F=!1},rt;if(typeof Fe=="function")rt=function(){Fe(yt)};else if(typeof MessageChannel<"u"){var tn=new MessageChannel,mn=tn.port2;tn.port1.onmessage=yt,rt=function(){mn.postMessage(null)}}else rt=function(){q(yt,0)};function Ot(Z){G=Z,F||(F=!0,rt())}function Lt(Z,xe){Te=q(function(){Z(w.unstable_now())},xe)}function hn(){Ye(Te),Te=-1}var ia=ht,va=null;w.unstable_IdlePriority=K,w.unstable_ImmediatePriority=te,w.unstable_LowPriority=H,w.unstable_NormalPriority=I,w.unstable_Profiling=va,w.unstable_UserBlockingPriority=bt,w.unstable_cancelCallback=rn,w.unstable_continueExecution=Qe,w.unstable_forceFrameRate=Ge,w.unstable_getCurrentPriorityLevel=fn,w.unstable_getFirstCallbackNode=Gt,w.unstable_next=cn,w.unstable_pauseExecution=gt,w.unstable_requestPaint=ia,w.unstable_runWithPriority=Ue,w.unstable_scheduleCallback=Ht,w.unstable_shouldYield=st,w.unstable_wrapCallback=et,typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error)})()}(ef)),ef}var gm;function B0(){return gm||(gm=1,Jc.exports=V0()),Jc.exports}var ra={},bm;function $0(){if(bm)return ra;bm=1;/** * @license React * react-dom.development.js * @@ -65,15 +65,15 @@ React keys must be passed directly to JSX without using spread: * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */return function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var C=Zc(),k=PS(),me=C.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,Se=!1;function ve(e){Se=e}function Ee(e){if(!Se){for(var t=arguments.length,n=new Array(t>1?t-1:0),a=1;a1?t-1:0),a=1;a2&&(e[0]==="o"||e[0]==="O")&&(e[1]==="n"||e[1]==="N")}function Kt(e,t,n,a){if(n!==null&&n.type===Ce)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":{if(a)return!1;if(n!==null)return!n.acceptsBooleans;var r=e.toLowerCase().slice(0,5);return r!=="data-"&&r!=="aria-"}default:return!1}}function Dn(e,t,n,a){if(t===null||typeof t>"u"||Kt(e,t,n,a))return!0;if(a)return!1;if(n!==null)switch(n.type){case Le:return!t;case ct:return t===!1;case wt:return isNaN(t);case un:return isNaN(t)||t<1}return!1}function It(e){return bt.hasOwnProperty(e)?bt[e]:null}function Vt(e,t,n,a,r,i,o){this.acceptsBooleans=t===yt||t===Le||t===ct,this.attributeName=a,this.attributeNamespace=r,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var bt={},ua=["children","dangerouslySetInnerHTML","defaultValue","defaultChecked","innerHTML","suppressContentEditableWarning","suppressHydrationWarning","style"];ua.forEach(function(e){bt[e]=new Vt(e,Ce,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0],n=e[1];bt[t]=new Vt(t,De,!1,n,null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){bt[e]=new Vt(e,yt,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){bt[e]=new Vt(e,yt,!1,e,null,!1,!1)}),["allowFullScreen","async","autoFocus","autoPlay","controls","default","defer","disabled","disablePictureInPicture","disableRemotePlayback","formNoValidate","hidden","loop","noModule","noValidate","open","playsInline","readOnly","required","reversed","scoped","seamless","itemScope"].forEach(function(e){bt[e]=new Vt(e,Le,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){bt[e]=new Vt(e,Le,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){bt[e]=new Vt(e,ct,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){bt[e]=new Vt(e,un,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){bt[e]=new Vt(e,wt,!1,e.toLowerCase(),null,!1,!1)});var ha=/[\-\:]([a-z])/g,Yn=function(e){return e[1].toUpperCase()};["accent-height","alignment-baseline","arabic-form","baseline-shift","cap-height","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","dominant-baseline","enable-background","fill-opacity","fill-rule","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","horiz-adv-x","horiz-origin-x","image-rendering","letter-spacing","lighting-color","marker-end","marker-mid","marker-start","overline-position","overline-thickness","paint-order","panose-1","pointer-events","rendering-intent","shape-rendering","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-anchor","text-decoration","text-rendering","underline-position","underline-thickness","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","vector-effect","vert-adv-y","vert-origin-x","vert-origin-y","word-spacing","writing-mode","xmlns:xlink","x-height"].forEach(function(e){var t=e.replace(ha,Yn);bt[t]=new Vt(t,De,!1,e,null,!1,!1)}),["xlink:actuate","xlink:arcrole","xlink:role","xlink:show","xlink:title","xlink:type"].forEach(function(e){var t=e.replace(ha,Yn);bt[t]=new Vt(t,De,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ha,Yn);bt[t]=new Vt(t,De,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){bt[e]=new Vt(e,De,!1,e.toLowerCase(),null,!1,!1)});var nr="xlinkHref";bt[nr]=new Vt("xlinkHref",De,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){bt[e]=new Vt(e,De,!1,e.toLowerCase(),null,!0,!0)});var Vr=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i,Pr=!1;function hr(e){!Pr&&Vr.test(e)&&(Pr=!0,d("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.",JSON.stringify(e)))}function Ha(e,t,n,a){if(a.mustUseProperty){var r=a.propertyName;return e[r]}else{an(n,t),a.sanitizeURL&&hr(""+n);var i=a.attributeName,o=null;if(a.type===ct){if(e.hasAttribute(i)){var l=e.getAttribute(i);return l===""?!0:Dn(t,n,a,!1)?l:l===""+n?n:l}}else if(e.hasAttribute(i)){if(Dn(t,n,a,!1))return e.getAttribute(i);if(a.type===Le)return n;o=e.getAttribute(i)}return Dn(t,n,a,!1)?o===null?n:o:o===""+n?n:o}}function Fa(e,t,n,a){{if(!Pt(t))return;if(!e.hasAttribute(t))return n===void 0?void 0:null;var r=e.getAttribute(t);return an(n,t),r===""+n?n:r}}function ya(e,t,n,a){var r=It(t);if(!Qt(t,r,a)){if(Dn(t,n,r,a)&&(n=null),a||r===null){if(Pt(t)){var i=t;n===null?e.removeAttribute(i):(an(n,t),e.setAttribute(i,""+n))}return}var o=r.mustUseProperty;if(o){var l=r.propertyName;if(n===null){var u=r.type;e[l]=u===Le?!1:""}else e[l]=n;return}var f=r.attributeName,v=r.attributeNamespace;if(n===null)e.removeAttribute(f);else{var b=r.type,g;b===Le||b===ct&&n===!0?g="":(an(n,f),g=""+n,r.sanitizeURL&&hr(g.toString())),v?e.setAttributeNS(v,f,g):e.setAttribute(f,g)}}}var Kn=Symbol.for("react.element"),y=Symbol.for("react.portal"),F=Symbol.for("react.fragment"),_=Symbol.for("react.strict_mode"),S=Symbol.for("react.profiler"),w=Symbol.for("react.provider"),c=Symbol.for("react.context"),p=Symbol.for("react.forward_ref"),T=Symbol.for("react.suspense"),E=Symbol.for("react.suspense_list"),P=Symbol.for("react.memo"),ae=Symbol.for("react.lazy"),H=Symbol.for("react.scope"),le=Symbol.for("react.debug_trace_mode"),Qe=Symbol.for("react.offscreen"),_e=Symbol.for("react.legacy_hidden"),ot=Symbol.for("react.cache"),fn=Symbol.for("react.tracing_marker"),_n=Symbol.iterator,en="@@iterator";function Ft(e){if(e===null||typeof e!="object")return null;var t=_n&&e[_n]||e[en];return typeof t=="function"?t:null}var Me=Object.assign,Dt=0,Zn,yr,Va,Br,ga,ba,$r;function gi(){}gi.__reactDisabledLog=!0;function dn(){{if(Dt===0){Zn=console.log,yr=console.info,Va=console.warn,Br=console.error,ga=console.group,ba=console.groupCollapsed,$r=console.groupEnd;var e={configurable:!0,enumerable:!0,value:gi,writable:!0};Object.defineProperties(console,{info:e,log:e,warn:e,error:e,group:e,groupCollapsed:e,groupEnd:e})}Dt++}}function _t(){{if(Dt--,Dt===0){var e={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:Me({},e,{value:Zn}),info:Me({},e,{value:yr}),warn:Me({},e,{value:Va}),error:Me({},e,{value:Br}),group:Me({},e,{value:ga}),groupCollapsed:Me({},e,{value:ba}),groupEnd:Me({},e,{value:$r})})}Dt<0&&d("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var Sa=me.ReactCurrentDispatcher,Ta;function sa(e,t,n){{if(Ta===void 0)try{throw Error()}catch(r){var a=r.stack.trim().match(/\n( *(at )?)/);Ta=a&&a[1]||""}return` -`+Ta+e}}var bi=!1,Yr;{var ao=typeof WeakMap=="function"?WeakMap:Map;Yr=new ao}function Si(e,t){if(!e||bi)return"";{var n=Yr.get(e);if(n!==void 0)return n}var a;bi=!0;var r=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var i;i=Sa.current,Sa.current=null,dn();try{if(t){var o=function(){throw Error()};if(Object.defineProperty(o.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(o,[])}catch(L){a=L}Reflect.construct(e,[],o)}else{try{o.call()}catch(L){a=L}e.call(o.prototype)}}else{try{throw Error()}catch(L){a=L}e()}}catch(L){if(L&&a&&typeof L.stack=="string"){for(var l=L.stack.split(` + */return function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var w=Zc(),L=B0(),me=w.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,Ee=!1;function fe(e){Ee=e}function Re(e){if(!Ee){for(var t=arguments.length,n=new Array(t>1?t-1:0),a=1;a1?t-1:0),a=1;a2&&(e[0]==="o"||e[0]==="O")&&(e[1]==="n"||e[1]==="N")}function Qt(e,t,n,a){if(n!==null&&n.type===rt)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":{if(a)return!1;if(n!==null)return!n.acceptsBooleans;var r=e.toLowerCase().slice(0,5);return r!=="data-"&&r!=="aria-"}default:return!1}}function wn(e,t,n,a){if(t===null||typeof t>"u"||Qt(e,t,n,a))return!0;if(a)return!1;if(n!==null)switch(n.type){case Ot:return!t;case Lt:return t===!1;case hn:return isNaN(t);case ia:return isNaN(t)||t<1}return!1}function Pt(e){return Dt.hasOwnProperty(e)?Dt[e]:null}function Tt(e,t,n,a,r,i,o){this.acceptsBooleans=t===mn||t===Ot||t===Lt,this.attributeName=a,this.attributeNamespace=r,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var Dt={},er=["children","dangerouslySetInnerHTML","defaultValue","defaultChecked","innerHTML","suppressContentEditableWarning","suppressHydrationWarning","style"];er.forEach(function(e){Dt[e]=new Tt(e,rt,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0],n=e[1];Dt[t]=new Tt(t,tn,!1,n,null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){Dt[e]=new Tt(e,mn,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Dt[e]=new Tt(e,mn,!1,e,null,!1,!1)}),["allowFullScreen","async","autoFocus","autoPlay","controls","default","defer","disabled","disablePictureInPicture","disableRemotePlayback","formNoValidate","hidden","loop","noModule","noValidate","open","playsInline","readOnly","required","reversed","scoped","seamless","itemScope"].forEach(function(e){Dt[e]=new Tt(e,Ot,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){Dt[e]=new Tt(e,Ot,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){Dt[e]=new Tt(e,Lt,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){Dt[e]=new Tt(e,ia,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){Dt[e]=new Tt(e,hn,!1,e.toLowerCase(),null,!1,!1)});var $n=/[\-\:]([a-z])/g,pa=function(e){return e[1].toUpperCase()};["accent-height","alignment-baseline","arabic-form","baseline-shift","cap-height","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","dominant-baseline","enable-background","fill-opacity","fill-rule","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","horiz-adv-x","horiz-origin-x","image-rendering","letter-spacing","lighting-color","marker-end","marker-mid","marker-start","overline-position","overline-thickness","paint-order","panose-1","pointer-events","rendering-intent","shape-rendering","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-anchor","text-decoration","text-rendering","underline-position","underline-thickness","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","vector-effect","vert-adv-y","vert-origin-x","vert-origin-y","word-spacing","writing-mode","xmlns:xlink","x-height"].forEach(function(e){var t=e.replace($n,pa);Dt[t]=new Tt(t,tn,!1,e,null,!1,!1)}),["xlink:actuate","xlink:arcrole","xlink:role","xlink:show","xlink:title","xlink:type"].forEach(function(e){var t=e.replace($n,pa);Dt[t]=new Tt(t,tn,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace($n,pa);Dt[t]=new Tt(t,tn,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){Dt[e]=new Tt(e,tn,!1,e.toLowerCase(),null,!1,!1)});var tr="xlinkHref";Dt[tr]=new Tt("xlinkHref",tn,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){Dt[e]=new Tt(e,tn,!1,e.toLowerCase(),null,!0,!0)});var Fr=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i,Vr=!1;function wa(e){!Vr&&Fr.test(e)&&(Vr=!0,d("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.",JSON.stringify(e)))}function ja(e,t,n,a){if(a.mustUseProperty){var r=a.propertyName;return e[r]}else{Ce(n,t),a.sanitizeURL&&wa(""+n);var i=a.attributeName,o=null;if(a.type===Lt){if(e.hasAttribute(i)){var l=e.getAttribute(i);return l===""?!0:wn(t,n,a,!1)?l:l===""+n?n:l}}else if(e.hasAttribute(i)){if(wn(t,n,a,!1))return e.getAttribute(i);if(a.type===Ot)return n;o=e.getAttribute(i)}return wn(t,n,a,!1)?o===null?n:o:o===""+n?n:o}}function Br(e,t,n,a){{if(!Bt(t))return;if(!e.hasAttribute(t))return n===void 0?void 0:null;var r=e.getAttribute(t);return Ce(n,t),r===""+n?n:r}}function ma(e,t,n,a){var r=Pt(t);if(!Xt(t,r,a)){if(wn(t,n,r,a)&&(n=null),a||r===null){if(Bt(t)){var i=t;n===null?e.removeAttribute(i):(Ce(n,t),e.setAttribute(i,""+n))}return}var o=r.mustUseProperty;if(o){var l=r.propertyName;if(n===null){var u=r.type;e[l]=u===Ot?!1:""}else e[l]=n;return}var f=r.attributeName,v=r.attributeNamespace;if(n===null)e.removeAttribute(f);else{var b=r.type,g;b===Ot||b===Lt&&n===!0?g="":(Ce(n,f),g=""+n,r.sanitizeURL&&wa(g.toString())),v?e.setAttributeNS(v,f,g):e.setAttribute(f,g)}}}var h=Symbol.for("react.element"),B=Symbol.for("react.portal"),D=Symbol.for("react.fragment"),T=Symbol.for("react.strict_mode"),x=Symbol.for("react.profiler"),O=Symbol.for("react.provider"),c=Symbol.for("react.context"),y=Symbol.for("react.forward_ref"),S=Symbol.for("react.suspense"),A=Symbol.for("react.suspense_list"),ve=Symbol.for("react.memo"),z=Symbol.for("react.lazy"),ue=Symbol.for("react.scope"),ye=Symbol.for("react.debug_trace_mode"),ze=Symbol.for("react.offscreen"),it=Symbol.for("react.legacy_hidden"),Ft=Symbol.for("react.cache"),nn=Symbol.for("react.tracing_marker"),Zt=Symbol.iterator,Pn="@@iterator";function ft(e){if(e===null||typeof e!="object")return null;var t=Zt&&e[Zt]||e[Pn];return typeof t=="function"?t:null}var be=Object.assign,Rn=0,Ua,il,Ha,ha,ya,$r,yi;function un(){}un.__reactDisabledLog=!0;function _t(){{if(Rn===0){Ua=console.log,il=console.info,Ha=console.warn,ha=console.error,ya=console.group,$r=console.groupCollapsed,yi=console.groupEnd;var e={configurable:!0,enumerable:!0,value:un,writable:!0};Object.defineProperties(console,{info:e,log:e,warn:e,error:e,group:e,groupCollapsed:e,groupEnd:e})}Rn++}}function za(){{if(Rn--,Rn===0){var e={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:be({},e,{value:Ua}),info:be({},e,{value:il}),warn:be({},e,{value:Ha}),error:be({},e,{value:ha}),group:be({},e,{value:ya}),groupCollapsed:be({},e,{value:$r}),groupEnd:be({},e,{value:yi})})}Rn<0&&d("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var oa=me.ReactCurrentDispatcher,hr;function nr(e,t,n){{if(hr===void 0)try{throw Error()}catch(r){var a=r.stack.trim().match(/\n( *(at )?)/);hr=a&&a[1]||""}return` +`+hr+e}}var gi=!1,Pr;{var no=typeof WeakMap=="function"?WeakMap:Map;Pr=new no}function bi(e,t){if(!e||gi)return"";{var n=Pr.get(e);if(n!==void 0)return n}var a;gi=!0;var r=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var i;i=oa.current,oa.current=null,_t();try{if(t){var o=function(){throw Error()};if(Object.defineProperty(o.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(o,[])}catch(M){a=M}Reflect.construct(e,[],o)}else{try{o.call()}catch(M){a=M}e.call(o.prototype)}}else{try{throw Error()}catch(M){a=M}e()}}catch(M){if(M&&a&&typeof M.stack=="string"){for(var l=M.stack.split(` `),u=a.stack.split(` `),f=l.length-1,v=u.length-1;f>=1&&v>=0&&l[f]!==u[v];)v--;for(;f>=1&&v>=0;f--,v--)if(l[f]!==u[v]){if(f!==1||v!==1)do if(f--,v--,v<0||l[f]!==u[v]){var b=` -`+l[f].replace(" at new "," at ");return e.displayName&&b.includes("")&&(b=b.replace("",e.displayName)),typeof e=="function"&&Yr.set(e,b),b}while(f>=1&&v>=0);break}}}finally{bi=!1,Sa.current=i,_t(),Error.prepareStackTrace=r}var g=e?e.displayName||e.name:"",D=g?sa(g):"";return typeof e=="function"&&Yr.set(e,D),D}function ro(e,t,n){return Si(e,!0)}function ol(e,t,n){return Si(e,!1)}function Yu(e){var t=e.prototype;return!!(t&&t.isReactComponent)}function ll(e,t,n){if(e==null)return"";if(typeof e=="function")return Si(e,Yu(e));if(typeof e=="string")return sa(e);switch(e){case T:return sa("Suspense");case E:return sa("SuspenseList")}if(typeof e=="object")switch(e.$$typeof){case p:return ol(e.render);case P:return ll(e.type,t,n);case ae:{var a=e,r=a._payload,i=a._init;try{return ll(i(r),t,n)}catch{}}}return""}function af(e){switch(e._debugOwner&&e._debugOwner.type,e._debugSource,e.tag){case ne:return sa(e.type);case Ot:return sa("Lazy");case Re:return sa("Suspense");case ee:return sa("SuspenseList");case ye:case rt:case B:return ol(e.type);case xe:return ol(e.type.render);case oe:return ro(e.type);default:return""}}function Ti(e){try{var t="",n=e;do t+=af(n),n=n.return;while(n);return t}catch(a){return` +`+l[f].replace(" at new "," at ");return e.displayName&&b.includes("")&&(b=b.replace("",e.displayName)),typeof e=="function"&&Pr.set(e,b),b}while(f>=1&&v>=0);break}}}finally{gi=!1,oa.current=i,za(),Error.prepareStackTrace=r}var g=e?e.displayName||e.name:"",R=g?nr(g):"";return typeof e=="function"&&Pr.set(e,R),R}function ao(e,t,n){return bi(e,!0)}function ol(e,t,n){return bi(e,!1)}function Yu(e){var t=e.prototype;return!!(t&&t.isReactComponent)}function ll(e,t,n){if(e==null)return"";if(typeof e=="function")return bi(e,Yu(e));if(typeof e=="string")return nr(e);switch(e){case S:return nr("Suspense");case A:return nr("SuspenseList")}if(typeof e=="object")switch(e.$$typeof){case y:return ol(e.render);case ve:return ll(e.type,t,n);case z:{var a=e,r=a._payload,i=a._init;try{return ll(i(r),t,n)}catch{}}}return""}function af(e){switch(e._debugOwner&&e._debugOwner.type,e._debugSource,e.tag){case K:return nr(e.type);case Vt:return nr("Lazy");case _:return nr("Suspense");case nt:return nr("SuspenseList");case he:case bt:case De:return ol(e.type);case _e:return ol(e.type.render);case te:return ao(e.type);default:return""}}function Si(e){try{var t="",n=e;do t+=af(n),n=n.return;while(n);return t}catch(a){return` Error generating stack: `+a.message+` -`+a.stack}}function Iu(e,t,n){var a=e.displayName;if(a)return a;var r=t.displayName||t.name||"";return r!==""?n+"("+r+")":n}function ul(e){return e.displayName||"Context"}function Et(e){if(e==null)return null;if(typeof e.tag=="number"&&d("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case F:return"Fragment";case y:return"Portal";case S:return"Profiler";case _:return"StrictMode";case T:return"Suspense";case E:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case c:var t=e;return ul(t)+".Consumer";case w:var n=e;return ul(n._context)+".Provider";case p:return Iu(e,e.render,"ForwardRef");case P:var a=e.displayName||null;return a!==null?a:Et(e.type)||"Memo";case ae:{var r=e,i=r._payload,o=r._init;try{return Et(o(i))}catch{return null}}}return null}function rf(e,t,n){var a=t.displayName||t.name||"";return e.displayName||(a!==""?n+"("+a+")":n)}function gr(e){return e.displayName||"Context"}function Ke(e){var t=e.tag,n=e.type;switch(t){case Be:return"Cache";case Rt:var a=n;return gr(a)+".Consumer";case Xe:var r=n;return gr(r._context)+".Provider";case Fe:return"DehydratedFragment";case xe:return rf(n,n.render,"ForwardRef");case kt:return"Fragment";case ne:return n;case j:return"Portal";case G:return"Root";case Ue:return"Text";case Ot:return Et(n);case xt:return n===_?"StrictMode":"Mode";case X:return"Offscreen";case mt:return"Profiler";case Je:return"Scope";case Re:return"Suspense";case ee:return"SuspenseList";case I:return"TracingMarker";case oe:case ye:case at:case rt:case et:case B:if(typeof n=="function")return n.displayName||n.name||null;if(typeof n=="string")return n;break}return null}var sl=me.ReactDebugCurrentFrame,Jn=null,Ei=!1;function Ir(){{if(Jn===null)return null;var e=Jn._debugOwner;if(e!==null&&typeof e<"u")return Ke(e)}return null}function of(){return Jn===null?"":Ti(Jn)}function Mn(){sl.getCurrentStack=null,Jn=null,Ei=!1}function tn(e){sl.getCurrentStack=e===null?null:of,Jn=e,Ei=!1}function Wu(){return Jn}function Ma(e){Ei=e}function ea(e){return""+e}function Pa(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return ue(e),e;default:return""}}var lf={button:!0,checkbox:!0,image:!0,hidden:!0,radio:!0,reset:!0,submit:!0};function cl(e,t){lf[t.type]||t.onChange||t.onInput||t.readOnly||t.disabled||t.value==null||d("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."),t.onChange||t.readOnly||t.disabled||t.checked==null||d("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.")}function qu(e){var t=e.type,n=e.nodeName;return n&&n.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function fl(e){return e._valueTracker}function io(e){e._valueTracker=null}function uf(e){var t="";return e&&(qu(e)?t=e.checked?"true":"false":t=e.value),t}function Wr(e){var t=qu(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t);ue(e[t]);var a=""+e[t];if(!(e.hasOwnProperty(t)||typeof n>"u"||typeof n.get!="function"||typeof n.set!="function")){var r=n.get,i=n.set;Object.defineProperty(e,t,{configurable:!0,get:function(){return r.call(this)},set:function(l){ue(l),a=""+l,i.call(this,l)}}),Object.defineProperty(e,t,{enumerable:n.enumerable});var o={getValue:function(){return a},setValue:function(l){ue(l),a=""+l},stopTracking:function(){io(e),delete e[t]}};return o}}function Ci(e){fl(e)||(e._valueTracker=Wr(e))}function dl(e){if(!e)return!1;var t=fl(e);if(!t)return!0;var n=t.getValue(),a=uf(e);return a!==n?(t.setValue(a),!0):!1}function br(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var oo=!1,lo=!1,uo=!1,Gu=!1;function Xu(e){var t=e.type==="checkbox"||e.type==="radio";return t?e.checked!=null:e.value!=null}function vl(e,t){var n=e,a=t.checked,r=Me({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:a??n._wrapperState.initialChecked});return r}function Qu(e,t){cl("input",t),t.checked!==void 0&&t.defaultChecked!==void 0&&!lo&&(d("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",Ir()||"A component",t.type),lo=!0),t.value!==void 0&&t.defaultValue!==void 0&&!oo&&(d("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",Ir()||"A component",t.type),oo=!0);var n=e,a=t.defaultValue==null?"":t.defaultValue;n._wrapperState={initialChecked:t.checked!=null?t.checked:t.defaultChecked,initialValue:Pa(t.value!=null?t.value:a),controlled:Xu(t)}}function s(e,t){var n=e,a=t.checked;a!=null&&ya(n,"checked",a,!1)}function h(e,t){var n=e;{var a=Xu(t);!n._wrapperState.controlled&&a&&!Gu&&(d("A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),Gu=!0),n._wrapperState.controlled&&!a&&!uo&&(d("A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),uo=!0)}s(e,t);var r=Pa(t.value),i=t.type;if(r!=null)i==="number"?(r===0&&n.value===""||n.value!=r)&&(n.value=ea(r)):n.value!==ea(r)&&(n.value=ea(r));else if(i==="submit"||i==="reset"){n.removeAttribute("value");return}t.hasOwnProperty("value")?Oe(n,t.type,r):t.hasOwnProperty("defaultValue")&&Oe(n,t.type,Pa(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(n.defaultChecked=!!t.defaultChecked)}function M(e,t,n){var a=e;if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type,i=r==="submit"||r==="reset";if(i&&(t.value===void 0||t.value===null))return;var o=ea(a._wrapperState.initialValue);n||o!==a.value&&(a.value=o),a.defaultValue=o}var l=a.name;l!==""&&(a.name=""),a.defaultChecked=!a.defaultChecked,a.defaultChecked=!!a._wrapperState.initialChecked,l!==""&&(a.name=l)}function O(e,t){var n=e;h(n,t),K(n,t)}function K(e,t){var n=t.name;if(t.type==="radio"&&n!=null){for(var a=e;a.parentNode;)a=a.parentNode;an(n,"name");for(var r=a.querySelectorAll("input[name="+JSON.stringify(""+n)+'][type="radio"]'),i=0;i.")))}):t.dangerouslySetInnerHTML!=null&&(ft||(ft=!0,d("Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.")))),t.selected!=null&&!be&&(d("Use the `defaultValue` or `value` props on instead of setting `selected` on