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

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

View File

@ -41,7 +41,7 @@ const App = () => {
videoInitialized, videoInitialized,
setVideoInitialized, setVideoInitialized,
isPlayingSegments, isPlayingSegments,
handlePlaySegments, handlePlaySegments
} = useVideoTrimmer(); } = useVideoTrimmer();
// Function to play from the beginning // Function to play from the beginning
@ -69,31 +69,31 @@ const App = () => {
const handlePlay = () => { const handlePlay = () => {
if (!videoRef.current) return; if (!videoRef.current) return;
const video = videoRef.current; const video = videoRef.current;
// If already playing, just pause the video // If already playing, just pause the video
if (isPlaying) { if (isPlaying) {
video.pause(); video.pause();
setIsPlaying(false); setIsPlaying(false);
return; return;
} }
const currentPosition = Number(video.currentTime.toFixed(6)); // Fix to microsecond precision const currentPosition = Number(video.currentTime.toFixed(6)); // Fix to microsecond precision
// Find the next stopping point based on current position // Find the next stopping point based on current position
let stopTime = duration; let stopTime = duration;
let currentSegment = null; let currentSegment = null;
let nextSegment = null; let nextSegment = null;
// Sort segments by start time to ensure correct order // Sort segments by start time to ensure correct order
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime); const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
// First, check if we're inside a segment or exactly at its start/end // First, check if we're inside a segment or exactly at its start/end
currentSegment = sortedSegments.find(seg => { currentSegment = sortedSegments.find((seg) => {
const segStartTime = Number(seg.startTime.toFixed(6)); const segStartTime = Number(seg.startTime.toFixed(6));
const segEndTime = Number(seg.endTime.toFixed(6)); const segEndTime = Number(seg.endTime.toFixed(6));
// Check if we're inside the segment // Check if we're inside the segment
if (currentPosition > segStartTime && currentPosition < segEndTime) { if (currentPosition > segStartTime && currentPosition < segEndTime) {
return true; return true;
@ -109,15 +109,15 @@ const App = () => {
} }
return false; return false;
}); });
// If we're not in a segment, find the next segment // If we're not in a segment, find the next segment
if (!currentSegment) { if (!currentSegment) {
nextSegment = sortedSegments.find(seg => { nextSegment = sortedSegments.find((seg) => {
const segStartTime = Number(seg.startTime.toFixed(6)); const segStartTime = Number(seg.startTime.toFixed(6));
return segStartTime > currentPosition; return segStartTime > currentPosition;
}); });
} }
// Determine where to stop based on position // Determine where to stop based on position
if (currentSegment) { if (currentSegment) {
// If we're in a segment, stop at its end // If we're in a segment, stop at its end
@ -126,113 +126,123 @@ const App = () => {
// If we're in a cutaway and there's a next segment, stop at its start // If we're in a cutaway and there's a next segment, stop at its start
stopTime = Number(nextSegment.startTime.toFixed(6)); stopTime = Number(nextSegment.startTime.toFixed(6));
} }
// Create a boundary checker function with high precision // Create a boundary checker function with high precision
const checkBoundary = () => { const checkBoundary = () => {
if (!video) return; if (!video) return;
const currentPosition = Number(video.currentTime.toFixed(6)); const currentPosition = Number(video.currentTime.toFixed(6));
const timeLeft = Number((stopTime - currentPosition).toFixed(6)); const timeLeft = Number((stopTime - currentPosition).toFixed(6));
// If we've reached or passed the boundary // If we've reached or passed the boundary
if (timeLeft <= 0 || currentPosition >= stopTime) { if (timeLeft <= 0 || currentPosition >= stopTime) {
// First pause playback // First pause playback
video.pause(); video.pause();
// Force exact position with multiple verification attempts // Force exact position with multiple verification attempts
const setExactPosition = () => { const setExactPosition = () => {
if (!video) return; if (!video) return;
// Set to exact boundary time // Set to exact boundary time
video.currentTime = stopTime; video.currentTime = stopTime;
handleMobileSafeSeek(stopTime); handleMobileSafeSeek(stopTime);
const actualPosition = Number(video.currentTime.toFixed(6)); const actualPosition = Number(video.currentTime.toFixed(6));
const difference = Number(Math.abs(actualPosition - stopTime).toFixed(6)); const difference = Number(Math.abs(actualPosition - stopTime).toFixed(6));
logger.debug("Position verification:", { logger.debug("Position verification:", {
target: formatDetailedTime(stopTime), target: formatDetailedTime(stopTime),
actual: formatDetailedTime(actualPosition), actual: formatDetailedTime(actualPosition),
difference: difference difference: difference
}); });
// If we're not exactly at the target position, try one more time // If we're not exactly at the target position, try one more time
if (difference > 0) { if (difference > 0) {
video.currentTime = stopTime; video.currentTime = stopTime;
handleMobileSafeSeek(stopTime); handleMobileSafeSeek(stopTime);
} }
}; };
// Multiple attempts to ensure precision, with increasing delays // Multiple attempts to ensure precision, with increasing delays
setExactPosition(); setExactPosition();
setTimeout(setExactPosition, 5); // Quick first retry setTimeout(setExactPosition, 5); // Quick first retry
setTimeout(setExactPosition, 10); // Second retry setTimeout(setExactPosition, 10); // Second retry
setTimeout(setExactPosition, 20); // Third retry if needed setTimeout(setExactPosition, 20); // Third retry if needed
setTimeout(setExactPosition, 50); // Final verification setTimeout(setExactPosition, 50); // Final verification
// Remove our boundary checker // Remove our boundary checker
video.removeEventListener('timeupdate', checkBoundary); video.removeEventListener("timeupdate", checkBoundary);
setIsPlaying(false); setIsPlaying(false);
// Log the final position for debugging // Log the final position for debugging
logger.debug("Stopped at position:", { logger.debug("Stopped at position:", {
target: formatDetailedTime(stopTime), target: formatDetailedTime(stopTime),
actual: formatDetailedTime(video.currentTime), actual: formatDetailedTime(video.currentTime),
type: currentSegment ? "segment end" : (nextSegment ? "next segment start" : "end of video"), type: currentSegment
segment: currentSegment ? { ? "segment end"
id: currentSegment.id, : nextSegment
start: formatDetailedTime(currentSegment.startTime), ? "next segment start"
end: formatDetailedTime(currentSegment.endTime) : "end of video",
} : null, segment: currentSegment
nextSegment: nextSegment ? { ? {
id: nextSegment.id, id: currentSegment.id,
start: formatDetailedTime(nextSegment.startTime), start: formatDetailedTime(currentSegment.startTime),
end: formatDetailedTime(nextSegment.endTime) end: formatDetailedTime(currentSegment.endTime)
} : null }
: null,
nextSegment: nextSegment
? {
id: nextSegment.id,
start: formatDetailedTime(nextSegment.startTime),
end: formatDetailedTime(nextSegment.endTime)
}
: null
}); });
return; return;
} }
}; };
// Start our boundary checker // Start our boundary checker
video.addEventListener('timeupdate', checkBoundary); video.addEventListener("timeupdate", checkBoundary);
// Start playing // Start playing
video.play() video
.play()
.then(() => { .then(() => {
setIsPlaying(true); setIsPlaying(true);
setVideoInitialized(true); setVideoInitialized(true);
logger.debug("Playback started:", { logger.debug("Playback started:", {
from: formatDetailedTime(currentPosition), from: formatDetailedTime(currentPosition),
to: formatDetailedTime(stopTime), to: formatDetailedTime(stopTime),
currentSegment: currentSegment ? { currentSegment: currentSegment
id: currentSegment.id, ? {
start: formatDetailedTime(currentSegment.startTime), id: currentSegment.id,
end: formatDetailedTime(currentSegment.endTime) start: formatDetailedTime(currentSegment.startTime),
} : 'None', end: formatDetailedTime(currentSegment.endTime)
nextSegment: nextSegment ? { }
id: nextSegment.id, : "None",
start: formatDetailedTime(nextSegment.startTime), nextSegment: nextSegment
end: formatDetailedTime(nextSegment.endTime) ? {
} : 'None' id: nextSegment.id,
start: formatDetailedTime(nextSegment.startTime),
end: formatDetailedTime(nextSegment.endTime)
}
: "None"
}); });
}) })
.catch(err => { .catch((err) => {
console.error("Error playing video:", err); console.error("Error playing video:", err);
}); });
}; };
return ( return (
<div className="bg-background min-h-screen"> <div className="bg-background min-h-screen">
<MobilePlayPrompt <MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />
videoRef={videoRef}
onPlay={handlePlay}
/>
<div className="container mx-auto px-4 py-6 max-w-6xl"> <div className="container mx-auto px-4 py-6 max-w-6xl">
{/* Video Player */} {/* Video Player */}
<VideoPlayer <VideoPlayer
videoRef={videoRef} videoRef={videoRef}
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
@ -244,7 +254,7 @@ const App = () => {
/> />
{/* Editing Tools */} {/* Editing Tools */}
<EditingTools <EditingTools
onSplit={handleSplit} onSplit={handleSplit}
onReset={handleReset} onReset={handleReset}
onUndo={handleUndo} onUndo={handleUndo}
@ -258,7 +268,7 @@ const App = () => {
/> />
{/* Timeline Controls */} {/* Timeline Controls */}
<TimelineControls <TimelineControls
currentTime={currentTime} currentTime={currentTime}
duration={duration} duration={duration}
thumbnails={thumbnails} thumbnails={thumbnails}

View File

@ -1,5 +1,5 @@
import { formatTime, formatLongTime } from "@/lib/timeUtils"; import { formatTime, formatLongTime } from "@/lib/timeUtils";
import '../styles/ClipSegments.css'; import "../styles/ClipSegments.css";
export interface Segment { export interface Segment {
id: number; id: number;
@ -16,41 +16,36 @@ interface ClipSegmentsProps {
const ClipSegments = ({ segments }: ClipSegmentsProps) => { const ClipSegments = ({ segments }: ClipSegmentsProps) => {
// Sort segments by startTime // Sort segments by startTime
const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime); const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime);
// Handle delete segment click // Handle delete segment click
const handleDeleteSegment = (segmentId: number) => { const handleDeleteSegment = (segmentId: number) => {
// Create and dispatch the delete event // Create and dispatch the delete event
const deleteEvent = new CustomEvent('delete-segment', { const deleteEvent = new CustomEvent("delete-segment", {
detail: { segmentId } detail: { segmentId }
}); });
document.dispatchEvent(deleteEvent); document.dispatchEvent(deleteEvent);
}; };
// Generate the same color background for a segment as shown in the timeline // Generate the same color background for a segment as shown in the timeline
const getSegmentColorClass = (index: number) => { 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 // This matches the CSS nth-child selectors in the timeline
return `segment-default-color segment-color-${(index % 8) + 1}`; return `segment-default-color segment-color-${(index % 8) + 1}`;
}; };
return ( return (
<div className="clip-segments-container"> <div className="clip-segments-container">
<h3 className="clip-segments-title">Clip Segments</h3> <h3 className="clip-segments-title">Clip Segments</h3>
{sortedSegments.map((segment, index) => ( {sortedSegments.map((segment, index) => (
<div <div key={segment.id} className={`segment-item ${getSegmentColorClass(index)}`}>
key={segment.id}
className={`segment-item ${getSegmentColorClass(index)}`}
>
<div className="segment-content"> <div className="segment-content">
<div <div
className="segment-thumbnail" className="segment-thumbnail"
style={{ backgroundImage: `url(${segment.thumbnail})` }} style={{ backgroundImage: `url(${segment.thumbnail})` }}
></div> ></div>
<div className="segment-info"> <div className="segment-info">
<div className="segment-title"> <div className="segment-title">Segment {index + 1}</div>
Segment {index + 1}
</div>
<div className="segment-time"> <div className="segment-time">
{formatTime(segment.startTime)} - {formatTime(segment.endTime)} {formatTime(segment.startTime)} - {formatTime(segment.endTime)}
</div> </div>
@ -60,20 +55,24 @@ const ClipSegments = ({ segments }: ClipSegmentsProps) => {
</div> </div>
</div> </div>
<div className="segment-actions"> <div className="segment-actions">
<button <button
className="delete-button" className="delete-button"
aria-label="Delete Segment" aria-label="Delete Segment"
data-tooltip="Delete this segment" data-tooltip="Delete this segment"
onClick={() => handleDeleteSegment(segment.id)} onClick={() => handleDeleteSegment(segment.id)}
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
clipRule="evenodd"
/>
</svg> </svg>
</button> </button>
</div> </div>
</div> </div>
))} ))}
{sortedSegments.length === 0 && ( {sortedSegments.length === 0 && (
<div className="empty-message"> <div className="empty-message">
No segments created yet. Use the split button to create segments. No segments created yet. Use the split button to create segments.

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import '../styles/IOSPlayPrompt.css'; import "../styles/IOSPlayPrompt.css";
interface MobilePlayPromptProps { interface MobilePlayPromptProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
@ -13,7 +13,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
useEffect(() => { useEffect(() => {
const checkIsMobile = () => { const checkIsMobile = () => {
// More comprehensive check for mobile/tablet devices // More comprehensive check for mobile/tablet devices
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(navigator.userAgent); return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(
navigator.userAgent
);
}; };
// Always show for mobile devices on each visit // Always show for mobile devices on each visit
@ -31,9 +33,9 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
setIsVisible(false); setIsVisible(false);
}; };
video.addEventListener('play', handlePlay); video.addEventListener("play", handlePlay);
return () => { return () => {
video.removeEventListener('play', handlePlay); video.removeEventListener("play", handlePlay);
}; };
}, [videoRef]); }, [videoRef]);
@ -62,11 +64,8 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
<li>Then you'll be able to use all timeline controls</li> <li>Then you'll be able to use all timeline controls</li>
</ol> </ol>
</div> */} </div> */}
<button <button className="mobile-play-button" onClick={handlePlayClick}>
className="mobile-play-button"
onClick={handlePlayClick}
>
Click to start editing... Click to start editing...
</button> </button>
</div> </div>
@ -74,4 +73,4 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
); );
}; };
export default MobilePlayPrompt; export default MobilePlayPrompt;

View File

@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { formatTime } from "@/lib/timeUtils"; import { formatTime } from "@/lib/timeUtils";
import '../styles/IOSVideoPlayer.css'; import "../styles/IOSVideoPlayer.css";
interface IOSVideoPlayerProps { interface IOSVideoPlayerProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
@ -8,14 +8,10 @@ interface IOSVideoPlayerProps {
duration: number; duration: number;
} }
const IOSVideoPlayer = ({ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
videoRef,
currentTime,
duration,
}: IOSVideoPlayerProps) => {
const [videoUrl, setVideoUrl] = useState<string>(""); const [videoUrl, setVideoUrl] = useState<string>("");
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null); const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
// Refs for hold-to-continue functionality // Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null); const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
const decrementIntervalRef = useRef<NodeJS.Timeout | null>(null); const decrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@ -27,11 +23,11 @@ const IOSVideoPlayer = ({
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current); if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
}; };
}, []); }, []);
// Get the video source URL from the main player // Get the video source URL from the main player
useEffect(() => { useEffect(() => {
if (videoRef.current && videoRef.current.querySelector('source')) { if (videoRef.current && videoRef.current.querySelector("source")) {
const source = videoRef.current.querySelector('source') as HTMLSourceElement; const source = videoRef.current.querySelector("source") as HTMLSourceElement;
if (source && source.src) { if (source && source.src) {
setVideoUrl(source.src); setVideoUrl(source.src);
} }
@ -61,13 +57,13 @@ const IOSVideoPlayer = ({
const startIncrement = (e: React.MouseEvent | React.TouchEvent) => { const startIncrement = (e: React.MouseEvent | React.TouchEvent) => {
// Prevent default to avoid text selection // Prevent default to avoid text selection
e.preventDefault(); e.preventDefault();
if (!iosVideoRef) return; if (!iosVideoRef) return;
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current); if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
// First immediate adjustment // First immediate adjustment
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05); iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
// Setup continuous adjustment // Setup continuous adjustment
incrementIntervalRef.current = setInterval(() => { incrementIntervalRef.current = setInterval(() => {
if (iosVideoRef) { if (iosVideoRef) {
@ -88,13 +84,13 @@ const IOSVideoPlayer = ({
const startDecrement = (e: React.MouseEvent | React.TouchEvent) => { const startDecrement = (e: React.MouseEvent | React.TouchEvent) => {
// Prevent default to avoid text selection // Prevent default to avoid text selection
e.preventDefault(); e.preventDefault();
if (!iosVideoRef) return; if (!iosVideoRef) return;
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current); if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
// First immediate adjustment // First immediate adjustment
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05); iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
// Setup continuous adjustment // Setup continuous adjustment
decrementIntervalRef.current = setInterval(() => { decrementIntervalRef.current = setInterval(() => {
if (iosVideoRef) { if (iosVideoRef) {
@ -115,12 +111,14 @@ const IOSVideoPlayer = ({
<div className="ios-video-player-container"> <div className="ios-video-player-container">
{/* Current Time / Duration Display */} {/* Current Time / Duration Display */}
<div className="ios-time-display mb-2"> <div className="ios-time-display mb-2">
<span className="text-sm">{formatTime(currentTime)} / {formatTime(duration)}</span> <span className="text-sm">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
</div> </div>
{/* iOS-optimized Video Element with Native Controls */} {/* iOS-optimized Video Element with Native Controls */}
<video <video
ref={ref => setIosVideoRef(ref)} ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md" className="w-full rounded-md"
src={videoUrl} src={videoUrl}
controls controls
@ -133,26 +131,26 @@ const IOSVideoPlayer = ({
<source src={videoUrl} type="video/mp4" /> <source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>
</video> </video>
{/* iOS Video Skip Controls */} {/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4"> <div className="ios-skip-controls mt-3 flex justify-center gap-4">
<button <button
onClick={jumpBackward15} onClick={jumpBackward15}
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md" className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
> >
-15s -15s
</button> </button>
<button <button
onClick={jumpForward15} onClick={jumpForward15}
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md" className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
> >
+15s +15s
</button> </button>
</div> </div>
{/* iOS Fine Control Buttons */} {/* iOS Fine Control Buttons */}
<div className="ios-fine-controls mt-2 flex justify-center gap-4"> <div className="ios-fine-controls mt-2 flex justify-center gap-4">
<button <button
onMouseDown={startDecrement} onMouseDown={startDecrement}
onTouchStart={startDecrement} onTouchStart={startDecrement}
onMouseUp={stopDecrement} onMouseUp={stopDecrement}
@ -163,7 +161,7 @@ const IOSVideoPlayer = ({
> >
-50ms -50ms
</button> </button>
<button <button
onMouseDown={startIncrement} onMouseDown={startIncrement}
onTouchStart={startIncrement} onTouchStart={startIncrement}
onMouseUp={stopIncrement} onMouseUp={stopIncrement}
@ -175,7 +173,7 @@ const IOSVideoPlayer = ({
+50ms +50ms
</button> </button>
</div> </div>
<div className="ios-note mt-2 text-xs text-gray-500"> <div className="ios-note mt-2 text-xs text-gray-500">
<p>This player uses native iOS controls for better compatibility with iOS devices.</p> <p>This player uses native iOS controls for better compatibility with iOS devices.</p>
</div> </div>
@ -183,4 +181,4 @@ const IOSVideoPlayer = ({
); );
}; };
export default IOSVideoPlayer; export default IOSVideoPlayer;

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from "react";
import '../styles/Modal.css'; import "../styles/Modal.css";
interface ModalProps { interface ModalProps {
isOpen: boolean; isOpen: boolean;
@ -9,36 +9,30 @@ interface ModalProps {
actions?: React.ReactNode; actions?: React.ReactNode;
} }
const Modal: React.FC<ModalProps> = ({ const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions }) => {
isOpen,
onClose,
title,
children,
actions
}) => {
// Close modal when Escape key is pressed // Close modal when Escape key is pressed
useEffect(() => { useEffect(() => {
const handleEscapeKey = (event: KeyboardEvent) => { const handleEscapeKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpen) { if (event.key === "Escape" && isOpen) {
onClose(); onClose();
} }
}; };
document.addEventListener('keydown', handleEscapeKey); document.addEventListener("keydown", handleEscapeKey);
// Disable body scrolling when modal is open // Disable body scrolling when modal is open
if (isOpen) { if (isOpen) {
document.body.style.overflow = 'hidden'; document.body.style.overflow = "hidden";
} }
return () => { return () => {
document.removeEventListener('keydown', handleEscapeKey); document.removeEventListener("keydown", handleEscapeKey);
document.body.style.overflow = ''; document.body.style.overflow = "";
}; };
}, [isOpen, onClose]); }, [isOpen, onClose]);
if (!isOpen) return null; if (!isOpen) return null;
// Handle click outside the modal content to close it // Handle click outside the modal content to close it
const handleClickOutside = (event: React.MouseEvent) => { const handleClickOutside = (event: React.MouseEvent) => {
if (event.target === event.currentTarget) { if (event.target === event.currentTarget) {
@ -48,23 +42,19 @@ const Modal: React.FC<ModalProps> = ({
return ( return (
<div className="modal-overlay" onClick={handleClickOutside}> <div className="modal-overlay" onClick={handleClickOutside}>
<div className="modal-container" onClick={e => e.stopPropagation()}> <div className="modal-container" onClick={(e) => e.stopPropagation()}>
<div className="modal-header"> <div className="modal-header">
<h2 className="modal-title">{title}</h2> <h2 className="modal-title">{title}</h2>
<button <button className="modal-close-button" onClick={onClose} aria-label="Close modal">
className="modal-close-button" <svg
onClick={onClose} xmlns="http://www.w3.org/2000/svg"
aria-label="Close modal" width="24"
> height="24"
<svg viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" fill="none"
width="24" stroke="currentColor"
height="24" strokeWidth="2"
viewBox="0 0 24 24" strokeLinecap="round"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
> >
<line x1="18" y1="6" x2="6" y2="18"></line> <line x1="18" y1="6" x2="6" y2="18"></line>
@ -72,19 +62,13 @@ const Modal: React.FC<ModalProps> = ({
</svg> </svg>
</button> </button>
</div> </div>
<div className="modal-content"> <div className="modal-content">{children}</div>
{children}
</div> {actions && <div className="modal-actions">{actions}</div>}
{actions && (
<div className="modal-actions">
{actions}
</div>
)}
</div> </div>
</div> </div>
); );
}; };
export default Modal; export default Modal;

View File

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

View File

@ -1,7 +1,7 @@
import React, { useRef, useEffect, useState } from "react"; import React, { useRef, useEffect, useState } from "react";
import { formatTime, formatDetailedTime } from "@/lib/timeUtils"; import { formatTime, formatDetailedTime } from "@/lib/timeUtils";
import logger from '../lib/logger'; import logger from "../lib/logger";
import '../styles/VideoPlayer.css'; import "../styles/VideoPlayer.css";
interface VideoPlayerProps { interface VideoPlayerProps {
videoRef: React.RefObject<HTMLVideoElement>; videoRef: React.RefObject<HTMLVideoElement>;
@ -32,37 +32,37 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const isDraggingProgressRef = useRef(false); const isDraggingProgressRef = useRef(false);
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 }); const [tooltipPosition, setTooltipPosition] = useState({ x: 0 });
const [tooltipTime, setTooltipTime] = useState(0); const [tooltipTime, setTooltipTime] = useState(0);
const sampleVideoUrl = typeof window !== 'undefined' && const sampleVideoUrl =
(window as any).MEDIA_DATA?.videoUrl || (typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) ||
"/videos/sample-video-10m.mp4"; "/videos/sample-video-10m.mp4";
// Detect iOS device // Detect iOS device
useEffect(() => { useEffect(() => {
const checkIOS = () => { const checkIOS = () => {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera; const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream; return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
}; };
setIsIOS(checkIOS()); setIsIOS(checkIOS());
// Check if video was previously initialized // Check if video was previously initialized
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
const wasInitialized = localStorage.getItem('video_initialized') === 'true'; const wasInitialized = localStorage.getItem("video_initialized") === "true";
setHasInitialized(wasInitialized); setHasInitialized(wasInitialized);
} }
}, []); }, []);
// Update initialized state when video plays // Update initialized state when video plays
useEffect(() => { useEffect(() => {
if (isPlaying && !hasInitialized) { if (isPlaying && !hasInitialized) {
setHasInitialized(true); setHasInitialized(true);
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
localStorage.setItem('video_initialized', 'true'); localStorage.setItem("video_initialized", "true");
} }
} }
}, [isPlaying, hasInitialized]); }, [isPlaying, hasInitialized]);
// Add iOS-specific attributes to prevent fullscreen playback // Add iOS-specific attributes to prevent fullscreen playback
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
@ -70,15 +70,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
// These attributes need to be set directly on the DOM element // These attributes need to be set directly on the DOM element
// for iOS Safari to respect inline playback // for iOS Safari to respect inline playback
video.setAttribute('playsinline', 'true'); video.setAttribute("playsinline", "true");
video.setAttribute('webkit-playsinline', 'true'); video.setAttribute("webkit-playsinline", "true");
video.setAttribute('x-webkit-airplay', 'allow'); video.setAttribute("x-webkit-airplay", "allow");
// Store the last known good position for iOS // Store the last known good position for iOS
const handleTimeUpdate = () => { const handleTimeUpdate = () => {
if (!isDraggingProgressRef.current) { if (!isDraggingProgressRef.current) {
setLastPosition(video.currentTime); setLastPosition(video.currentTime);
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
window.lastSeekedPosition = video.currentTime; window.lastSeekedPosition = video.currentTime;
} }
} }
@ -86,33 +86,33 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
// Handle iOS-specific play/pause state // Handle iOS-specific play/pause state
const handlePlay = () => { const handlePlay = () => {
logger.debug('Video play event fired'); logger.debug("Video play event fired");
if (isIOS) { if (isIOS) {
setHasInitialized(true); setHasInitialized(true);
localStorage.setItem('video_initialized', 'true'); localStorage.setItem("video_initialized", "true");
} }
}; };
const handlePause = () => { const handlePause = () => {
logger.debug('Video pause event fired'); logger.debug("Video pause event fired");
}; };
video.addEventListener('timeupdate', handleTimeUpdate); video.addEventListener("timeupdate", handleTimeUpdate);
video.addEventListener('play', handlePlay); video.addEventListener("play", handlePlay);
video.addEventListener('pause', handlePause); video.addEventListener("pause", handlePause);
return () => { return () => {
video.removeEventListener('timeupdate', handleTimeUpdate); video.removeEventListener("timeupdate", handleTimeUpdate);
video.removeEventListener('play', handlePlay); video.removeEventListener("play", handlePlay);
video.removeEventListener('pause', handlePause); video.removeEventListener("pause", handlePause);
}; };
}, [videoRef, isIOS, isDraggingProgressRef]); }, [videoRef, isIOS, isDraggingProgressRef]);
// Save current time to lastPosition when it changes (from external seeking) // Save current time to lastPosition when it changes (from external seeking)
useEffect(() => { useEffect(() => {
setLastPosition(currentTime); setLastPosition(currentTime);
}, [currentTime]); }, [currentTime]);
// Jump 10 seconds forward // Jump 10 seconds forward
const handleForward = () => { const handleForward = () => {
const newTime = Math.min(currentTime + 10, duration); const newTime = Math.min(currentTime + 10, duration);
@ -126,58 +126,58 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
onSeek(newTime); onSeek(newTime);
setLastPosition(newTime); setLastPosition(newTime);
}; };
// Calculate progress percentage // Calculate progress percentage
const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0; const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;
// Handle start of progress bar dragging // Handle start of progress bar dragging
const handleProgressDragStart = (e: React.MouseEvent) => { const handleProgressDragStart = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
setIsDraggingProgress(true); setIsDraggingProgress(true);
isDraggingProgressRef.current = true; isDraggingProgressRef.current = true;
// Get initial position // Get initial position
handleProgressDrag(e); handleProgressDrag(e);
// Set up document-level event listeners for mouse movement and release // Set up document-level event listeners for mouse movement and release
const handleMouseMove = (moveEvent: MouseEvent) => { const handleMouseMove = (moveEvent: MouseEvent) => {
if (isDraggingProgressRef.current) { if (isDraggingProgressRef.current) {
handleProgressDrag(moveEvent); handleProgressDrag(moveEvent);
} }
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
setIsDraggingProgress(false); setIsDraggingProgress(false);
isDraggingProgressRef.current = false; isDraggingProgressRef.current = false;
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener("mouseup", handleMouseUp);
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener("mousemove", handleMouseMove);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener("mouseup", handleMouseUp);
}; };
// Handle progress dragging for both mouse and touch events // Handle progress dragging for both mouse and touch events
const handleProgressDrag = (e: MouseEvent | React.MouseEvent) => { const handleProgressDrag = (e: MouseEvent | React.MouseEvent) => {
if (!progressRef.current) return; if (!progressRef.current) return;
const rect = progressRef.current.getBoundingClientRect(); const rect = progressRef.current.getBoundingClientRect();
const clickPosition = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const clickPosition = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const seekTime = duration * clickPosition; const seekTime = duration * clickPosition;
// Update tooltip position and time // Update tooltip position and time
setTooltipPosition({ x: e.clientX }); setTooltipPosition({ x: e.clientX });
setTooltipTime(seekTime); setTooltipTime(seekTime);
// Store position locally for iOS Safari - critical for timeline seeking // Store position locally for iOS Safari - critical for timeline seeking
setLastPosition(seekTime); setLastPosition(seekTime);
// Also store globally for integration with other components // Also store globally for integration with other components
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
(window as any).lastSeekedPosition = seekTime; (window as any).lastSeekedPosition = seekTime;
} }
onSeek(seekTime); onSeek(seekTime);
}; };
@ -185,59 +185,59 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleProgressTouchStart = (e: React.TouchEvent) => { const handleProgressTouchStart = (e: React.TouchEvent) => {
if (!progressRef.current || !e.touches[0]) return; if (!progressRef.current || !e.touches[0]) return;
e.preventDefault(); e.preventDefault();
setIsDraggingProgress(true); setIsDraggingProgress(true);
isDraggingProgressRef.current = true; isDraggingProgressRef.current = true;
// Get initial position using touch // Get initial position using touch
handleProgressTouchMove(e); handleProgressTouchMove(e);
// Set up document-level event listeners for touch movement and release // Set up document-level event listeners for touch movement and release
const handleTouchMove = (moveEvent: TouchEvent) => { const handleTouchMove = (moveEvent: TouchEvent) => {
if (isDraggingProgressRef.current) { if (isDraggingProgressRef.current) {
handleProgressTouchMove(moveEvent); handleProgressTouchMove(moveEvent);
} }
}; };
const handleTouchEnd = () => { const handleTouchEnd = () => {
setIsDraggingProgress(false); setIsDraggingProgress(false);
isDraggingProgressRef.current = false; isDraggingProgressRef.current = false;
document.removeEventListener('touchmove', handleTouchMove); document.removeEventListener("touchmove", handleTouchMove);
document.removeEventListener('touchend', handleTouchEnd); document.removeEventListener("touchend", handleTouchEnd);
document.removeEventListener('touchcancel', handleTouchEnd); document.removeEventListener("touchcancel", handleTouchEnd);
}; };
document.addEventListener('touchmove', handleTouchMove, { passive: false }); document.addEventListener("touchmove", handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd); document.addEventListener("touchend", handleTouchEnd);
document.addEventListener('touchcancel', handleTouchEnd); document.addEventListener("touchcancel", handleTouchEnd);
}; };
// Handle touch dragging on progress bar // Handle touch dragging on progress bar
const handleProgressTouchMove = (e: TouchEvent | React.TouchEvent) => { const handleProgressTouchMove = (e: TouchEvent | React.TouchEvent) => {
if (!progressRef.current) return; if (!progressRef.current) return;
// Get the touch coordinates // Get the touch coordinates
const touch = 'touches' in e ? e.touches[0] : null; const touch = "touches" in e ? e.touches[0] : null;
if (!touch) return; if (!touch) return;
e.preventDefault(); // Prevent scrolling while dragging e.preventDefault(); // Prevent scrolling while dragging
const rect = progressRef.current.getBoundingClientRect(); const rect = progressRef.current.getBoundingClientRect();
const touchPosition = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); const touchPosition = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
const seekTime = duration * touchPosition; const seekTime = duration * touchPosition;
// Update tooltip position and time // Update tooltip position and time
setTooltipPosition({ x: touch.clientX }); setTooltipPosition({ x: touch.clientX });
setTooltipTime(seekTime); setTooltipTime(seekTime);
// Store position for iOS Safari // Store position for iOS Safari
setLastPosition(seekTime); setLastPosition(seekTime);
// Also store globally for integration with other components // Also store globally for integration with other components
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
(window as any).lastSeekedPosition = seekTime; (window as any).lastSeekedPosition = seekTime;
} }
onSeek(seekTime); onSeek(seekTime);
}; };
@ -245,20 +245,20 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => { const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
// If we're already dragging, don't handle the click // If we're already dragging, don't handle the click
if (isDraggingProgress) return; if (isDraggingProgress) return;
if (progressRef.current) { if (progressRef.current) {
const rect = progressRef.current.getBoundingClientRect(); const rect = progressRef.current.getBoundingClientRect();
const clickPosition = (e.clientX - rect.left) / rect.width; const clickPosition = (e.clientX - rect.left) / rect.width;
const seekTime = duration * clickPosition; const seekTime = duration * clickPosition;
// Store position locally for iOS Safari - critical for timeline seeking // Store position locally for iOS Safari - critical for timeline seeking
setLastPosition(seekTime); setLastPosition(seekTime);
// Also store globally for integration with other components // Also store globally for integration with other components
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
(window as any).lastSeekedPosition = seekTime; (window as any).lastSeekedPosition = seekTime;
} }
onSeek(seekTime); onSeek(seekTime);
} }
}; };
@ -278,38 +278,43 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleVideoClick = () => { const handleVideoClick = () => {
const video = videoRef.current; const video = videoRef.current;
if (!video) return; if (!video) return;
// If the video is paused, we want to play it // If the video is paused, we want to play it
if (video.paused) { if (video.paused) {
// For iOS Safari: Before playing, explicitly seek to the remembered position // For iOS Safari: Before playing, explicitly seek to the remembered position
if (isIOS && lastPosition !== null && lastPosition > 0) { if (isIOS && lastPosition !== null && lastPosition > 0) {
logger.debug("iOS: Explicitly setting position before play:", lastPosition); logger.debug("iOS: Explicitly setting position before play:", lastPosition);
// First, seek to the position // First, seek to the position
video.currentTime = lastPosition; video.currentTime = lastPosition;
// Use a small timeout to ensure seeking is complete before play // Use a small timeout to ensure seeking is complete before play
setTimeout(() => { setTimeout(() => {
if (videoRef.current) { if (videoRef.current) {
// Try to play with proper promise handling // Try to play with proper promise handling
videoRef.current.play() videoRef.current
.play()
.then(() => { .then(() => {
logger.debug("iOS: Play started successfully at position:", videoRef.current?.currentTime); logger.debug(
"iOS: Play started successfully at position:",
videoRef.current?.currentTime
);
onPlayPause(); // Update parent state after successful play onPlayPause(); // Update parent state after successful play
}) })
.catch(err => { .catch((err) => {
console.error("iOS: Error playing video:", err); console.error("iOS: Error playing video:", err);
}); });
} }
}, 50); }, 50);
} else { } else {
// Normal play (non-iOS or no remembered position) // Normal play (non-iOS or no remembered position)
video.play() video
.play()
.then(() => { .then(() => {
logger.debug("Normal: Play started successfully"); logger.debug("Normal: Play started successfully");
onPlayPause(); // Update parent state after successful play onPlayPause(); // Update parent state after successful play
}) })
.catch(err => { .catch((err) => {
console.error("Error playing video:", err); console.error("Error playing video:", err);
}); });
} }
@ -336,19 +341,17 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<source src={sampleVideoUrl} type="video/mp4" /> <source src={sampleVideoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p> <p>Your browser doesn't support HTML5 video.</p>
</video> </video>
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */} {/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}
{isIOS && !hasInitialized && !isPlaying && ( {isIOS && !hasInitialized && !isPlaying && (
<div className="ios-first-play-indicator"> <div className="ios-first-play-indicator">
<div className="ios-play-message"> <div className="ios-play-message">Tap Play to initialize video controls</div>
Tap Play to initialize video controls
</div>
</div> </div>
)} )}
{/* Play/Pause Indicator (shows based on current state) */} {/* Play/Pause Indicator (shows based on current state) */}
<div className={`play-pause-indicator ${isPlaying ? 'pause-icon' : 'play-icon'}`}></div> <div className={`play-pause-indicator ${isPlaying ? "pause-icon" : "play-icon"}`}></div>
{/* Video Controls Overlay */} {/* Video Controls Overlay */}
<div className="video-controls"> <div className="video-controls">
{/* Time and Duration */} {/* Time and Duration */}
@ -356,47 +359,52 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<span className="video-current-time">{formatTime(currentTime)}</span> <span className="video-current-time">{formatTime(currentTime)}</span>
<span className="video-duration">/ {formatTime(duration)}</span> <span className="video-duration">/ {formatTime(duration)}</span>
</div> </div>
{/* Progress Bar with enhanced dragging */} {/* Progress Bar with enhanced dragging */}
<div <div
ref={progressRef} ref={progressRef}
className={`video-progress ${isDraggingProgress ? 'dragging' : ''}`} className={`video-progress ${isDraggingProgress ? "dragging" : ""}`}
onClick={handleProgressClick} onClick={handleProgressClick}
onMouseDown={handleProgressDragStart} onMouseDown={handleProgressDragStart}
onTouchStart={handleProgressTouchStart} onTouchStart={handleProgressTouchStart}
> >
<div <div className="video-progress-fill" style={{ width: `${progressPercentage}%` }}></div>
className="video-progress-fill" <div className="video-scrubber" style={{ left: `${progressPercentage}%` }}></div>
style={{ width: `${progressPercentage}%` }}
></div>
<div
className="video-scrubber"
style={{ left: `${progressPercentage}%` }}
></div>
{/* Floating time tooltip when dragging */} {/* Floating time tooltip when dragging */}
{isDraggingProgress && ( {isDraggingProgress && (
<div className="video-time-tooltip" style={{ <div
left: `${tooltipPosition.x}px`, className="video-time-tooltip"
transform: 'translateX(-50%)' style={{
}}> left: `${tooltipPosition.x}px`,
transform: "translateX(-50%)"
}}
>
{formatDetailedTime(tooltipTime)} {formatDetailedTime(tooltipTime)}
</div> </div>
)} )}
</div> </div>
{/* Controls - Mute and Fullscreen buttons */} {/* Controls - Mute and Fullscreen buttons */}
<div className="video-controls-buttons"> <div className="video-controls-buttons">
{/* Mute/Unmute Button */} {/* Mute/Unmute Button */}
{onToggleMute && ( {onToggleMute && (
<button <button
className="mute-button" className="mute-button"
aria-label={isMuted ? "Unmute" : "Mute"} aria-label={isMuted ? "Unmute" : "Mute"}
onClick={onToggleMute} onClick={onToggleMute}
data-tooltip={isMuted ? "Unmute" : "Mute"} data-tooltip={isMuted ? "Unmute" : "Mute"}
> >
{isMuted ? ( {isMuted ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="1" y1="1" x2="23" y2="23"></line> <line x1="1" y1="1" x2="23" y2="23"></line>
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path> <path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path> <path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
@ -404,23 +412,35 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<line x1="8" y1="23" x2="16" y2="23"></line> <line x1="8" y1="23" x2="16" y2="23"></line>
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon> <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path> <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg> </svg>
)} )}
</button> </button>
)} )}
{/* Fullscreen Button */} {/* Fullscreen Button */}
<button <button
className="fullscreen-button" className="fullscreen-button"
aria-label="Fullscreen" aria-label="Fullscreen"
onClick={handleFullscreen} onClick={handleFullscreen}
data-tooltip="Toggle fullscreen" data-tooltip="Toggle fullscreen"
> >
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z" clipRule="evenodd" /> <path
fillRule="evenodd"
d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg> </svg>
</button> </button>
</div> </div>

View File

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

View File

@ -7,25 +7,25 @@ const logger = {
* Logs debug messages only in development environment * Logs debug messages only in development environment
*/ */
debug: (...args: any[]) => { debug: (...args: any[]) => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === "development") {
console.debug(...args); console.debug(...args);
} }
}, },
/** /**
* Always logs error messages * Always logs error messages
*/ */
error: (...args: any[]) => console.error(...args), error: (...args: any[]) => console.error(...args),
/** /**
* Always logs warning messages * Always logs warning messages
*/ */
warn: (...args: any[]) => console.warn(...args), warn: (...args: any[]) => console.warn(...args),
/** /**
* Always logs info messages * Always logs info messages
*/ */
info: (...args: any[]) => console.info(...args) info: (...args: any[]) => console.info(...args)
}; };
export default logger; export default logger;

View File

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

View File

@ -3,17 +3,17 @@
*/ */
export const formatDetailedTime = (seconds: number): string => { export const formatDetailedTime = (seconds: number): string => {
if (isNaN(seconds)) return "00:00:00.000"; if (isNaN(seconds)) return "00:00:00.000";
const hours = Math.floor(seconds / 3600); const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60); const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60); const remainingSeconds = Math.floor(seconds % 60);
const milliseconds = Math.round((seconds % 1) * 1000); const milliseconds = Math.round((seconds % 1) * 1000);
const formattedHours = String(hours).padStart(2, "0"); const formattedHours = String(hours).padStart(2, "0");
const formattedMinutes = String(minutes).padStart(2, "0"); const formattedMinutes = String(minutes).padStart(2, "0");
const formattedSeconds = String(remainingSeconds).padStart(2, "0"); const formattedSeconds = String(remainingSeconds).padStart(2, "0");
const formattedMilliseconds = String(milliseconds).padStart(3, "0"); const formattedMilliseconds = String(milliseconds).padStart(3, "0");
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`;
}; };

View File

@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs));
} }

View File

@ -2,20 +2,17 @@
* Generate a solid color background for a segment * Generate a solid color background for a segment
* Returns a CSS color based on the segment position * Returns a CSS color based on the segment position
*/ */
export const generateSolidColor = ( export const generateSolidColor = (time: number, duration: number): string => {
time: number,
duration: number
): string => {
// Use the time position to create different colors // Use the time position to create different colors
// This gives each segment a different color without needing an image // This gives each segment a different color without needing an image
const position = Math.min(Math.max(time / (duration || 1), 0), 1); const position = Math.min(Math.max(time / (duration || 1), 0), 1);
// Calculate color based on position // Calculate color based on position
// Use an extremely light blue-based color palette // Use an extremely light blue-based color palette
const hue = 210; // Blue base const hue = 210; // Blue base
const saturation = 40 + Math.floor(position * 20); // 40-60% (less saturated) const saturation = 40 + Math.floor(position * 20); // 40-60% (less saturated)
const lightness = 85 + Math.floor(position * 8); // 85-93% (extremely light) const lightness = 85 + Math.floor(position * 8); // 85-93% (extremely light)
return `hsl(${hue}, ${saturation}%, ${lightness}%)`; 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 * Now returns a data URL for a solid color square instead of a video thumbnail
*/ */
export const generateThumbnail = async ( export const generateThumbnail = async (
videoElement: HTMLVideoElement, videoElement: HTMLVideoElement,
time: number time: number
): Promise<string> => { ): Promise<string> => {
return new Promise((resolve) => { return new Promise((resolve) => {
// Create a small canvas for the solid color // Create a small canvas for the solid color
const canvas = document.createElement('canvas'); const canvas = document.createElement("canvas");
canvas.width = 10; // Much smaller - we only need a color canvas.width = 10; // Much smaller - we only need a color
canvas.height = 10; canvas.height = 10;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
// Get the solid color based on time // Get the solid color based on time
const color = generateSolidColor(time, videoElement.duration); const color = generateSolidColor(time, videoElement.duration);
// Fill with solid color // Fill with solid color
ctx.fillStyle = color; ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
} }
// Convert to data URL (much smaller now) // Convert to data URL (much smaller now)
const dataUrl = canvas.toDataURL('image/png', 0.5); const dataUrl = canvas.toDataURL("image/png", 0.5);
resolve(dataUrl); resolve(dataUrl);
}); });
}; };

View File

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

View File

@ -4,36 +4,36 @@ interface TrimVideoRequest {
segments: { segments: {
startTime: string; startTime: string;
endTime: string; endTime: string;
name?: string; name?: string;
}[]; }[];
saveAsCopy?: boolean; saveAsCopy?: boolean;
saveIndividualSegments?: boolean; saveIndividualSegments?: boolean;
} }
interface TrimVideoResponse { interface TrimVideoResponse {
msg: string; msg: string;
url_redirect: string; url_redirect: string;
status?: number; // HTTP status code for success/error status?: number; // HTTP status code for success/error
error?: string; // Error message if status is not 200 error?: string; // Error message if status is not 200
} }
// Helper function to simulate delay // Helper function to simulate delay
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
// For now, we'll use a mock API that returns a promise // For now, we'll use a mock API that returns a promise
// This can be replaced with actual API calls later // This can be replaced with actual API calls later
export const trimVideo = async ( export const trimVideo = async (
mediaId: string, mediaId: string,
data: TrimVideoRequest data: TrimVideoRequest
): Promise<TrimVideoResponse> => { ): Promise<TrimVideoResponse> => {
try { try {
// Attempt the real API call // Attempt the real API call
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, { const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
method: 'POST', method: "POST",
headers: { 'Content-Type': 'application/json' }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
if (!response.ok) { if (!response.ok) {
// For error responses, return with error status and message // For error responses, return with error status and message
if (response.status === 400) { if (response.status === 400) {
@ -86,7 +86,7 @@ export const trimVideo = async (
}; };
} }
} }
// Successful response // Successful response
const jsonResponse = await response.json(); const jsonResponse = await response.json();
return { return {
@ -104,7 +104,7 @@ export const trimVideo = async (
url_redirect: `./view?m=${mediaId}` url_redirect: `./view?m=${mediaId}`
}; };
} }
/* Mock implementation that simulates network latency /* Mock implementation that simulates network latency
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
@ -115,4 +115,4 @@ export const trimVideo = async (
}, 1500); // Simulate 1.5 second server delay }, 1500); // Simulate 1.5 second server delay
}); });
*/ */
}; };

View File

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

View File

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

View File

@ -132,7 +132,7 @@
.ios-notification { .ios-notification {
padding-top: env(safe-area-inset-top); padding-top: env(safe-area-inset-top);
} }
.ios-notification-close { .ios-notification-close {
padding: 10px; padding: 10px;
} }
@ -143,11 +143,11 @@
.ios-notification-content { .ios-notification-content {
padding: 5px; padding: 5px;
} }
.ios-notification-message h3 { .ios-notification-message h3 {
font-size: 15px; font-size: 15px;
} }
.ios-notification-message p, .ios-notification-message p,
.ios-notification-message ol { .ios-notification-message ol {
font-size: 13px; font-size: 13px;
@ -164,4 +164,4 @@ html.ios-device {
html.ios-device .ios-control-btn { html.ios-device .ios-control-btn {
/* Make buttons easier to tap in desktop mode */ /* Make buttons easier to tap in desktop mode */
min-height: 44px; min-height: 44px;
} }

View File

@ -93,4 +93,4 @@
/* Extra spacing for mobile */ /* Extra spacing for mobile */
padding: 14px 25px; padding: 14px 25px;
} }
} }

View File

@ -36,13 +36,13 @@
.ios-video-player-container video { .ios-video-player-container video {
max-height: 50vh; /* Use viewport height on iOS */ max-height: 50vh; /* Use viewport height on iOS */
} }
/* Improve controls visibility on iOS */ /* Improve controls visibility on iOS */
video::-webkit-media-controls { video::-webkit-media-controls {
opacity: 1 !important; opacity: 1 !important;
visibility: visible !important; visibility: visible !important;
} }
/* Ensure controls don't disappear too quickly */ /* Ensure controls don't disappear too quickly */
video::-webkit-media-controls-panel { video::-webkit-media-controls-panel {
transition-duration: 3s !important; transition-duration: 3s !important;
@ -76,19 +76,19 @@
/* Prevent text selection on buttons */ /* Prevent text selection on buttons */
.no-select { .no-select {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */ -webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */ -khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */ -moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, supported by Chrome and Opera */ user-select: none; /* Non-prefixed version, supported by Chrome and Opera */
cursor: default; cursor: default;
} }
/* Specifically prevent default behavior on fine controls */ /* Specifically prevent default behavior on fine controls */
.ios-fine-controls button, .ios-fine-controls button,
.ios-external-controls .no-select { .ios-external-controls .no-select {
touch-action: manipulation; touch-action: manipulation;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
pointer-events: auto; pointer-events: auto;
} }

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
[data-tooltip] { [data-tooltip] {
position: relative; position: relative;
} }
[data-tooltip]:before { [data-tooltip]:before {
content: attr(data-tooltip); content: attr(data-tooltip);
position: absolute; position: absolute;
@ -21,13 +21,15 @@
white-space: nowrap; white-space: nowrap;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
z-index: 1000; z-index: 1000;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]:after { [data-tooltip]:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
left: 50%; left: 50%;
@ -37,17 +39,19 @@
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent; border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s; transition:
opacity 0.2s,
visibility 0.2s;
pointer-events: none; pointer-events: none;
} }
[data-tooltip]:hover:before, [data-tooltip]:hover:before,
[data-tooltip]:hover:after { [data-tooltip]:hover:after {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
} }
/* Hide button tooltips on touch devices */ /* Hide button tooltips on touch devices */
@media (pointer: coarse) { @media (pointer: coarse) {
[data-tooltip]:before, [data-tooltip]:before,
@ -71,7 +75,7 @@
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
} }
.video-player-container video { .video-player-container video {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -83,7 +87,7 @@
-webkit-user-select: none; -webkit-user-select: none;
user-select: none; user-select: none;
} }
/* iOS-specific styles */ /* iOS-specific styles */
@supports (-webkit-touch-callout: none) { @supports (-webkit-touch-callout: none) {
.video-player-container video { .video-player-container video {
@ -92,7 +96,7 @@
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
} }
.play-pause-indicator { .play-pause-indicator {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -106,19 +110,19 @@
transition: opacity 0.3s; transition: opacity 0.3s;
pointer-events: none; pointer-events: none;
} }
.video-player-container:hover .play-pause-indicator { .video-player-container:hover .play-pause-indicator {
opacity: 1; opacity: 1;
} }
.play-pause-indicator::before { .play-pause-indicator::before {
content: ''; content: "";
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.play-pause-indicator.play-icon::before { .play-pause-indicator.play-icon::before {
width: 0; width: 0;
height: 0; height: 0;
@ -127,14 +131,14 @@
border-left: 25px solid white; border-left: 25px solid white;
margin-left: 3px; margin-left: 3px;
} }
.play-pause-indicator.pause-icon::before { .play-pause-indicator.pause-icon::before {
width: 20px; width: 20px;
height: 25px; height: 25px;
border-left: 6px solid white; border-left: 6px solid white;
border-right: 6px solid white; border-right: 6px solid white;
} }
/* iOS First-play indicator */ /* iOS First-play indicator */
.ios-first-play-indicator { .ios-first-play-indicator {
position: absolute; position: absolute;
@ -148,7 +152,7 @@
justify-content: center; justify-content: center;
z-index: 10; z-index: 10;
} }
.ios-play-message { .ios-play-message {
color: white; color: white;
font-size: 1.2rem; font-size: 1.2rem;
@ -158,13 +162,22 @@
border-radius: 0.5rem; border-radius: 0.5rem;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@keyframes pulse { @keyframes pulse {
0% { opacity: 0.7; transform: scale(1); } 0% {
50% { opacity: 1; transform: scale(1.05); } opacity: 0.7;
100% { opacity: 0.7; transform: scale(1); } transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
100% {
opacity: 0.7;
transform: scale(1);
}
} }
.video-controls { .video-controls {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@ -175,21 +188,21 @@
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.3s;
} }
.video-player-container:hover .video-controls { .video-player-container:hover .video-controls {
opacity: 1; opacity: 1;
} }
.video-current-time { .video-current-time {
color: white; color: white;
font-size: 0.875rem; font-size: 0.875rem;
} }
.video-duration { .video-duration {
color: white; color: white;
font-size: 0.875rem; font-size: 0.875rem;
} }
.video-time-display { .video-time-display {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -197,7 +210,7 @@
color: white; color: white;
font-size: 0.875rem; font-size: 0.875rem;
} }
.video-progress { .video-progress {
position: relative; position: relative;
height: 6px; height: 6px;
@ -208,11 +221,11 @@
touch-action: none; /* Prevent browser handling of drag gestures */ touch-action: none; /* Prevent browser handling of drag gestures */
flex-grow: 1; flex-grow: 1;
} }
.video-progress.dragging { .video-progress.dragging {
height: 8px; height: 8px;
} }
.video-progress-fill { .video-progress-fill {
position: absolute; position: absolute;
top: 0; top: 0;
@ -222,7 +235,7 @@
border-radius: 3px; border-radius: 3px;
pointer-events: none; pointer-events: none;
} }
.video-scrubber { .video-scrubber {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -232,9 +245,12 @@
background-color: #ff0000; background-color: #ff0000;
border-radius: 50%; border-radius: 50%;
cursor: grab; cursor: grab;
transition: transform 0.1s ease, width 0.1s ease, height 0.1s ease; transition:
transform 0.1s ease,
width 0.1s ease,
height 0.1s ease;
} }
/* Make the scrubber larger when dragging for better control */ /* Make the scrubber larger when dragging for better control */
.video-progress.dragging .video-scrubber { .video-progress.dragging .video-scrubber {
transform: translate(-50%, -50%) scale(1.2); transform: translate(-50%, -50%) scale(1.2);
@ -243,22 +259,22 @@
cursor: grabbing; cursor: grabbing;
box-shadow: 0 0 8px rgba(255, 0, 0, 0.6); box-shadow: 0 0 8px rgba(255, 0, 0, 0.6);
} }
/* Enhance for touch devices */ /* Enhance for touch devices */
@media (pointer: coarse) { @media (pointer: coarse) {
.video-scrubber { .video-scrubber {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
.video-progress.dragging .video-scrubber { .video-progress.dragging .video-scrubber {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
/* Create a larger invisible touch target */ /* Create a larger invisible touch target */
.video-scrubber:before { .video-scrubber:before {
content: ''; content: "";
position: absolute; position: absolute;
top: -10px; top: -10px;
left: -10px; left: -10px;
@ -266,14 +282,14 @@
bottom: -10px; bottom: -10px;
} }
} }
.video-controls-buttons { .video-controls-buttons {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: 0.75rem; gap: 0.75rem;
} }
.mute-button, .mute-button,
.fullscreen-button { .fullscreen-button {
min-width: auto; min-width: auto;
@ -283,17 +299,17 @@
cursor: pointer; cursor: pointer;
padding: 0.25rem; padding: 0.25rem;
transition: transform 0.2s; transition: transform 0.2s;
&:hover { &:hover {
transform: scale(1.1); transform: scale(1.1);
} }
svg { svg {
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
} }
} }
/* Time tooltip that appears when dragging */ /* Time tooltip that appears when dragging */
.video-time-tooltip { .video-time-tooltip {
position: absolute; position: absolute;
@ -309,10 +325,10 @@
white-space: nowrap; white-space: nowrap;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
/* Add a small arrow to the tooltip */ /* Add a small arrow to the tooltip */
.video-time-tooltip:after { .video-time-tooltip:after {
content: ''; content: "";
position: absolute; position: absolute;
bottom: -4px; bottom: -4px;
left: 50%; left: 50%;
@ -323,4 +339,4 @@
border-right: 4px solid transparent; border-right: 4px solid transparent;
border-top: 4px solid rgba(0, 0, 0, 0.7); border-top: 4px solid rgba(0, 0, 0, 0.7);
} }
} }