mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 07:28:53 -05:00
style: Format entire codebase (video-editor) with Prettier
This commit is contained in:
parent
e101a48c48
commit
add6f6a704
@ -41,7 +41,7 @@ const App = () => {
|
|||||||
videoInitialized,
|
videoInitialized,
|
||||||
setVideoInitialized,
|
setVideoInitialized,
|
||||||
isPlayingSegments,
|
isPlayingSegments,
|
||||||
handlePlaySegments,
|
handlePlaySegments
|
||||||
} = useVideoTrimmer();
|
} = useVideoTrimmer();
|
||||||
|
|
||||||
// Function to play from the beginning
|
// Function to play from the beginning
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -125,13 +125,13 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
background-color: #EEE; /* Very light gray background */
|
background-color: #eee; /* Very light gray background */
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #EEE; /* Very light gray background */
|
background-color: #eee; /* Very light gray background */
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -208,17 +208,27 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: box-shadow 0.2s, transform 0.1s;
|
transition:
|
||||||
|
box-shadow 0.2s,
|
||||||
|
transform 0.1s;
|
||||||
/* Original z-index for stacking order based on segment ID */
|
/* Original z-index for stacking order based on segment ID */
|
||||||
z-index: 15;
|
z-index: 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* No background colors for segments, just borders with 2-color scheme */
|
/* No background colors for segments, just borders with 2-color scheme */
|
||||||
.clip-segment:nth-child(odd), .segment-color-1, .segment-color-3, .segment-color-5, .segment-color-7 {
|
.clip-segment:nth-child(odd),
|
||||||
|
.segment-color-1,
|
||||||
|
.segment-color-3,
|
||||||
|
.segment-color-5,
|
||||||
|
.segment-color-7 {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 2px solid rgba(0, 123, 255, 0.9); /* Blue border */
|
border: 2px solid rgba(0, 123, 255, 0.9); /* Blue border */
|
||||||
}
|
}
|
||||||
.clip-segment:nth-child(even), .segment-color-2, .segment-color-4, .segment-color-6, .segment-color-8 {
|
.clip-segment:nth-child(even),
|
||||||
|
.segment-color-2,
|
||||||
|
.segment-color-4,
|
||||||
|
.segment-color-6,
|
||||||
|
.segment-color-8 {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */
|
border: 2px solid rgba(108, 117, 125, 0.9); /* Gray border */
|
||||||
}
|
}
|
||||||
@ -315,7 +325,7 @@
|
|||||||
input[type="range"] {
|
input[type="range"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background: #E0E0E0;
|
background: #e0e0e0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,12 +360,14 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]::after {
|
[data-tooltip]::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -366,7 +378,9 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,7 +478,7 @@ button[disabled][data-tooltip]::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.segment-tooltip::after {
|
.segment-tooltip::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -539,7 +553,7 @@ button[disabled][data-tooltip]::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-space-tooltip::after {
|
.empty-space-tooltip::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -8px;
|
bottom: -8px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -617,7 +631,9 @@ button[disabled][data-tooltip]::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Save buttons styling */
|
/* Save buttons styling */
|
||||||
.save-button, .save-copy-button, .save-segments-button {
|
.save-button,
|
||||||
|
.save-copy-button,
|
||||||
|
.save-segments-button {
|
||||||
background-color: rgba(0, 123, 255, 0.8);
|
background-color: rgba(0, 123, 255, 0.8);
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
@ -628,7 +644,8 @@ button[disabled][data-tooltip]::after {
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button:hover, .save-copy-button:hover {
|
.save-button:hover,
|
||||||
|
.save-copy-button:hover {
|
||||||
background-color: rgba(0, 123, 255, 1);
|
background-color: rgba(0, 123, 255, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -735,7 +752,8 @@ button[disabled][data-tooltip]::after {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-time, .duration-time {
|
.current-time,
|
||||||
|
.duration-time {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -770,7 +788,8 @@ button[disabled][data-tooltip]::after {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button, .save-copy-button {
|
.save-button,
|
||||||
|
.save-copy-button {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,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;
|
||||||
|
|||||||
@ -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
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,4 +93,4 @@
|
|||||||
/* Extra spacing for mobile */
|
/* Extra spacing for mobile */
|
||||||
padding: 14px 25px;
|
padding: 14px 25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -257,7 +257,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.clip-segment-handle:after {
|
.clip-segment-handle:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -321,7 +321,7 @@
|
|||||||
|
|
||||||
.segment-tooltip:after,
|
.segment-tooltip:after,
|
||||||
.empty-space-tooltip:after {
|
.empty-space-tooltip:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -335,7 +335,7 @@
|
|||||||
|
|
||||||
.segment-tooltip:before,
|
.segment-tooltip:before,
|
||||||
.empty-space-tooltip:before {
|
.empty-space-tooltip:before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -6px;
|
bottom: -6px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -612,13 +612,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
[data-tooltip]:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -628,7 +630,9 @@
|
|||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -111,7 +111,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 2500; /* High z-index */
|
z-index: 2500; /* High z-index */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@ -130,7 +132,9 @@
|
|||||||
margin-left: 0; /* Reset margin */
|
margin-left: 0; /* Reset margin */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
z-index: 2500; /* High z-index */
|
z-index: 2500; /* High z-index */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user