mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-06 07:28:53 -05:00
chore: video-editor update with new prettier settings
This commit is contained in:
parent
eaf87e20d8
commit
074638e237
@ -1,85 +1,83 @@
|
|||||||
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;
|
||||||
name: string;
|
name: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
endTime: number;
|
endTime: number;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClipSegmentsProps {
|
interface ClipSegmentsProps {
|
||||||
segments: Segment[];
|
segments: Segment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
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 key={segment.id} className={`segment-item ${getSegmentColorClass(index)}`}>
|
<div 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">Segment {index + 1}</div>
|
<div className="segment-title">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>
|
||||||
<div className="segment-duration">
|
<div className="segment-duration">
|
||||||
Duration: {formatLongTime(segment.endTime - segment.startTime)}
|
Duration: {formatLongTime(segment.endTime - segment.startTime)}
|
||||||
</div>
|
</div>
|
||||||
</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
|
<path
|
||||||
fillRule="evenodd"
|
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"
|
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"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{sortedSegments.length === 0 && (
|
||||||
|
<div className="empty-message">No segments created yet. Use the split button to create segments.</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
|
||||||
{sortedSegments.length === 0 && (
|
|
||||||
<div className="empty-message">
|
|
||||||
No segments created yet. Use the split button to create segments.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ClipSegments;
|
export default ClipSegments;
|
||||||
|
|||||||
@ -1,108 +1,108 @@
|
|||||||
import "../styles/EditingTools.css";
|
import '../styles/EditingTools.css';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
interface EditingToolsProps {
|
interface EditingToolsProps {
|
||||||
onSplit: () => void;
|
onSplit: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onUndo: () => void;
|
onUndo: () => void;
|
||||||
onRedo: () => void;
|
onRedo: () => void;
|
||||||
onPlaySegments: () => void;
|
onPlaySegments: () => void;
|
||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
canRedo: boolean;
|
canRedo: boolean;
|
||||||
isPlaying?: boolean;
|
isPlaying?: boolean;
|
||||||
isPlayingSegments?: boolean;
|
isPlayingSegments?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditingTools = ({
|
const EditingTools = ({
|
||||||
onSplit,
|
onSplit,
|
||||||
onReset,
|
onReset,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
onPlaySegments,
|
onPlaySegments,
|
||||||
onPlay,
|
onPlay,
|
||||||
canUndo,
|
canUndo,
|
||||||
canRedo,
|
canRedo,
|
||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
isPlayingSegments = false
|
isPlayingSegments = false,
|
||||||
}: EditingToolsProps) => {
|
}: EditingToolsProps) => {
|
||||||
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
const [isSmallScreen, setIsSmallScreen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkScreenSize = () => {
|
const checkScreenSize = () => {
|
||||||
setIsSmallScreen(window.innerWidth <= 640);
|
setIsSmallScreen(window.innerWidth <= 640);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkScreenSize();
|
||||||
|
window.addEventListener('resize', checkScreenSize);
|
||||||
|
return () => window.removeEventListener('resize', checkScreenSize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle play button click with iOS fix
|
||||||
|
const handlePlay = () => {
|
||||||
|
// Ensure lastSeekedPosition is used when play is clicked
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('Play button clicked, current lastSeekedPosition:', window.lastSeekedPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the original handler
|
||||||
|
onPlay();
|
||||||
};
|
};
|
||||||
|
|
||||||
checkScreenSize();
|
return (
|
||||||
window.addEventListener("resize", checkScreenSize);
|
<div className="editing-tools-container">
|
||||||
return () => window.removeEventListener("resize", checkScreenSize);
|
<div className="flex-container single-row">
|
||||||
}, []);
|
{/* Left side - Play buttons group */}
|
||||||
|
<div className="button-group play-buttons-group">
|
||||||
|
{/* Play Segments button */}
|
||||||
|
<button
|
||||||
|
className={`button segments-button`}
|
||||||
|
onClick={onPlaySegments}
|
||||||
|
data-tooltip={
|
||||||
|
isPlayingSegments ? 'Stop segments playback' : 'Play segments in one continuous flow'
|
||||||
|
}
|
||||||
|
style={{ fontSize: '0.875rem' }}
|
||||||
|
>
|
||||||
|
{isPlayingSegments ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="10" y1="15" x2="10" y2="9" />
|
||||||
|
<line x1="14" y1="15" x2="14" y2="9" />
|
||||||
|
</svg>
|
||||||
|
<span className="full-text">Stop Preview</span>
|
||||||
|
<span className="short-text">Stop Preview</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polygon points="10 8 16 12 10 16 10 8" />
|
||||||
|
</svg>
|
||||||
|
<span className="full-text">Play Preview</span>
|
||||||
|
<span className="short-text">Play Preview</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
// Handle play button click with iOS fix
|
{/* Play Preview button */}
|
||||||
const handlePlay = () => {
|
{/* <button
|
||||||
// Ensure lastSeekedPosition is used when play is clicked
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
console.log("Play button clicked, current lastSeekedPosition:", window.lastSeekedPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the original handler
|
|
||||||
onPlay();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="editing-tools-container">
|
|
||||||
<div className="flex-container single-row">
|
|
||||||
{/* Left side - Play buttons group */}
|
|
||||||
<div className="button-group play-buttons-group">
|
|
||||||
{/* Play Segments button */}
|
|
||||||
<button
|
|
||||||
className={`button segments-button`}
|
|
||||||
onClick={onPlaySegments}
|
|
||||||
data-tooltip={
|
|
||||||
isPlayingSegments ? "Stop segments playback" : "Play segments in one continuous flow"
|
|
||||||
}
|
|
||||||
style={{ fontSize: "0.875rem" }}
|
|
||||||
>
|
|
||||||
{isPlayingSegments ? (
|
|
||||||
<>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<line x1="10" y1="15" x2="10" y2="9" />
|
|
||||||
<line x1="14" y1="15" x2="14" y2="9" />
|
|
||||||
</svg>
|
|
||||||
<span className="full-text">Stop Preview</span>
|
|
||||||
<span className="short-text">Stop Preview</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
<polygon points="10 8 16 12 10 16 10 8" />
|
|
||||||
</svg>
|
|
||||||
<span className="full-text">Play Preview</span>
|
|
||||||
<span className="short-text">Play Preview</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Play Preview button */}
|
|
||||||
{/* <button
|
|
||||||
className="button preview-button"
|
className="button preview-button"
|
||||||
onClick={onPreview}
|
onClick={onPreview}
|
||||||
data-tooltip={isPreviewMode ? "Stop preview playback" : "Play only segments (skips gaps between segments)"}
|
data-tooltip={isPreviewMode ? "Stop preview playback" : "Play only segments (skips gaps between segments)"}
|
||||||
@ -130,56 +130,56 @@ const EditingTools = ({
|
|||||||
)}
|
)}
|
||||||
</button> */}
|
</button> */}
|
||||||
|
|
||||||
{/* Standard Play button (only shown when not in segments playback on small screens) */}
|
{/* Standard Play button (only shown when not in segments playback on small screens) */}
|
||||||
{(!isPlayingSegments || !isSmallScreen) && (
|
{(!isPlayingSegments || !isSmallScreen) && (
|
||||||
<button
|
<button
|
||||||
className={`button play-button ${isPlayingSegments ? "greyed-out" : ""}`}
|
className={`button play-button ${isPlayingSegments ? 'greyed-out' : ''}`}
|
||||||
onClick={handlePlay}
|
onClick={handlePlay}
|
||||||
data-tooltip={isPlaying ? "Pause video" : "Play full video"}
|
data-tooltip={isPlaying ? 'Pause video' : 'Play full video'}
|
||||||
style={{ fontSize: "0.875rem" }}
|
style={{ fontSize: '0.875rem' }}
|
||||||
disabled={isPlayingSegments}
|
disabled={isPlayingSegments}
|
||||||
>
|
>
|
||||||
{isPlaying && !isPlayingSegments ? (
|
{isPlaying && !isPlayingSegments ? (
|
||||||
<>
|
<>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<line x1="10" y1="15" x2="10" y2="9" />
|
<line x1="10" y1="15" x2="10" y2="9" />
|
||||||
<line x1="14" y1="15" x2="14" y2="9" />
|
<line x1="14" y1="15" x2="14" y2="9" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="full-text">Pause</span>
|
<span className="full-text">Pause</span>
|
||||||
<span className="short-text">Pause</span>
|
<span className="short-text">Pause</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<polygon points="10 8 16 12 10 16 10 8" />
|
<polygon points="10 8 16 12 10 16 10 8" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="full-text">Play</span>
|
<span className="full-text">Play</span>
|
||||||
<span className="short-text">Play</span>
|
<span className="short-text">Play</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Segments Playback message (replaces play button during segments playback) */}
|
{/* Segments Playback message (replaces play button during segments playback) */}
|
||||||
{/* {isPlayingSegments && !isSmallScreen && (
|
{/* {isPlayingSegments && !isSmallScreen && (
|
||||||
<div className="segments-playback-message">
|
<div className="segments-playback-message">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
@ -190,8 +190,8 @@ const EditingTools = ({
|
|||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{/* Preview mode message (replaces play button) */}
|
{/* Preview mode message (replaces play button) */}
|
||||||
{/* {isPreviewMode && (
|
{/* {isPreviewMode && (
|
||||||
<div className="preview-mode-message">
|
<div className="preview-mode-message">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
@ -201,72 +201,72 @@ const EditingTools = ({
|
|||||||
Preview Mode
|
Preview Mode
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Editing tools */}
|
{/* Right side - Editing tools */}
|
||||||
<div className="button-group secondary">
|
<div className="button-group secondary">
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
aria-label="Undo"
|
aria-label="Undo"
|
||||||
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Undo last action"}
|
data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Undo last action'}
|
||||||
disabled={!canUndo || isPlayingSegments}
|
disabled={!canUndo || isPlayingSegments}
|
||||||
onClick={onUndo}
|
onClick={onUndo}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<path d="M9 14 4 9l5-5" />
|
<path d="M9 14 4 9l5-5" />
|
||||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11" />
|
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="button-text">Undo</span>
|
<span className="button-text">Undo</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
aria-label="Redo"
|
aria-label="Redo"
|
||||||
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Redo last undone action"}
|
data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Redo last undone action'}
|
||||||
disabled={!canRedo || isPlayingSegments}
|
disabled={!canRedo || isPlayingSegments}
|
||||||
onClick={onRedo}
|
onClick={onRedo}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<path d="m15 14 5-5-5-5" />
|
<path d="m15 14 5-5-5-5" />
|
||||||
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5v0A5.5 5.5 0 0 0 9.5 20H13" />
|
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5v0A5.5 5.5 0 0 0 9.5 20H13" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="button-text">Redo</span>
|
<span className="button-text">Redo</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="divider"></div>
|
<div className="divider"></div>
|
||||||
<button
|
<button
|
||||||
className="button"
|
className="button"
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
data-tooltip={isPlayingSegments ? "Disabled during preview" : "Reset to full video"}
|
data-tooltip={isPlayingSegments ? 'Disabled during preview' : 'Reset to full video'}
|
||||||
disabled={isPlayingSegments}
|
disabled={isPlayingSegments}
|
||||||
>
|
>
|
||||||
<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
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z"
|
||||||
clipRule="evenodd"
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="reset-text">Reset</span>
|
<span className="reset-text">Reset</span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditingTools;
|
export default EditingTools;
|
||||||
|
|||||||
@ -1,55 +1,55 @@
|
|||||||
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>;
|
||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay }) => {
|
const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay }) => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
// Check if the device is mobile
|
// Check if the device is mobile
|
||||||
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(
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i.test(
|
||||||
navigator.userAgent
|
navigator.userAgent
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always show for mobile devices on each visit
|
||||||
|
const isMobile = checkIsMobile();
|
||||||
|
setIsVisible(isMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Close the prompt when video plays
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
// Just close the prompt when video plays
|
||||||
|
setIsVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('play', handlePlay);
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('play', handlePlay);
|
||||||
|
};
|
||||||
|
}, [videoRef]);
|
||||||
|
|
||||||
|
const handlePlayClick = () => {
|
||||||
|
onPlay();
|
||||||
|
// Prompt will be closed by the play event handler
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always show for mobile devices on each visit
|
if (!isVisible) return null;
|
||||||
const isMobile = checkIsMobile();
|
|
||||||
setIsVisible(isMobile);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Close the prompt when video plays
|
return (
|
||||||
useEffect(() => {
|
<div className="mobile-play-prompt-overlay">
|
||||||
const video = videoRef.current;
|
<div className="mobile-play-prompt">
|
||||||
if (!video) return;
|
{/* <h3>Mobile Device Notice</h3>
|
||||||
|
|
||||||
const handlePlay = () => {
|
|
||||||
// Just close the prompt when video plays
|
|
||||||
setIsVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
video.addEventListener("play", handlePlay);
|
|
||||||
return () => {
|
|
||||||
video.removeEventListener("play", handlePlay);
|
|
||||||
};
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
const handlePlayClick = () => {
|
|
||||||
onPlay();
|
|
||||||
// Prompt will be closed by the play event handler
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mobile-play-prompt-overlay">
|
|
||||||
<div className="mobile-play-prompt">
|
|
||||||
{/* <h3>Mobile Device Notice</h3>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
For the best video editing experience on mobile devices, you need to <strong>play the video first</strong> before
|
For the best video editing experience on mobile devices, you need to <strong>play the video first</strong> before
|
||||||
@ -65,12 +65,12 @@ const MobilePlayPrompt: React.FC<MobilePlayPromptProps> = ({ videoRef, onPlay })
|
|||||||
</ol>
|
</ol>
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
<button className="mobile-play-button" onClick={handlePlayClick}>
|
<button className="mobile-play-button" onClick={handlePlayClick}>
|
||||||
Click to start editing...
|
Click to start editing...
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MobilePlayPrompt;
|
export default MobilePlayPrompt;
|
||||||
|
|||||||
@ -1,184 +1,184 @@
|
|||||||
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>;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps) => {
|
const IOSVideoPlayer = ({ 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);
|
||||||
|
|
||||||
// Clean up intervals on unmount
|
// Clean up intervals on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
|
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
|
||||||
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
|
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get the video source URL from the main player
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef.current && videoRef.current.querySelector('source')) {
|
||||||
|
const source = videoRef.current.querySelector('source') as HTMLSourceElement;
|
||||||
|
if (source && source.src) {
|
||||||
|
setVideoUrl(source.src);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to sample video if needed
|
||||||
|
setVideoUrl('/videos/sample-video.mp4');
|
||||||
|
}
|
||||||
|
}, [videoRef]);
|
||||||
|
|
||||||
|
// Function to jump 15 seconds backward
|
||||||
|
const jumpBackward15 = () => {
|
||||||
|
if (iosVideoRef) {
|
||||||
|
const newTime = Math.max(0, iosVideoRef.currentTime - 15);
|
||||||
|
iosVideoRef.currentTime = newTime;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Get the video source URL from the main player
|
// Function to jump 15 seconds forward
|
||||||
useEffect(() => {
|
const jumpForward15 = () => {
|
||||||
if (videoRef.current && videoRef.current.querySelector("source")) {
|
if (iosVideoRef) {
|
||||||
const source = videoRef.current.querySelector("source") as HTMLSourceElement;
|
const newTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 15);
|
||||||
if (source && source.src) {
|
iosVideoRef.currentTime = newTime;
|
||||||
setVideoUrl(source.src);
|
}
|
||||||
}
|
};
|
||||||
} else {
|
|
||||||
// Fallback to sample video if needed
|
|
||||||
setVideoUrl("/videos/sample-video-10m.mp4");
|
|
||||||
}
|
|
||||||
}, [videoRef]);
|
|
||||||
|
|
||||||
// Function to jump 15 seconds backward
|
// Start continuous 50ms increment when button is held
|
||||||
const jumpBackward15 = () => {
|
const startIncrement = (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
if (iosVideoRef) {
|
// Prevent default to avoid text selection
|
||||||
const newTime = Math.max(0, iosVideoRef.currentTime - 15);
|
e.preventDefault();
|
||||||
iosVideoRef.currentTime = newTime;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to jump 15 seconds forward
|
if (!iosVideoRef) return;
|
||||||
const jumpForward15 = () => {
|
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
|
||||||
if (iosVideoRef) {
|
|
||||||
const newTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 15);
|
|
||||||
iosVideoRef.currentTime = newTime;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start continuous 50ms increment when button is held
|
// First immediate adjustment
|
||||||
const startIncrement = (e: React.MouseEvent | React.TouchEvent) => {
|
|
||||||
// Prevent default to avoid text selection
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!iosVideoRef) return;
|
|
||||||
if (incrementIntervalRef.current) clearInterval(incrementIntervalRef.current);
|
|
||||||
|
|
||||||
// First immediate adjustment
|
|
||||||
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
|
|
||||||
|
|
||||||
// Setup continuous adjustment
|
|
||||||
incrementIntervalRef.current = setInterval(() => {
|
|
||||||
if (iosVideoRef) {
|
|
||||||
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
|
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stop continuous increment
|
// Setup continuous adjustment
|
||||||
const stopIncrement = () => {
|
incrementIntervalRef.current = setInterval(() => {
|
||||||
if (incrementIntervalRef.current) {
|
if (iosVideoRef) {
|
||||||
clearInterval(incrementIntervalRef.current);
|
iosVideoRef.currentTime = Math.min(iosVideoRef.duration, iosVideoRef.currentTime + 0.05);
|
||||||
incrementIntervalRef.current = null;
|
}
|
||||||
}
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start continuous 50ms decrement when button is held
|
// Stop continuous increment
|
||||||
const startDecrement = (e: React.MouseEvent | React.TouchEvent) => {
|
const stopIncrement = () => {
|
||||||
// Prevent default to avoid text selection
|
if (incrementIntervalRef.current) {
|
||||||
e.preventDefault();
|
clearInterval(incrementIntervalRef.current);
|
||||||
|
incrementIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!iosVideoRef) return;
|
// Start continuous 50ms decrement when button is held
|
||||||
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
|
const startDecrement = (e: React.MouseEvent | React.TouchEvent) => {
|
||||||
|
// Prevent default to avoid text selection
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
// First immediate adjustment
|
if (!iosVideoRef) return;
|
||||||
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
|
if (decrementIntervalRef.current) clearInterval(decrementIntervalRef.current);
|
||||||
|
|
||||||
// Setup continuous adjustment
|
// First immediate adjustment
|
||||||
decrementIntervalRef.current = setInterval(() => {
|
|
||||||
if (iosVideoRef) {
|
|
||||||
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
|
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Stop continuous decrement
|
// Setup continuous adjustment
|
||||||
const stopDecrement = () => {
|
decrementIntervalRef.current = setInterval(() => {
|
||||||
if (decrementIntervalRef.current) {
|
if (iosVideoRef) {
|
||||||
clearInterval(decrementIntervalRef.current);
|
iosVideoRef.currentTime = Math.max(0, iosVideoRef.currentTime - 0.05);
|
||||||
decrementIntervalRef.current = null;
|
}
|
||||||
}
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
// Stop continuous decrement
|
||||||
<div className="ios-video-player-container">
|
const stopDecrement = () => {
|
||||||
{/* Current Time / Duration Display */}
|
if (decrementIntervalRef.current) {
|
||||||
<div className="ios-time-display mb-2">
|
clearInterval(decrementIntervalRef.current);
|
||||||
<span className="text-sm">
|
decrementIntervalRef.current = null;
|
||||||
{formatTime(currentTime)} / {formatTime(duration)}
|
}
|
||||||
</span>
|
};
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* iOS-optimized Video Element with Native Controls */}
|
return (
|
||||||
<video
|
<div className="ios-video-player-container">
|
||||||
ref={(ref) => setIosVideoRef(ref)}
|
{/* Current Time / Duration Display */}
|
||||||
className="w-full rounded-md"
|
<div className="ios-time-display mb-2">
|
||||||
src={videoUrl}
|
<span className="text-sm">
|
||||||
controls
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
playsInline
|
</span>
|
||||||
webkit-playsinline="true"
|
</div>
|
||||||
x-webkit-airplay="allow"
|
|
||||||
preload="auto"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
>
|
|
||||||
<source src={videoUrl} type="video/mp4" />
|
|
||||||
<p>Your browser doesn't support HTML5 video.</p>
|
|
||||||
</video>
|
|
||||||
|
|
||||||
{/* iOS Video Skip Controls */}
|
{/* iOS-optimized Video Element with Native Controls */}
|
||||||
<div className="ios-skip-controls mt-3 flex justify-center gap-4">
|
<video
|
||||||
<button
|
ref={(ref) => setIosVideoRef(ref)}
|
||||||
onClick={jumpBackward15}
|
className="w-full rounded-md"
|
||||||
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
|
src={videoUrl}
|
||||||
>
|
controls
|
||||||
-15s
|
playsInline
|
||||||
</button>
|
webkit-playsinline="true"
|
||||||
<button
|
x-webkit-airplay="allow"
|
||||||
onClick={jumpForward15}
|
preload="auto"
|
||||||
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
|
crossOrigin="anonymous"
|
||||||
>
|
>
|
||||||
+15s
|
<source src={videoUrl} type="video/mp4" />
|
||||||
</button>
|
<p>Your browser doesn't support HTML5 video.</p>
|
||||||
</div>
|
</video>
|
||||||
|
|
||||||
{/* iOS Fine Control Buttons */}
|
{/* iOS Video Skip Controls */}
|
||||||
<div className="ios-fine-controls mt-2 flex justify-center gap-4">
|
<div className="ios-skip-controls mt-3 flex justify-center gap-4">
|
||||||
<button
|
<button
|
||||||
onMouseDown={startDecrement}
|
onClick={jumpBackward15}
|
||||||
onTouchStart={startDecrement}
|
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
|
||||||
onMouseUp={stopDecrement}
|
>
|
||||||
onMouseLeave={stopDecrement}
|
-15s
|
||||||
onTouchEnd={stopDecrement}
|
</button>
|
||||||
onTouchCancel={stopDecrement}
|
<button
|
||||||
className="ios-control-btn flex items-center justify-center bg-indigo-600 text-white py-2 px-4 rounded-md no-select"
|
onClick={jumpForward15}
|
||||||
>
|
className="ios-control-btn flex items-center justify-center bg-purple-500 text-white py-2 px-4 rounded-md"
|
||||||
-50ms
|
>
|
||||||
</button>
|
+15s
|
||||||
<button
|
</button>
|
||||||
onMouseDown={startIncrement}
|
</div>
|
||||||
onTouchStart={startIncrement}
|
|
||||||
onMouseUp={stopIncrement}
|
|
||||||
onMouseLeave={stopIncrement}
|
|
||||||
onTouchEnd={stopIncrement}
|
|
||||||
onTouchCancel={stopIncrement}
|
|
||||||
className="ios-control-btn flex items-center justify-center bg-indigo-600 text-white py-2 px-4 rounded-md no-select"
|
|
||||||
>
|
|
||||||
+50ms
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ios-note mt-2 text-xs text-gray-500">
|
{/* iOS Fine Control Buttons */}
|
||||||
<p>This player uses native iOS controls for better compatibility with iOS devices.</p>
|
<div className="ios-fine-controls mt-2 flex justify-center gap-4">
|
||||||
</div>
|
<button
|
||||||
</div>
|
onMouseDown={startDecrement}
|
||||||
);
|
onTouchStart={startDecrement}
|
||||||
|
onMouseUp={stopDecrement}
|
||||||
|
onMouseLeave={stopDecrement}
|
||||||
|
onTouchEnd={stopDecrement}
|
||||||
|
onTouchCancel={stopDecrement}
|
||||||
|
className="ios-control-btn flex items-center justify-center bg-indigo-600 text-white py-2 px-4 rounded-md no-select"
|
||||||
|
>
|
||||||
|
-50ms
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onMouseDown={startIncrement}
|
||||||
|
onTouchStart={startIncrement}
|
||||||
|
onMouseUp={stopIncrement}
|
||||||
|
onMouseLeave={stopIncrement}
|
||||||
|
onTouchEnd={stopIncrement}
|
||||||
|
onTouchCancel={stopIncrement}
|
||||||
|
className="ios-control-btn flex items-center justify-center bg-indigo-600 text-white py-2 px-4 rounded-md no-select"
|
||||||
|
>
|
||||||
|
+50ms
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IOSVideoPlayer;
|
export default IOSVideoPlayer;
|
||||||
|
|||||||
@ -1,74 +1,74 @@
|
|||||||
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;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions }) => {
|
const Modal: React.FC<ModalProps> = ({ isOpen, onClose, title, children, actions }) => {
|
||||||
// Close modal when Escape key is pressed
|
// 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);
|
||||||
|
|
||||||
|
// Disable body scrolling when modal is open
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleEscapeKey);
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
// Handle click outside the modal content to close it
|
||||||
|
const handleClickOutside = (event: React.MouseEvent) => {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", handleEscapeKey);
|
return (
|
||||||
|
<div className="modal-overlay" onClick={handleClickOutside}>
|
||||||
|
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2 className="modal-title">{title}</h2>
|
||||||
|
<button className="modal-close-button" onClick={onClose} aria-label="Close modal">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
// Disable body scrolling when modal is open
|
<div className="modal-content">{children}</div>
|
||||||
if (isOpen) {
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
{actions && <div className="modal-actions">{actions}</div>}
|
||||||
document.removeEventListener("keydown", handleEscapeKey);
|
</div>
|
||||||
document.body.style.overflow = "";
|
|
||||||
};
|
|
||||||
}, [isOpen, onClose]);
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
// Handle click outside the modal content to close it
|
|
||||||
const handleClickOutside = (event: React.MouseEvent) => {
|
|
||||||
if (event.target === event.currentTarget) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="modal-overlay" onClick={handleClickOutside}>
|
|
||||||
<div className="modal-container" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="modal-header">
|
|
||||||
<h2 className="modal-title">{title}</h2>
|
|
||||||
<button className="modal-close-button" onClick={onClose} aria-label="Close modal">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
||||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<div className="modal-content">{children}</div>
|
|
||||||
|
|
||||||
{actions && <div className="modal-actions">{actions}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Modal;
|
export default Modal;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,452 +1,469 @@
|
|||||||
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>;
|
||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
onPlayPause: () => void;
|
onPlayPause: () => void;
|
||||||
onSeek: (time: number) => void;
|
onSeek: (time: number) => void;
|
||||||
onToggleMute?: () => void;
|
onToggleMute?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||||
videoRef,
|
videoRef,
|
||||||
currentTime,
|
currentTime,
|
||||||
duration,
|
duration,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
isMuted = false,
|
isMuted = false,
|
||||||
onPlayPause,
|
onPlayPause,
|
||||||
onSeek,
|
onSeek,
|
||||||
onToggleMute
|
onToggleMute,
|
||||||
}) => {
|
}) => {
|
||||||
const progressRef = useRef<HTMLDivElement>(null);
|
const progressRef = useRef<HTMLDivElement>(null);
|
||||||
const [isIOS, setIsIOS] = useState(false);
|
const [isIOS, setIsIOS] = useState(false);
|
||||||
const [hasInitialized, setHasInitialized] = useState(false);
|
const [hasInitialized, setHasInitialized] = useState(false);
|
||||||
const [lastPosition, setLastPosition] = useState<number | null>(null);
|
const [lastPosition, setLastPosition] = useState<number | null>(null);
|
||||||
const [isDraggingProgress, setIsDraggingProgress] = useState(false);
|
const [isDraggingProgress, setIsDraggingProgress] = useState(false);
|
||||||
const isDraggingProgressRef = useRef(false);
|
const isDraggingProgressRef = useRef(false);
|
||||||
const [tooltipPosition, setTooltipPosition] = useState({ x: 0 });
|
const [tooltipPosition, setTooltipPosition] = useState({
|
||||||
const [tooltipTime, setTooltipTime] = useState(0);
|
x: 0,
|
||||||
|
});
|
||||||
|
const [tooltipTime, setTooltipTime] = useState(0);
|
||||||
|
|
||||||
const sampleVideoUrl =
|
const sampleVideoUrl =
|
||||||
(typeof window !== "undefined" && (window as any).MEDIA_DATA?.videoUrl) ||
|
(typeof window !== 'undefined' && (window as any).MEDIA_DATA?.videoUrl) || '/videos/sample-video.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
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPlaying && !hasInitialized) {
|
|
||||||
setHasInitialized(true);
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem("video_initialized", "true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isPlaying, hasInitialized]);
|
|
||||||
|
|
||||||
// Add iOS-specific attributes to prevent fullscreen playback
|
|
||||||
useEffect(() => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (!video) return;
|
|
||||||
|
|
||||||
// These attributes need to be set directly on the DOM element
|
|
||||||
// for iOS Safari to respect inline playback
|
|
||||||
video.setAttribute("playsinline", "true");
|
|
||||||
video.setAttribute("webkit-playsinline", "true");
|
|
||||||
video.setAttribute("x-webkit-airplay", "allow");
|
|
||||||
|
|
||||||
// Store the last known good position for iOS
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
if (!isDraggingProgressRef.current) {
|
|
||||||
setLastPosition(video.currentTime);
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
window.lastSeekedPosition = video.currentTime;
|
|
||||||
}
|
}
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
|
// Update initialized state when video plays
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPlaying && !hasInitialized) {
|
||||||
|
setHasInitialized(true);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem('video_initialized', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isPlaying, hasInitialized]);
|
||||||
|
|
||||||
|
// Add iOS-specific attributes to prevent fullscreen playback
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
// These attributes need to be set directly on the DOM element
|
||||||
|
// for iOS Safari to respect inline playback
|
||||||
|
video.setAttribute('playsinline', 'true');
|
||||||
|
video.setAttribute('webkit-playsinline', 'true');
|
||||||
|
video.setAttribute('x-webkit-airplay', 'allow');
|
||||||
|
|
||||||
|
// Store the last known good position for iOS
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (!isDraggingProgressRef.current) {
|
||||||
|
setLastPosition(video.currentTime);
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.lastSeekedPosition = video.currentTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle iOS-specific play/pause state
|
||||||
|
const handlePlay = () => {
|
||||||
|
logger.debug('Video play event fired');
|
||||||
|
if (isIOS) {
|
||||||
|
setHasInitialized(true);
|
||||||
|
localStorage.setItem('video_initialized', 'true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
logger.debug('Video pause event fired');
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
video.addEventListener('play', handlePlay);
|
||||||
|
video.addEventListener('pause', handlePause);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('timeupdate', handleTimeUpdate);
|
||||||
|
video.removeEventListener('play', handlePlay);
|
||||||
|
video.removeEventListener('pause', handlePause);
|
||||||
|
};
|
||||||
|
}, [videoRef, isIOS, isDraggingProgressRef]);
|
||||||
|
|
||||||
|
// Save current time to lastPosition when it changes (from external seeking)
|
||||||
|
useEffect(() => {
|
||||||
|
setLastPosition(currentTime);
|
||||||
|
}, [currentTime]);
|
||||||
|
|
||||||
|
// Jump 10 seconds forward
|
||||||
|
const handleForward = () => {
|
||||||
|
const newTime = Math.min(currentTime + 10, duration);
|
||||||
|
onSeek(newTime);
|
||||||
|
setLastPosition(newTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle iOS-specific play/pause state
|
// Jump 10 seconds backward
|
||||||
const handlePlay = () => {
|
const handleBackward = () => {
|
||||||
logger.debug("Video play event fired");
|
const newTime = Math.max(currentTime - 10, 0);
|
||||||
if (isIOS) {
|
onSeek(newTime);
|
||||||
setHasInitialized(true);
|
setLastPosition(newTime);
|
||||||
localStorage.setItem("video_initialized", "true");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePause = () => {
|
// Calculate progress percentage
|
||||||
logger.debug("Video pause event fired");
|
const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;
|
||||||
|
|
||||||
|
// Handle start of progress bar dragging
|
||||||
|
const handleProgressDragStart = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setIsDraggingProgress(true);
|
||||||
|
isDraggingProgressRef.current = true;
|
||||||
|
|
||||||
|
// Get initial position
|
||||||
|
handleProgressDrag(e);
|
||||||
|
|
||||||
|
// Set up document-level event listeners for mouse movement and release
|
||||||
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
if (isDraggingProgressRef.current) {
|
||||||
|
handleProgressDrag(moveEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsDraggingProgress(false);
|
||||||
|
isDraggingProgressRef.current = false;
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
video.addEventListener("timeupdate", handleTimeUpdate);
|
// Handle progress dragging for both mouse and touch events
|
||||||
video.addEventListener("play", handlePlay);
|
const handleProgressDrag = (e: MouseEvent | React.MouseEvent) => {
|
||||||
video.addEventListener("pause", handlePause);
|
if (!progressRef.current) return;
|
||||||
|
|
||||||
return () => {
|
const rect = progressRef.current.getBoundingClientRect();
|
||||||
video.removeEventListener("timeupdate", handleTimeUpdate);
|
const clickPosition = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
||||||
video.removeEventListener("play", handlePlay);
|
const seekTime = duration * clickPosition;
|
||||||
video.removeEventListener("pause", handlePause);
|
|
||||||
};
|
|
||||||
}, [videoRef, isIOS, isDraggingProgressRef]);
|
|
||||||
|
|
||||||
// Save current time to lastPosition when it changes (from external seeking)
|
// Update tooltip position and time
|
||||||
useEffect(() => {
|
setTooltipPosition({
|
||||||
setLastPosition(currentTime);
|
x: e.clientX,
|
||||||
}, [currentTime]);
|
});
|
||||||
|
setTooltipTime(seekTime);
|
||||||
|
|
||||||
// Jump 10 seconds forward
|
// Store position locally for iOS Safari - critical for timeline seeking
|
||||||
const handleForward = () => {
|
setLastPosition(seekTime);
|
||||||
const newTime = Math.min(currentTime + 10, duration);
|
|
||||||
onSeek(newTime);
|
|
||||||
setLastPosition(newTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Jump 10 seconds backward
|
// Also store globally for integration with other components
|
||||||
const handleBackward = () => {
|
if (typeof window !== 'undefined') {
|
||||||
const newTime = Math.max(currentTime - 10, 0);
|
(window as any).lastSeekedPosition = seekTime;
|
||||||
onSeek(newTime);
|
}
|
||||||
setLastPosition(newTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate progress percentage
|
onSeek(seekTime);
|
||||||
const progressPercentage = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
||||||
|
|
||||||
// Handle start of progress bar dragging
|
|
||||||
const handleProgressDragStart = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setIsDraggingProgress(true);
|
|
||||||
isDraggingProgressRef.current = true;
|
|
||||||
|
|
||||||
// Get initial position
|
|
||||||
handleProgressDrag(e);
|
|
||||||
|
|
||||||
// Set up document-level event listeners for mouse movement and release
|
|
||||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
||||||
if (isDraggingProgressRef.current) {
|
|
||||||
handleProgressDrag(moveEvent);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
// Handle touch events for progress bar
|
||||||
setIsDraggingProgress(false);
|
const handleProgressTouchStart = (e: React.TouchEvent) => {
|
||||||
isDraggingProgressRef.current = false;
|
if (!progressRef.current || !e.touches[0]) return;
|
||||||
document.removeEventListener("mousemove", handleMouseMove);
|
e.preventDefault();
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
|
||||||
|
setIsDraggingProgress(true);
|
||||||
|
isDraggingProgressRef.current = true;
|
||||||
|
|
||||||
|
// Get initial position using touch
|
||||||
|
handleProgressTouchMove(e);
|
||||||
|
|
||||||
|
// Set up document-level event listeners for touch movement and release
|
||||||
|
const handleTouchMove = (moveEvent: TouchEvent) => {
|
||||||
|
if (isDraggingProgressRef.current) {
|
||||||
|
handleProgressTouchMove(moveEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
setIsDraggingProgress(false);
|
||||||
|
isDraggingProgressRef.current = false;
|
||||||
|
document.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
document.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
document.removeEventListener('touchcancel', handleTouchEnd);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', handleTouchMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
document.addEventListener('touchend', handleTouchEnd);
|
||||||
|
document.addEventListener('touchcancel', handleTouchEnd);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
// Handle touch dragging on progress bar
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
const handleProgressTouchMove = (e: TouchEvent | React.TouchEvent) => {
|
||||||
};
|
if (!progressRef.current) return;
|
||||||
|
|
||||||
// Handle progress dragging for both mouse and touch events
|
// Get the touch coordinates
|
||||||
const handleProgressDrag = (e: MouseEvent | React.MouseEvent) => {
|
const touch = 'touches' in e ? e.touches[0] : null;
|
||||||
if (!progressRef.current) return;
|
if (!touch) return;
|
||||||
|
|
||||||
const rect = progressRef.current.getBoundingClientRect();
|
e.preventDefault(); // Prevent scrolling while dragging
|
||||||
const clickPosition = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
||||||
const seekTime = duration * clickPosition;
|
|
||||||
|
|
||||||
// Update tooltip position and time
|
const rect = progressRef.current.getBoundingClientRect();
|
||||||
setTooltipPosition({ x: e.clientX });
|
const touchPosition = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
|
||||||
setTooltipTime(seekTime);
|
const seekTime = duration * touchPosition;
|
||||||
|
|
||||||
// Store position locally for iOS Safari - critical for timeline seeking
|
// Update tooltip position and time
|
||||||
setLastPosition(seekTime);
|
setTooltipPosition({
|
||||||
|
x: touch.clientX,
|
||||||
|
});
|
||||||
|
setTooltipTime(seekTime);
|
||||||
|
|
||||||
// Also store globally for integration with other components
|
// Store position for iOS Safari
|
||||||
if (typeof window !== "undefined") {
|
setLastPosition(seekTime);
|
||||||
(window as any).lastSeekedPosition = seekTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSeek(seekTime);
|
// Also store globally for integration with other components
|
||||||
};
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).lastSeekedPosition = seekTime;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle touch events for progress bar
|
onSeek(seekTime);
|
||||||
const handleProgressTouchStart = (e: React.TouchEvent) => {
|
|
||||||
if (!progressRef.current || !e.touches[0]) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setIsDraggingProgress(true);
|
|
||||||
isDraggingProgressRef.current = true;
|
|
||||||
|
|
||||||
// Get initial position using touch
|
|
||||||
handleProgressTouchMove(e);
|
|
||||||
|
|
||||||
// Set up document-level event listeners for touch movement and release
|
|
||||||
const handleTouchMove = (moveEvent: TouchEvent) => {
|
|
||||||
if (isDraggingProgressRef.current) {
|
|
||||||
handleProgressTouchMove(moveEvent);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchEnd = () => {
|
// Handle click on progress bar (for non-drag interactions)
|
||||||
setIsDraggingProgress(false);
|
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
isDraggingProgressRef.current = false;
|
// If we're already dragging, don't handle the click
|
||||||
document.removeEventListener("touchmove", handleTouchMove);
|
if (isDraggingProgress) return;
|
||||||
document.removeEventListener("touchend", handleTouchEnd);
|
|
||||||
document.removeEventListener("touchcancel", handleTouchEnd);
|
if (progressRef.current) {
|
||||||
|
const rect = progressRef.current.getBoundingClientRect();
|
||||||
|
const clickPosition = (e.clientX - rect.left) / rect.width;
|
||||||
|
const seekTime = duration * clickPosition;
|
||||||
|
|
||||||
|
// Store position locally for iOS Safari - critical for timeline seeking
|
||||||
|
setLastPosition(seekTime);
|
||||||
|
|
||||||
|
// Also store globally for integration with other components
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
(window as any).lastSeekedPosition = seekTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSeek(seekTime);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("touchmove", handleTouchMove, { passive: false });
|
// Handle toggling fullscreen
|
||||||
document.addEventListener("touchend", handleTouchEnd);
|
const handleFullscreen = () => {
|
||||||
document.addEventListener("touchcancel", handleTouchEnd);
|
if (videoRef.current) {
|
||||||
};
|
if (document.fullscreenElement) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else {
|
||||||
|
videoRef.current.requestFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Handle touch dragging on progress bar
|
// Handle click on video to play/pause
|
||||||
const handleProgressTouchMove = (e: TouchEvent | React.TouchEvent) => {
|
const handleVideoClick = () => {
|
||||||
if (!progressRef.current) return;
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
// Get the touch coordinates
|
// If the video is paused, we want to play it
|
||||||
const touch = "touches" in e ? e.touches[0] : null;
|
if (video.paused) {
|
||||||
if (!touch) return;
|
// For iOS Safari: Before playing, explicitly seek to the remembered position
|
||||||
|
if (isIOS && lastPosition !== null && lastPosition > 0) {
|
||||||
|
logger.debug('iOS: Explicitly setting position before play:', lastPosition);
|
||||||
|
|
||||||
e.preventDefault(); // Prevent scrolling while dragging
|
// First, seek to the position
|
||||||
|
video.currentTime = lastPosition;
|
||||||
|
|
||||||
const rect = progressRef.current.getBoundingClientRect();
|
// Use a small timeout to ensure seeking is complete before play
|
||||||
const touchPosition = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
|
setTimeout(() => {
|
||||||
const seekTime = duration * touchPosition;
|
if (videoRef.current) {
|
||||||
|
// Try to play with proper promise handling
|
||||||
|
videoRef.current
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
logger.debug(
|
||||||
|
'iOS: Play started successfully at position:',
|
||||||
|
videoRef.current?.currentTime
|
||||||
|
);
|
||||||
|
onPlayPause(); // Update parent state after successful play
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('iOS: Error playing video:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
// Normal play (non-iOS or no remembered position)
|
||||||
|
video
|
||||||
|
.play()
|
||||||
|
.then(() => {
|
||||||
|
logger.debug('Normal: Play started successfully');
|
||||||
|
onPlayPause(); // Update parent state after successful play
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Error playing video:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If playing, pause and update state
|
||||||
|
video.pause();
|
||||||
|
onPlayPause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update tooltip position and time
|
return (
|
||||||
setTooltipPosition({ x: touch.clientX });
|
<div className="video-player-container">
|
||||||
setTooltipTime(seekTime);
|
<video
|
||||||
|
ref={videoRef}
|
||||||
// Store position for iOS Safari
|
preload="auto"
|
||||||
setLastPosition(seekTime);
|
crossOrigin="anonymous"
|
||||||
|
onClick={handleVideoClick}
|
||||||
// Also store globally for integration with other components
|
playsInline
|
||||||
if (typeof window !== "undefined") {
|
webkit-playsinline="true"
|
||||||
(window as any).lastSeekedPosition = seekTime;
|
x-webkit-airplay="allow"
|
||||||
}
|
controls={false}
|
||||||
|
muted={isMuted}
|
||||||
onSeek(seekTime);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle click on progress bar (for non-drag interactions)
|
|
||||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
// If we're already dragging, don't handle the click
|
|
||||||
if (isDraggingProgress) return;
|
|
||||||
|
|
||||||
if (progressRef.current) {
|
|
||||||
const rect = progressRef.current.getBoundingClientRect();
|
|
||||||
const clickPosition = (e.clientX - rect.left) / rect.width;
|
|
||||||
const seekTime = duration * clickPosition;
|
|
||||||
|
|
||||||
// Store position locally for iOS Safari - critical for timeline seeking
|
|
||||||
setLastPosition(seekTime);
|
|
||||||
|
|
||||||
// Also store globally for integration with other components
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
(window as any).lastSeekedPosition = seekTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSeek(seekTime);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle toggling fullscreen
|
|
||||||
const handleFullscreen = () => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
if (document.fullscreenElement) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
} else {
|
|
||||||
videoRef.current.requestFullscreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle click on video to play/pause
|
|
||||||
const handleVideoClick = () => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (!video) return;
|
|
||||||
|
|
||||||
// If the video is paused, we want to play it
|
|
||||||
if (video.paused) {
|
|
||||||
// For iOS Safari: Before playing, explicitly seek to the remembered position
|
|
||||||
if (isIOS && lastPosition !== null && lastPosition > 0) {
|
|
||||||
logger.debug("iOS: Explicitly setting position before play:", lastPosition);
|
|
||||||
|
|
||||||
// First, seek to the position
|
|
||||||
video.currentTime = lastPosition;
|
|
||||||
|
|
||||||
// Use a small timeout to ensure seeking is complete before play
|
|
||||||
setTimeout(() => {
|
|
||||||
if (videoRef.current) {
|
|
||||||
// Try to play with proper promise handling
|
|
||||||
videoRef.current
|
|
||||||
.play()
|
|
||||||
.then(() => {
|
|
||||||
logger.debug(
|
|
||||||
"iOS: Play started successfully at position:",
|
|
||||||
videoRef.current?.currentTime
|
|
||||||
);
|
|
||||||
onPlayPause(); // Update parent state after successful play
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("iOS: Error playing video:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
} else {
|
|
||||||
// Normal play (non-iOS or no remembered position)
|
|
||||||
video
|
|
||||||
.play()
|
|
||||||
.then(() => {
|
|
||||||
logger.debug("Normal: Play started successfully");
|
|
||||||
onPlayPause(); // Update parent state after successful play
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error("Error playing video:", err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If playing, pause and update state
|
|
||||||
video.pause();
|
|
||||||
onPlayPause();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="video-player-container">
|
|
||||||
<video
|
|
||||||
ref={videoRef}
|
|
||||||
preload="auto"
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
onClick={handleVideoClick}
|
|
||||||
playsInline
|
|
||||||
webkit-playsinline="true"
|
|
||||||
x-webkit-airplay="allow"
|
|
||||||
controls={false}
|
|
||||||
muted={isMuted}
|
|
||||||
>
|
|
||||||
<source src={sampleVideoUrl} type="video/mp4" />
|
|
||||||
<p>Your browser doesn't support HTML5 video.</p>
|
|
||||||
</video>
|
|
||||||
|
|
||||||
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}
|
|
||||||
{isIOS && !hasInitialized && !isPlaying && (
|
|
||||||
<div className="ios-first-play-indicator">
|
|
||||||
<div className="ios-play-message">Tap Play to initialize video controls</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Play/Pause Indicator (shows based on current state) */}
|
|
||||||
<div className={`play-pause-indicator ${isPlaying ? "pause-icon" : "play-icon"}`}></div>
|
|
||||||
|
|
||||||
{/* Video Controls Overlay */}
|
|
||||||
<div className="video-controls">
|
|
||||||
{/* Time and Duration */}
|
|
||||||
<div className="video-time-display">
|
|
||||||
<span className="video-current-time">{formatTime(currentTime)}</span>
|
|
||||||
<span className="video-duration">/ {formatTime(duration)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar with enhanced dragging */}
|
|
||||||
<div
|
|
||||||
ref={progressRef}
|
|
||||||
className={`video-progress ${isDraggingProgress ? "dragging" : ""}`}
|
|
||||||
onClick={handleProgressClick}
|
|
||||||
onMouseDown={handleProgressDragStart}
|
|
||||||
onTouchStart={handleProgressTouchStart}
|
|
||||||
>
|
|
||||||
<div className="video-progress-fill" style={{ width: `${progressPercentage}%` }}></div>
|
|
||||||
<div className="video-scrubber" style={{ left: `${progressPercentage}%` }}></div>
|
|
||||||
|
|
||||||
{/* Floating time tooltip when dragging */}
|
|
||||||
{isDraggingProgress && (
|
|
||||||
<div
|
|
||||||
className="video-time-tooltip"
|
|
||||||
style={{
|
|
||||||
left: `${tooltipPosition.x}px`,
|
|
||||||
transform: "translateX(-50%)"
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{formatDetailedTime(tooltipTime)}
|
<source src={sampleVideoUrl} type="video/mp4" />
|
||||||
|
<p>Your browser doesn't support HTML5 video.</p>
|
||||||
|
</video>
|
||||||
|
|
||||||
|
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}
|
||||||
|
{isIOS && !hasInitialized && !isPlaying && (
|
||||||
|
<div className="ios-first-play-indicator">
|
||||||
|
<div className="ios-play-message">Tap Play to initialize video controls</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Play/Pause Indicator (shows based on current state) */}
|
||||||
|
<div className={`play-pause-indicator ${isPlaying ? 'pause-icon' : 'play-icon'}`}></div>
|
||||||
|
|
||||||
|
{/* Video Controls Overlay */}
|
||||||
|
<div className="video-controls">
|
||||||
|
{/* Time and Duration */}
|
||||||
|
<div className="video-time-display">
|
||||||
|
<span className="video-current-time">{formatTime(currentTime)}</span>
|
||||||
|
<span className="video-duration">/ {formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar with enhanced dragging */}
|
||||||
|
<div
|
||||||
|
ref={progressRef}
|
||||||
|
className={`video-progress ${isDraggingProgress ? 'dragging' : ''}`}
|
||||||
|
onClick={handleProgressClick}
|
||||||
|
onMouseDown={handleProgressDragStart}
|
||||||
|
onTouchStart={handleProgressTouchStart}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="video-progress-fill"
|
||||||
|
style={{
|
||||||
|
width: `${progressPercentage}%`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="video-scrubber"
|
||||||
|
style={{
|
||||||
|
left: `${progressPercentage}%`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{/* Floating time tooltip when dragging */}
|
||||||
|
{isDraggingProgress && (
|
||||||
|
<div
|
||||||
|
className="video-time-tooltip"
|
||||||
|
style={{
|
||||||
|
left: `${tooltipPosition.x}px`,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatDetailedTime(tooltipTime)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls - Mute and Fullscreen buttons */}
|
||||||
|
<div className="video-controls-buttons">
|
||||||
|
{/* Mute/Unmute Button */}
|
||||||
|
{onToggleMute && (
|
||||||
|
<button
|
||||||
|
className="mute-button"
|
||||||
|
aria-label={isMuted ? 'Unmute' : 'Mute'}
|
||||||
|
onClick={onToggleMute}
|
||||||
|
data-tooltip={isMuted ? 'Unmute' : 'Mute'}
|
||||||
|
>
|
||||||
|
{isMuted ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||||
|
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
|
||||||
|
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
|
||||||
|
<line x1="12" y1="19" x2="12" y2="23"></line>
|
||||||
|
<line x1="8" y1="23" x2="16" y2="23"></line>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
||||||
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fullscreen Button */}
|
||||||
|
<button
|
||||||
|
className="fullscreen-button"
|
||||||
|
aria-label="Fullscreen"
|
||||||
|
onClick={handleFullscreen}
|
||||||
|
data-tooltip="Toggle fullscreen"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
{/* Controls - Mute and Fullscreen buttons */}
|
|
||||||
<div className="video-controls-buttons">
|
|
||||||
{/* Mute/Unmute Button */}
|
|
||||||
{onToggleMute && (
|
|
||||||
<button
|
|
||||||
className="mute-button"
|
|
||||||
aria-label={isMuted ? "Unmute" : "Mute"}
|
|
||||||
onClick={onToggleMute}
|
|
||||||
data-tooltip={isMuted ? "Unmute" : "Mute"}
|
|
||||||
>
|
|
||||||
{isMuted ? (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
|
||||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V4a3 3 0 0 0-5.94-.6"></path>
|
|
||||||
<path d="M17 16.95A7 7 0 0 1 5 12v-2m14 0v2a7 7 0 0 1-.11 1.23"></path>
|
|
||||||
<line x1="12" y1="19" x2="12" y2="23"></line>
|
|
||||||
<line x1="8" y1="23" x2="16" y2="23"></line>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
|
|
||||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fullscreen Button */}
|
|
||||||
<button
|
|
||||||
className="fullscreen-button"
|
|
||||||
aria-label="Fullscreen"
|
|
||||||
onClick={handleFullscreen}
|
|
||||||
data-tooltip="Toggle fullscreen"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M3 4a1 1 0 011-1h4a1 1 0 010 2H6.414l2.293 2.293a1 1 0 01-1.414 1.414L5 6.414V8a1 1 0 01-2 0V4zm9 1a1 1 0 010-2h4a1 1 0 011 1v4a1 1 0 01-2 0V6.414l-2.293 2.293a1 1 0 11-1.414-1.414L13.586 5H12zm-9 7a1 1 0 012 0v1.586l2.293-2.293a1 1 0 011.414 1.414L6.414 15H8a1 1 0 010 2H4a1 1 0 01-1-1v-4zm13-1a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 010-2h1.586l-2.293-2.293a1 1 0 011.414-1.414L15 13.586V12a1 1 0 011-1z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VideoPlayer;
|
export default VideoPlayer;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -3,29 +3,29 @@
|
|||||||
* but always shows errors, warnings, and info messages.
|
* but always shows errors, warnings, and info messages.
|
||||||
*/
|
*/
|
||||||
const logger = {
|
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;
|
||||||
|
|||||||
@ -1,55 +1,51 @@
|
|||||||
import { QueryClient, QueryFunction } from "@tanstack/react-query";
|
import { QueryClient, QueryFunction } from '@tanstack/react-query';
|
||||||
|
|
||||||
async function throwIfResNotOk(res: Response) {
|
async function throwIfResNotOk(res: Response) {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = (await res.text()) || res.statusText;
|
const text = (await res.text()) || res.statusText;
|
||||||
throw new Error(`${res.status}: ${text}`);
|
throw new Error(`${res.status}: ${text}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiRequest(
|
export async function apiRequest(method: string, url: string, data?: unknown | undefined): Promise<Response> {
|
||||||
method: string,
|
const res = await fetch(url, {
|
||||||
url: string,
|
method,
|
||||||
data?: unknown | undefined
|
headers: data ? { 'Content-Type': 'application/json' } : {},
|
||||||
): Promise<Response> {
|
body: data ? JSON.stringify(data) : undefined,
|
||||||
const res = await fetch(url, {
|
credentials: 'include',
|
||||||
method,
|
|
||||||
headers: data ? { "Content-Type": "application/json" } : {},
|
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
|
||||||
credentials: "include"
|
|
||||||
});
|
|
||||||
|
|
||||||
await throwIfResNotOk(res);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnauthorizedBehavior = "returnNull" | "throw";
|
|
||||||
export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> =
|
|
||||||
({ on401: unauthorizedBehavior }) =>
|
|
||||||
async ({ queryKey }) => {
|
|
||||||
const res = await fetch(queryKey[0] as string, {
|
|
||||||
credentials: "include"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (unauthorizedBehavior === "returnNull" && res.status === 401) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await throwIfResNotOk(res);
|
await throwIfResNotOk(res);
|
||||||
return await res.json();
|
return res;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
type UnauthorizedBehavior = 'returnNull' | 'throw';
|
||||||
|
export const getQueryFn: <T>(options: { on401: UnauthorizedBehavior }) => QueryFunction<T> =
|
||||||
|
({ on401: unauthorizedBehavior }) =>
|
||||||
|
async ({ queryKey }) => {
|
||||||
|
const res = await fetch(queryKey[0] as string, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unauthorizedBehavior === 'returnNull' && res.status === 401) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await throwIfResNotOk(res);
|
||||||
|
return await res.json();
|
||||||
|
};
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
queryFn: getQueryFn({ on401: "throw" }),
|
queryFn: getQueryFn({ on401: 'throw' }),
|
||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
retry: false
|
retry: false,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
|
||||||
retry: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,33 +2,33 @@
|
|||||||
* Format seconds to HH:MM:SS.mmm format with millisecond precision
|
* Format seconds to HH:MM:SS.mmm format with millisecond precision
|
||||||
*/
|
*/
|
||||||
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}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format seconds to MM:SS format - now uses the detailed format with hours and milliseconds
|
* Format seconds to MM:SS format - now uses the detailed format with hours and milliseconds
|
||||||
*/
|
*/
|
||||||
export const formatTime = (seconds: number): string => {
|
export const formatTime = (seconds: number): string => {
|
||||||
// Use the detailed format instead of the old MM:SS format
|
// Use the detailed format instead of the old MM:SS format
|
||||||
return formatDetailedTime(seconds);
|
return formatDetailedTime(seconds);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format seconds to HH:MM:SS format - now uses the detailed format with milliseconds
|
* Format seconds to HH:MM:SS format - now uses the detailed format with milliseconds
|
||||||
*/
|
*/
|
||||||
export const formatLongTime = (seconds: number): string => {
|
export const formatLongTime = (seconds: number): string => {
|
||||||
// Use the detailed format
|
// Use the detailed format
|
||||||
return formatDetailedTime(seconds);
|
return formatDetailedTime(seconds);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,45 +3,42 @@
|
|||||||
* Returns a CSS color based on the segment position
|
* Returns a CSS color based on the segment position
|
||||||
*/
|
*/
|
||||||
export const generateSolidColor = (time: number, duration: number): string => {
|
export const generateSolidColor = (time: number, duration: number): string => {
|
||||||
// Use the time position to create different colors
|
// 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}%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy function kept for compatibility
|
* Legacy function kept for compatibility
|
||||||
* 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, time: number): Promise<string> => {
|
||||||
videoElement: HTMLVideoElement,
|
return new Promise((resolve) => {
|
||||||
time: number
|
// Create a small canvas for the solid color
|
||||||
): Promise<string> => {
|
const canvas = document.createElement('canvas');
|
||||||
return new Promise((resolve) => {
|
canvas.width = 10; // Much smaller - we only need a color
|
||||||
// Create a small canvas for the solid color
|
canvas.height = 10;
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = 10; // Much smaller - we only need a color
|
|
||||||
canvas.height = 10;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
if (ctx) {
|
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);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
// API service for video trimming operations
|
// API service for video trimming operations
|
||||||
|
|
||||||
interface TrimVideoRequest {
|
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
|
||||||
@ -22,90 +22,87 @@ 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, data: TrimVideoRequest): Promise<TrimVideoResponse> => {
|
||||||
mediaId: string,
|
try {
|
||||||
data: TrimVideoRequest
|
// Attempt the real API call
|
||||||
): Promise<TrimVideoResponse> => {
|
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
|
||||||
try {
|
method: 'POST',
|
||||||
// Attempt the real API call
|
headers: { 'Content-Type': 'application/json' },
|
||||||
const response = await fetch(`/api/v1/media/${mediaId}/trim_video`, {
|
body: JSON.stringify(data),
|
||||||
method: "POST",
|
});
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
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) {
|
||||||
// Handle 400 Bad Request - return with error details
|
// Handle 400 Bad Request - return with error details
|
||||||
try {
|
try {
|
||||||
// Try to get error details from response
|
// Try to get error details from response
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
error: errorData.error || "An error occurred during processing",
|
error: errorData.error || 'An error occurred during processing',
|
||||||
msg: "Video Processing Error",
|
msg: 'Video Processing Error',
|
||||||
url_redirect: ""
|
url_redirect: '',
|
||||||
};
|
};
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
// If can't parse response JSON, return generic error
|
// If can't parse response JSON, return generic error
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
error: "An error occurred during video processing",
|
error: 'An error occurred during video processing',
|
||||||
msg: "Video Processing Error",
|
msg: 'Video Processing Error',
|
||||||
url_redirect: ""
|
url_redirect: '',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
} else if (response.status !== 404) {
|
||||||
|
// Handle other error responses
|
||||||
|
try {
|
||||||
|
// Try to get error details from response
|
||||||
|
const errorData = await response.json();
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
error: errorData.error || 'An error occurred during processing',
|
||||||
|
msg: 'Video Processing Error',
|
||||||
|
url_redirect: '',
|
||||||
|
};
|
||||||
|
} catch (parseError) {
|
||||||
|
// If can't parse response JSON, return generic error
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
error: 'An error occurred during video processing',
|
||||||
|
msg: 'Video Processing Error',
|
||||||
|
url_redirect: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If endpoint not ready (404), return mock success response
|
||||||
|
await delay(1500); // Simulate 1.5 second server delay
|
||||||
|
return {
|
||||||
|
status: 200, // Mock success status
|
||||||
|
msg: 'Video Processed Successfully', // Updated per requirements
|
||||||
|
url_redirect: `./view?m=${mediaId}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (response.status !== 404) {
|
|
||||||
// Handle other error responses
|
// Successful response
|
||||||
try {
|
const jsonResponse = await response.json();
|
||||||
// Try to get error details from response
|
return {
|
||||||
const errorData = await response.json();
|
status: 200,
|
||||||
return {
|
msg: 'Video Processed Successfully', // Ensure the success message is correct
|
||||||
status: response.status,
|
url_redirect: jsonResponse.url_redirect || `./view?m=${mediaId}`,
|
||||||
error: errorData.error || "An error occurred during processing",
|
...jsonResponse,
|
||||||
msg: "Video Processing Error",
|
};
|
||||||
url_redirect: ""
|
} catch (error) {
|
||||||
};
|
// For any fetch errors, return mock success response with delay
|
||||||
} catch (parseError) {
|
|
||||||
// If can't parse response JSON, return generic error
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
error: "An error occurred during video processing",
|
|
||||||
msg: "Video Processing Error",
|
|
||||||
url_redirect: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If endpoint not ready (404), return mock success response
|
|
||||||
await delay(1500); // Simulate 1.5 second server delay
|
await delay(1500); // Simulate 1.5 second server delay
|
||||||
return {
|
return {
|
||||||
status: 200, // Mock success status
|
status: 200, // Mock success status
|
||||||
msg: "Video Processed Successfully", // Updated per requirements
|
msg: 'Video Processed Successfully', // Consistent with requirements
|
||||||
url_redirect: `./view?m=${mediaId}`
|
url_redirect: `./view?m=${mediaId}`,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Successful response
|
/* Mock implementation that simulates network latency
|
||||||
const jsonResponse = await response.json();
|
|
||||||
return {
|
|
||||||
status: 200,
|
|
||||||
msg: "Video Processed Successfully", // Ensure the success message is correct
|
|
||||||
url_redirect: jsonResponse.url_redirect || `./view?m=${mediaId}`,
|
|
||||||
...jsonResponse
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// For any fetch errors, return mock success response with delay
|
|
||||||
await delay(1500); // Simulate 1.5 second server delay
|
|
||||||
return {
|
|
||||||
status: 200, // Mock success status
|
|
||||||
msg: "Video Processed Successfully", // Consistent with requirements
|
|
||||||
url_redirect: `./view?m=${mediaId}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mock implementation that simulates network latency
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resolve({
|
resolve({
|
||||||
|
|||||||
@ -1,196 +1,196 @@
|
|||||||
#video-editor-trim-root {
|
#video-editor-trim-root {
|
||||||
/* Tooltip styles - only on desktop where hover is available */
|
/* Tooltip styles - only on desktop where hover is available */
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
[data-tooltip] {
|
[data-tooltip] {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:before {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:hover:before,
|
||||||
|
[data-tooltip]:hover:after {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:before {
|
/* Hide button tooltips on touch devices */
|
||||||
content: attr(data-tooltip);
|
@media (pointer: coarse) {
|
||||||
position: absolute;
|
[data-tooltip]:before,
|
||||||
bottom: 100%;
|
[data-tooltip]:after {
|
||||||
left: 50%;
|
display: none !important;
|
||||||
transform: translateX(-50%);
|
content: none !important;
|
||||||
margin-bottom: 5px;
|
opacity: 0 !important;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
visibility: hidden !important;
|
||||||
color: white;
|
pointer-events: none !important;
|
||||||
text-align: center;
|
}
|
||||||
padding: 5px 10px;
|
}
|
||||||
border-radius: 3px;
|
.clip-segments-container {
|
||||||
font-size: 12px;
|
margin-top: 1rem;
|
||||||
white-space: nowrap;
|
background-color: white;
|
||||||
opacity: 0;
|
border-radius: 0.5rem;
|
||||||
visibility: hidden;
|
padding: 1rem;
|
||||||
transition:
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
opacity 0.2s,
|
|
||||||
visibility 0.2s;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
.clip-segments-title {
|
||||||
content: "";
|
font-size: 0.875rem;
|
||||||
position: absolute;
|
font-weight: 500;
|
||||||
bottom: 100%;
|
color: var(--foreground, #333);
|
||||||
left: 50%;
|
margin-bottom: 0.75rem;
|
||||||
transform: translateX(-50%);
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition:
|
|
||||||
opacity 0.2s,
|
|
||||||
visibility 0.2s;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:hover:before,
|
.segment-item {
|
||||||
[data-tooltip]:hover:after {
|
display: flex;
|
||||||
opacity: 1;
|
align-items: center;
|
||||||
visibility: visible;
|
justify-content: space-between;
|
||||||
}
|
padding: 0.5rem;
|
||||||
}
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
|
||||||
/* Hide button tooltips on touch devices */
|
&:hover {
|
||||||
@media (pointer: coarse) {
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
[data-tooltip]:before,
|
}
|
||||||
[data-tooltip]:after {
|
|
||||||
display: none !important;
|
|
||||||
content: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.clip-segments-container {
|
|
||||||
margin-top: 1rem;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.clip-segments-title {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--foreground, #333);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
transition: box-shadow 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-thumbnail {
|
|
||||||
width: 4rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-title {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-time {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-duration {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.segment-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-button {
|
|
||||||
padding: 0.375rem;
|
|
||||||
color: #4b5563;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
border-radius: 9999px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background-color 0.2s,
|
|
||||||
color 0.2s;
|
|
||||||
min-width: auto;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: black;
|
|
||||||
background-color: #d1d5db;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
.segment-content {
|
||||||
height: 1rem;
|
display: flex;
|
||||||
width: 1rem;
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.empty-message {
|
.segment-thumbnail {
|
||||||
padding: 1rem;
|
width: 4rem;
|
||||||
text-align: center;
|
height: 2.25rem;
|
||||||
color: rgba(51, 51, 51, 0.7);
|
background-size: cover;
|
||||||
}
|
background-position: center;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.segment-color-1 {
|
.segment-info {
|
||||||
background-color: rgba(59, 130, 246, 0.15);
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
.segment-color-2 {
|
}
|
||||||
background-color: rgba(16, 185, 129, 0.15);
|
|
||||||
}
|
.segment-title {
|
||||||
.segment-color-3 {
|
font-weight: 500;
|
||||||
background-color: rgba(245, 158, 11, 0.15);
|
font-size: 0.875rem;
|
||||||
}
|
color: black;
|
||||||
.segment-color-4 {
|
}
|
||||||
background-color: rgba(239, 68, 68, 0.15);
|
|
||||||
}
|
.segment-time {
|
||||||
.segment-color-5 {
|
font-size: 0.75rem;
|
||||||
background-color: rgba(139, 92, 246, 0.15);
|
color: black;
|
||||||
}
|
}
|
||||||
.segment-color-6 {
|
|
||||||
background-color: rgba(236, 72, 153, 0.15);
|
.segment-duration {
|
||||||
}
|
font-size: 0.75rem;
|
||||||
.segment-color-7 {
|
margin-top: 0.25rem;
|
||||||
background-color: rgba(6, 182, 212, 0.15);
|
display: inline-block;
|
||||||
}
|
background-color: #f3f4f6;
|
||||||
.segment-color-8 {
|
padding: 0 0.5rem;
|
||||||
background-color: rgba(250, 204, 21, 0.15);
|
border-radius: 0.25rem;
|
||||||
}
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
padding: 0.375rem;
|
||||||
|
color: #4b5563;
|
||||||
|
background-color: #e5e7eb;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
min-width: auto;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: black;
|
||||||
|
background-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-message {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(51, 51, 51, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-color-1 {
|
||||||
|
background-color: rgba(59, 130, 246, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-2 {
|
||||||
|
background-color: rgba(16, 185, 129, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-3 {
|
||||||
|
background-color: rgba(245, 158, 11, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-4 {
|
||||||
|
background-color: rgba(239, 68, 68, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-5 {
|
||||||
|
background-color: rgba(139, 92, 246, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-6 {
|
||||||
|
background-color: rgba(236, 72, 153, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-7 {
|
||||||
|
background-color: rgba(6, 182, 212, 0.15);
|
||||||
|
}
|
||||||
|
.segment-color-8 {
|
||||||
|
background-color: rgba(250, 204, 21, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,397 +1,397 @@
|
|||||||
#video-editor-trim-root {
|
#video-editor-trim-root {
|
||||||
/* Tooltip styles - only on desktop where hover is available */
|
/* Tooltip styles - only on desktop where hover is available */
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
[data-tooltip] {
|
[data-tooltip] {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:before {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:hover:before,
|
||||||
|
[data-tooltip]:hover:after {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:before {
|
/* Hide button tooltips on touch devices */
|
||||||
content: attr(data-tooltip);
|
@media (pointer: coarse) {
|
||||||
position: absolute;
|
[data-tooltip]:before,
|
||||||
bottom: 100%;
|
[data-tooltip]:after {
|
||||||
left: 50%;
|
display: none !important;
|
||||||
transform: translateX(-50%);
|
content: none !important;
|
||||||
margin-bottom: 5px;
|
opacity: 0 !important;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
visibility: hidden !important;
|
||||||
color: white;
|
pointer-events: none !important;
|
||||||
text-align: center;
|
}
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 12px;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition:
|
|
||||||
opacity 0.2s,
|
|
||||||
visibility 0.2s;
|
|
||||||
z-index: 1000;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition:
|
|
||||||
opacity 0.2s,
|
|
||||||
visibility 0.2s;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-tooltip]:hover:before,
|
|
||||||
[data-tooltip]:hover:after {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide button tooltips on touch devices */
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
[data-tooltip]:before,
|
|
||||||
[data-tooltip]:after {
|
|
||||||
display: none !important;
|
|
||||||
content: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing-tools-container {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 2.5rem;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
gap: 15px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container.single-row {
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show full text on larger screens, hide short text */
|
|
||||||
.full-text {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.short-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset text always visible by default */
|
|
||||||
.reset-text {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
&.play-buttons-group {
|
|
||||||
gap: 0.75rem;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex: 0 0 auto; /* Don't expand to fill space */
|
|
||||||
}
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
gap: 0.75rem;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-left: auto; /* Push to right edge */
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #333;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: auto;
|
|
||||||
|
|
||||||
&:hover:not(:disabled) {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
height: 1.25rem;
|
|
||||||
width: 1.25rem;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
border-right: 1px solid #d1d5db;
|
|
||||||
height: 1.5rem;
|
|
||||||
margin: 0 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style for play buttons with highlight effect */
|
|
||||||
.play-button,
|
|
||||||
.preview-button {
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
min-width: 80px;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Greyed out play button when segments are playing */
|
|
||||||
.play-button.greyed-out {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlighted stop button with blue pulse on small screens */
|
|
||||||
.segments-button.highlighted-stop {
|
|
||||||
background-color: rgba(59, 130, 246, 0.1);
|
|
||||||
color: #3b82f6;
|
|
||||||
border: 1px solid #3b82f6;
|
|
||||||
animation: bluePulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bluePulse {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Completely disable ALL hover effects for play buttons */
|
|
||||||
.play-button:hover:not(:disabled),
|
|
||||||
.preview-button:hover:not(:disabled) {
|
|
||||||
/* Reset everything to prevent any changes */
|
|
||||||
color: inherit !important;
|
|
||||||
transform: none !important;
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
width: auto !important;
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-button svg,
|
|
||||||
.preview-button svg {
|
|
||||||
height: 1.5rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
/* Make sure SVG scales with the button but doesn't change layout */
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add responsive button text class */
|
|
||||||
.button-text {
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Media queries for the editing tools */
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
/* Hide text for undo/redo buttons on medium screens */
|
|
||||||
.button-group.secondary .button-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
/* Keep all buttons in a single row, make them more compact */
|
|
||||||
.flex-container.single-row {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keep font size consistent regardless of screen size */
|
|
||||||
.preview-button,
|
|
||||||
.play-button {
|
|
||||||
font-size: 0.875rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
/* Prevent container overflow on mobile */
|
|
||||||
.editing-tools-container {
|
.editing-tools-container {
|
||||||
padding: 0.75rem;
|
background-color: white;
|
||||||
overflow-x: hidden;
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* At this breakpoint, make preview button text shorter */
|
.flex-container {
|
||||||
.preview-button {
|
display: flex;
|
||||||
min-width: auto;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Switch to short text versions */
|
.flex-container.single-row {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show full text on larger screens, hide short text */
|
||||||
.full-text {
|
.full-text {
|
||||||
display: none;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.short-text {
|
.short-text {
|
||||||
display: inline;
|
display: none;
|
||||||
margin-left: 0.15rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide reset text */
|
/* Reset text always visible by default */
|
||||||
.reset-text {
|
.reset-text {
|
||||||
display: none;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure buttons stay in correct position */
|
|
||||||
.button-group.play-buttons-group {
|
|
||||||
flex: initial;
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group.secondary {
|
|
||||||
flex: initial;
|
|
||||||
justify-content: flex-end;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce button sizes on mobile */
|
|
||||||
.button-group button {
|
|
||||||
padding: 0.375rem;
|
|
||||||
min-width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group button svg {
|
|
||||||
height: 1.125rem;
|
|
||||||
width: 1.125rem;
|
|
||||||
margin-right: 0.125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
/* Keep single row, left-align play buttons, right-align controls */
|
|
||||||
.flex-container.single-row {
|
|
||||||
justify-content: space-between;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix left-align for play buttons */
|
|
||||||
.button-group.play-buttons-group {
|
|
||||||
justify-content: flex-start;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix right-align for editing controls */
|
|
||||||
.button-group.secondary {
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce button padding to fit more easily */
|
|
||||||
.button-group button {
|
|
||||||
padding: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
margin: 0 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Very small screens - maintain layout but reduce further */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.editing-tools-container {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container.single-row {
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group.play-buttons-group,
|
|
||||||
.button-group.secondary {
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
display: none; /* Hide divider on very small screens */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Even smaller buttons on very small screens */
|
|
||||||
.button-group button {
|
|
||||||
padding: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group button svg {
|
|
||||||
height: 1rem;
|
|
||||||
width: 1rem;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide all button text on very small screens */
|
|
||||||
.button-text,
|
|
||||||
.reset-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Portrait orientation specific fixes */
|
|
||||||
@media (max-width: 640px) and (orientation: portrait) {
|
|
||||||
.editing-tools-container {
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-container.single-row {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure button groups don't overflow */
|
|
||||||
.button-group {
|
.button-group {
|
||||||
max-width: 50%;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.play-buttons-group {
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 0 0 auto; /* Don't expand to fill space */
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-left: auto; /* Push to right edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #333;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: auto;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group.play-buttons-group {
|
.divider {
|
||||||
max-width: 60%;
|
border-right: 1px solid #d1d5db;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group.secondary {
|
/* Style for play buttons with highlight effect */
|
||||||
max-width: 40%;
|
.play-button,
|
||||||
|
.preview-button {
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 80px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Greyed out play button when segments are playing */
|
||||||
|
.play-button.greyed-out {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlighted stop button with blue pulse on small screens */
|
||||||
|
.segments-button.highlighted-stop {
|
||||||
|
background-color: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1px solid #3b82f6;
|
||||||
|
animation: bluePulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bluePulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Completely disable ALL hover effects for play buttons */
|
||||||
|
.play-button:hover:not(:disabled),
|
||||||
|
.preview-button:hover:not(:disabled) {
|
||||||
|
/* Reset everything to prevent any changes */
|
||||||
|
color: inherit !important;
|
||||||
|
transform: none !important;
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
width: auto !important;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button svg,
|
||||||
|
.preview-button svg {
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
/* Make sure SVG scales with the button but doesn't change layout */
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add responsive button text class */
|
||||||
|
.button-text {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media queries for the editing tools */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
/* Hide text for undo/redo buttons on medium screens */
|
||||||
|
.button-group.secondary .button-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* Keep all buttons in a single row, make them more compact */
|
||||||
|
.flex-container.single-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep font size consistent regardless of screen size */
|
||||||
|
.preview-button,
|
||||||
|
.play-button {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
/* Prevent container overflow on mobile */
|
||||||
|
.editing-tools-container {
|
||||||
|
padding: 0.75rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* At this breakpoint, make preview button text shorter */
|
||||||
|
.preview-button {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Switch to short text versions */
|
||||||
|
.full-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.short-text {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide reset text */
|
||||||
|
.reset-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure buttons stay in correct position */
|
||||||
|
.button-group.play-buttons-group {
|
||||||
|
flex: initial;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group.secondary {
|
||||||
|
flex: initial;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce button sizes on mobile */
|
||||||
|
.button-group button {
|
||||||
|
padding: 0.375rem;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group button svg {
|
||||||
|
height: 1.125rem;
|
||||||
|
width: 1.125rem;
|
||||||
|
margin-right: 0.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
/* Keep single row, left-align play buttons, right-align controls */
|
||||||
|
.flex-container.single-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix left-align for play buttons */
|
||||||
|
.button-group.play-buttons-group {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix right-align for editing controls */
|
||||||
|
.button-group.secondary {
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce button padding to fit more easily */
|
||||||
|
.button-group button {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Very small screens - maintain layout but reduce further */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.editing-tools-container {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-container.single-row {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group.play-buttons-group,
|
||||||
|
.button-group.secondary {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
display: none; /* Hide divider on very small screens */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Even smaller buttons on very small screens */
|
||||||
|
.button-group button {
|
||||||
|
padding: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group button svg {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide all button text on very small screens */
|
||||||
|
.button-text,
|
||||||
|
.reset-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Portrait orientation specific fixes */
|
||||||
|
@media (max-width: 640px) and (orientation: portrait) {
|
||||||
|
.editing-tools-container {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-container.single-row {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure button groups don't overflow */
|
||||||
|
.button-group {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group.play-buttons-group {
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group.secondary {
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,167 +1,167 @@
|
|||||||
.ios-notification {
|
.ios-notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background-color: #fffdeb;
|
background-color: #fffdeb;
|
||||||
border-bottom: 1px solid #e2e2e2;
|
border-bottom: 1px solid #e2e2e2;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
animation: slide-down 0.5s ease-in-out;
|
animation: slide-down 0.5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-down {
|
@keyframes slide-down {
|
||||||
from {
|
from {
|
||||||
transform: translateY(-100%);
|
transform: translateY(-100%);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-content {
|
.ios-notification-content {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-icon {
|
.ios-notification-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #0066cc;
|
color: #0066cc;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-message {
|
.ios-notification-message {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-message h3 {
|
.ios-notification-message h3 {
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #000;
|
color: #000;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-message p {
|
.ios-notification-message p {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-message ol {
|
.ios-notification-message ol {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-message li {
|
.ios-notification-message li {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-close {
|
.ios-notification-close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: #666;
|
color: #666;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-notification-close:hover {
|
.ios-notification-close:hover {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop mode button styling */
|
/* Desktop mode button styling */
|
||||||
.ios-mode-options {
|
.ios-mode-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-desktop-mode-btn {
|
.ios-desktop-mode-btn {
|
||||||
background-color: #0066cc;
|
background-color: #0066cc;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-desktop-mode-btn:hover {
|
.ios-desktop-mode-btn:hover {
|
||||||
background-color: #0055aa;
|
background-color: #0055aa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-desktop-mode-btn:active {
|
.ios-desktop-mode-btn:active {
|
||||||
background-color: #004499;
|
background-color: #004499;
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-or {
|
.ios-or {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin: 0 0 6px 0;
|
margin: 0 0 6px 0;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS-specific styles */
|
/* iOS-specific styles */
|
||||||
@supports (-webkit-touch-callout: none) {
|
@supports (-webkit-touch-callout: none) {
|
||||||
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure this notification has better visibility on smaller screens */
|
/* Make sure this notification has better visibility on smaller screens */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add iOS-specific styles when in desktop mode */
|
/* Add iOS-specific styles when in desktop mode */
|
||||||
html.ios-device {
|
html.ios-device {
|
||||||
/* Force the content to be rendered at desktop width */
|
/* Force the content to be rendered at desktop width */
|
||||||
min-width: 1024px;
|
min-width: 1024px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,96 +1,96 @@
|
|||||||
.mobile-play-prompt-overlay {
|
.mobile-play-prompt-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
-webkit-backdrop-filter: blur(5px);
|
-webkit-backdrop-filter: blur(5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-play-prompt {
|
.mobile-play-prompt {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-play-prompt h3 {
|
.mobile-play-prompt h3 {
|
||||||
margin: 0 0 15px 0;
|
margin: 0 0 15px 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-play-prompt p {
|
.mobile-play-prompt p {
|
||||||
margin: 0 0 15px 0;
|
margin: 0 0 15px 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #444;
|
color: #444;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-prompt-instructions {
|
.mobile-prompt-instructions {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-prompt-instructions p {
|
.mobile-prompt-instructions p {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-prompt-instructions ol {
|
.mobile-prompt-instructions ol {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 22px;
|
padding-left: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-prompt-instructions li {
|
.mobile-prompt-instructions li {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-play-button {
|
.mobile-play-button {
|
||||||
background-color: #007bff;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 12px 25px;
|
padding: 12px 25px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
/* Make button easier to tap on mobile */
|
/* Make button easier to tap on mobile */
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-play-button:hover {
|
.mobile-play-button:hover {
|
||||||
background-color: #0069d9;
|
background-color: #0069d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-play-button:active {
|
.mobile-play-button:active {
|
||||||
background-color: #0062cc;
|
background-color: #0062cc;
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Special styles for mobile devices */
|
/* Special styles for mobile devices */
|
||||||
@supports (-webkit-touch-callout: none) {
|
@supports (-webkit-touch-callout: none) {
|
||||||
.mobile-play-button {
|
.mobile-play-button {
|
||||||
/* Extra spacing for mobile */
|
/* Extra spacing for mobile */
|
||||||
padding: 14px 25px;
|
padding: 14px 25px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,94 +1,94 @@
|
|||||||
.ios-video-player-container {
|
.ios-video-player-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
border: 1px solid #e2e2e2;
|
border: 1px solid #e2e2e2;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-video-player-container video {
|
.ios-video-player-container video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 360px;
|
max-height: 360px;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-time-display {
|
.ios-time-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-note {
|
.ios-note {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #777;
|
color: #777;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS-specific styling tweaks */
|
/* iOS-specific styling tweaks */
|
||||||
@supports (-webkit-touch-callout: none) {
|
@supports (-webkit-touch-callout: none) {
|
||||||
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* External controls styling */
|
/* External controls styling */
|
||||||
.ios-external-controls {
|
.ios-external-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-control-btn {
|
.ios-control-btn {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
height: 44px; /* Minimum touch target size for iOS */
|
height: 44px; /* Minimum touch target size for iOS */
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
-webkit-tap-highlight-color: transparent; /* Remove tap highlight on iOS */
|
-webkit-tap-highlight-color: transparent; /* Remove tap highlight on iOS */
|
||||||
}
|
}
|
||||||
|
|
||||||
.ios-control-btn:active {
|
.ios-control-btn:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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,306 +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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
.modal-button-primary {
|
||||||
color: #f44336;
|
background-color: #0066cc;
|
||||||
font-weight: 500;
|
color: white;
|
||||||
background-color: rgba(244, 67, 54, 0.1);
|
}
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 4px solid #f44336;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.redirect-message {
|
.modal-button-primary:hover {
|
||||||
margin-top: 20px;
|
background-color: #0055aa;
|
||||||
color: #555;
|
}
|
||||||
font-size: 0.95rem;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countdown {
|
.modal-button-secondary {
|
||||||
font-weight: bold;
|
background-color: #f0f0f0;
|
||||||
color: #0066cc;
|
color: #333;
|
||||||
font-size: 1.1rem;
|
}
|
||||||
}
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,70 +1,70 @@
|
|||||||
.two-row-tooltip {
|
.two-row-tooltip {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 3000; /* Highest z-index to ensure it's above all other elements */
|
z-index: 3000; /* Highest z-index to ensure it's above all other elements */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide ±100ms buttons for more compact tooltip */
|
/* Hide ±100ms buttons for more compact tooltip */
|
||||||
.tooltip-time-btn[data-tooltip="Decrease by 100ms"],
|
.tooltip-time-btn[data-tooltip="Decrease by 100ms"],
|
||||||
.tooltip-time-btn[data-tooltip="Increase by 100ms"] {
|
.tooltip-time-btn[data-tooltip="Increase by 100ms"] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-row {
|
.tooltip-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-row:first-child {
|
.tooltip-row:first-child {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-time-btn {
|
.tooltip-time-btn {
|
||||||
background-color: #f0f0f0 !important;
|
background-color: #f0f0f0 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
padding: 4px 8px !important;
|
padding: 4px 8px !important;
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
color: #333 !important;
|
color: #333 !important;
|
||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
transition: background-color 0.2s !important;
|
transition: background-color 0.2s !important;
|
||||||
min-width: 20px !important;
|
min-width: 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-time-btn:hover {
|
.tooltip-time-btn:hover {
|
||||||
background-color: #e0e0e0 !important;
|
background-color: #e0e0e0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-time-display {
|
.tooltip-time-display {
|
||||||
font-family: monospace !important;
|
font-family: monospace !important;
|
||||||
font-size: 0.875rem !important;
|
font-size: 0.875rem !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
color: #333 !important;
|
color: #333 !important;
|
||||||
padding: 4px 6px !important;
|
padding: 4px 6px !important;
|
||||||
background-color: #f7f7f7 !important;
|
background-color: #f7f7f7 !important;
|
||||||
border-radius: 4px !important;
|
border-radius: 4px !important;
|
||||||
min-width: 100px !important;
|
min-width: 100px !important;
|
||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled state for time display */
|
/* Disabled state for time display */
|
||||||
.tooltip-time-display.disabled {
|
.tooltip-time-display.disabled {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
opacity: 0.6 !important;
|
opacity: 0.6 !important;
|
||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
-webkit-user-select: none !important;
|
-webkit-user-select: none !important;
|
||||||
-moz-user-select: none !important;
|
-moz-user-select: none !important;
|
||||||
-ms-user-select: none !important;
|
-ms-user-select: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Force disabled tooltips to show on hover for better user feedback */
|
/* Force disabled tooltips to show on hover for better user feedback */
|
||||||
@ -72,269 +72,269 @@
|
|||||||
.tooltip-time-btn.disabled[data-tooltip]:hover:after,
|
.tooltip-time-btn.disabled[data-tooltip]:hover:after,
|
||||||
.tooltip-action-btn.disabled[data-tooltip]:hover:before,
|
.tooltip-action-btn.disabled[data-tooltip]:hover:before,
|
||||||
.tooltip-action-btn.disabled[data-tooltip]:hover:after {
|
.tooltip-action-btn.disabled[data-tooltip]:hover:after {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-actions {
|
.tooltip-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2500; /* Higher z-index to ensure buttons appear above other elements */
|
z-index: 2500; /* Higher z-index to ensure buttons appear above other elements */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn {
|
.tooltip-action-btn {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
min-width: 20px !important;
|
min-width: 20px !important;
|
||||||
position: relative; /* Add relative positioning for tooltips */
|
position: relative; /* Add relative positioning for tooltips */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom tooltip styles for second row action buttons - positioned below */
|
/* Custom tooltip styles for second row action buttons - positioned below */
|
||||||
.tooltip-action-btn[data-tooltip]:before {
|
.tooltip-action-btn[data-tooltip]:before {
|
||||||
content: attr(data-tooltip);
|
content: attr(data-tooltip);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
top: 35px; /* Position below the button with increased space */
|
top: 35px; /* Position below the button with increased space */
|
||||||
left: 50%; /* Center horizontally */
|
left: 50%; /* Center horizontally */
|
||||||
transform: translateX(-50%); /* Center horizontally */
|
transform: translateX(-50%); /* Center horizontally */
|
||||||
margin-left: 0; /* Reset margin */
|
margin-left: 0; /* Reset margin */
|
||||||
background-color: rgba(0, 0, 0, 0.85);
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
color: white;
|
color: white;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition:
|
transition:
|
||||||
opacity 0.2s,
|
opacity 0.2s,
|
||||||
visibility 0.2s;
|
visibility 0.2s;
|
||||||
z-index: 2500; /* High z-index */
|
z-index: 2500; /* High z-index */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Triangle arrow pointing up to the button */
|
/* Triangle arrow pointing up to the button */
|
||||||
.tooltip-action-btn[data-tooltip]:after {
|
.tooltip-action-btn[data-tooltip]:after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 35px; /* Match the before element */
|
top: 35px; /* Match the before element */
|
||||||
left: 50%; /* Center horizontally */
|
left: 50%; /* Center horizontally */
|
||||||
transform: translateX(-50%); /* Center horizontally */
|
transform: translateX(-50%); /* Center horizontally */
|
||||||
border-width: 4px;
|
border-width: 4px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
/* Arrow pointing down from button to tooltip */
|
/* Arrow pointing down from button to tooltip */
|
||||||
border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent;
|
border-color: rgba(0, 0, 0, 0.85) transparent transparent transparent;
|
||||||
margin-left: 0; /* Reset margin */
|
margin-left: 0; /* Reset margin */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition:
|
transition:
|
||||||
opacity 0.2s,
|
opacity 0.2s,
|
||||||
visibility 0.2s;
|
visibility 0.2s;
|
||||||
z-index: 2500; /* High z-index */
|
z-index: 2500; /* High z-index */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show tooltips on hover - but only on devices with hover capability (desktops) */
|
/* Show tooltips on hover - but only on devices with hover capability (desktops) */
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
.tooltip-action-btn[data-tooltip]:hover:before,
|
.tooltip-action-btn[data-tooltip]:hover:before,
|
||||||
.tooltip-action-btn[data-tooltip]:hover:after {
|
.tooltip-action-btn[data-tooltip]:hover:after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the two-row-tooltip visible but hide button attribute tooltips on touch devices */
|
/* Keep the two-row-tooltip visible but hide button attribute tooltips on touch devices */
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
.tooltip-action-btn[data-tooltip]:before,
|
.tooltip-action-btn[data-tooltip]:before,
|
||||||
.tooltip-action-btn[data-tooltip]:after {
|
.tooltip-action-btn[data-tooltip]:after {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
content: none !important;
|
content: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn:hover {
|
.tooltip-action-btn:hover {
|
||||||
background-color: #e5e7eb;
|
background-color: #e5e7eb;
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.delete {
|
.tooltip-action-btn.delete {
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.delete:hover {
|
.tooltip-action-btn.delete:hover {
|
||||||
background-color: #fee2e2;
|
background-color: #fee2e2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.play {
|
.tooltip-action-btn.play {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.play:hover {
|
.tooltip-action-btn.play:hover {
|
||||||
background-color: #d1fae5;
|
background-color: #d1fae5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.pause {
|
.tooltip-action-btn.pause {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.pause:hover {
|
.tooltip-action-btn.pause:hover {
|
||||||
background-color: #dbeafe;
|
background-color: #dbeafe;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.play-from-start {
|
.tooltip-action-btn.play-from-start {
|
||||||
color: #4f46e5;
|
color: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.play-from-start:hover {
|
.tooltip-action-btn.play-from-start:hover {
|
||||||
background-color: #e0e7ff;
|
background-color: #e0e7ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn svg {
|
.tooltip-action-btn svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust the new segment button style */
|
/* Adjust the new segment button style */
|
||||||
.tooltip-action-btn.new-segment {
|
.tooltip-action-btn.new-segment {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.new-segment:hover {
|
.tooltip-action-btn.new-segment:hover {
|
||||||
background-color: #d1fae5;
|
background-color: #d1fae5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.new-segment .tooltip-btn-text {
|
.tooltip-action-btn.new-segment .tooltip-btn-text {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled state for tooltip action buttons */
|
/* Disabled state for tooltip action buttons */
|
||||||
.tooltip-action-btn.disabled {
|
.tooltip-action-btn.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.disabled:hover {
|
.tooltip-action-btn.disabled:hover {
|
||||||
background-color: #f3f4f6;
|
background-color: #f3f4f6;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.disabled svg {
|
.tooltip-action-btn.disabled svg {
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.disabled .tooltip-btn-text {
|
.tooltip-action-btn.disabled .tooltip-btn-text {
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure pause button is properly styled when disabled */
|
/* Ensure pause button is properly styled when disabled */
|
||||||
.tooltip-action-btn.pause.disabled {
|
.tooltip-action-btn.pause.disabled {
|
||||||
color: #9ca3af !important;
|
color: #9ca3af !important;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.pause.disabled:hover {
|
.tooltip-action-btn.pause.disabled:hover {
|
||||||
background-color: #f3f4f6 !important;
|
background-color: #f3f4f6 !important;
|
||||||
color: #9ca3af !important;
|
color: #9ca3af !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure play button is properly styled when disabled */
|
/* Ensure play button is properly styled when disabled */
|
||||||
.tooltip-action-btn.play.disabled {
|
.tooltip-action-btn.play.disabled {
|
||||||
color: #9ca3af !important;
|
color: #9ca3af !important;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.play.disabled:hover {
|
.tooltip-action-btn.play.disabled:hover {
|
||||||
background-color: #f3f4f6 !important;
|
background-color: #f3f4f6 !important;
|
||||||
color: #9ca3af !important;
|
color: #9ca3af !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure time adjustment buttons are properly styled when disabled */
|
/* Ensure time adjustment buttons are properly styled when disabled */
|
||||||
.tooltip-time-btn.disabled {
|
.tooltip-time-btn.disabled {
|
||||||
opacity: 0.5 !important;
|
opacity: 0.5 !important;
|
||||||
cursor: not-allowed !important;
|
cursor: not-allowed !important;
|
||||||
background-color: #f3f4f6 !important;
|
background-color: #f3f4f6 !important;
|
||||||
color: #9ca3af !important;
|
color: #9ca3af !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-time-btn.disabled:hover {
|
.tooltip-time-btn.disabled:hover {
|
||||||
background-color: #f3f4f6 !important;
|
background-color: #f3f4f6 !important;
|
||||||
color: #9ca3af !important;
|
color: #9ca3af !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Additional mobile optimizations */
|
/* Additional mobile optimizations */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.two-row-tooltip {
|
.two-row-tooltip {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-row:first-child {
|
.tooltip-row:first-child {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-time-btn {
|
.tooltip-time-btn {
|
||||||
min-width: 20px !important;
|
min-width: 20px !important;
|
||||||
font-size: 0.7rem !important;
|
font-size: 0.7rem !important;
|
||||||
padding: 3px 6px !important;
|
padding: 3px 6px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-time-display {
|
.tooltip-time-display {
|
||||||
font-size: 0.8rem !important;
|
font-size: 0.8rem !important;
|
||||||
padding: 3px 4px !important;
|
padding: 3px 4px !important;
|
||||||
min-width: 90px !important;
|
min-width: 90px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn {
|
.tooltip-action-btn {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn.new-segment {
|
.tooltip-action-btn.new-segment {
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn svg {
|
.tooltip-action-btn svg {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust tooltip position for small screens - maintain the same position but adjust size */
|
/* Adjust tooltip position for small screens - maintain the same position but adjust size */
|
||||||
.tooltip-action-btn[data-tooltip]:before {
|
.tooltip-action-btn[data-tooltip]:before {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
top: 33px; /* Maintain the same relative distance on mobile */
|
top: 33px; /* Maintain the same relative distance on mobile */
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-action-btn[data-tooltip]:after {
|
.tooltip-action-btn[data-tooltip]:after {
|
||||||
top: 33px; /* Match the tooltip position */
|
top: 33px; /* Match the tooltip position */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,342 +1,342 @@
|
|||||||
#video-editor-trim-root {
|
#video-editor-trim-root {
|
||||||
/* Tooltip styles - only on desktop where hover is available */
|
/* Tooltip styles - only on desktop where hover is available */
|
||||||
@media (hover: hover) and (pointer: fine) {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
[data-tooltip] {
|
[data-tooltip] {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:before {
|
||||||
|
content: attr(data-tooltip);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-width: 5px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
visibility 0.2s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tooltip]:hover:before,
|
||||||
|
[data-tooltip]:hover:after {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:before {
|
/* Hide button tooltips on touch devices */
|
||||||
content: attr(data-tooltip);
|
@media (pointer: coarse) {
|
||||||
position: absolute;
|
[data-tooltip]:before,
|
||||||
bottom: 100%;
|
[data-tooltip]:after {
|
||||||
left: 50%;
|
display: none !important;
|
||||||
transform: translateX(-50%);
|
content: none !important;
|
||||||
margin-bottom: 5px;
|
opacity: 0 !important;
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
visibility: hidden !important;
|
||||||
color: white;
|
pointer-events: none !important;
|
||||||
text-align: center;
|
}
|
||||||
padding: 5px 10px;
|
}
|
||||||
border-radius: 3px;
|
.video-player-container {
|
||||||
font-size: 12px;
|
position: relative;
|
||||||
white-space: nowrap;
|
width: 100%;
|
||||||
opacity: 0;
|
background: #000;
|
||||||
visibility: hidden;
|
border-radius: 0.5rem;
|
||||||
transition:
|
overflow: hidden;
|
||||||
opacity 0.2s,
|
margin-bottom: 1rem;
|
||||||
visibility 0.2s;
|
aspect-ratio: 16/9;
|
||||||
z-index: 1000;
|
/* Prevent iOS Safari from showing default video controls */
|
||||||
pointer-events: none;
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-tooltip]:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 100%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: rgba(0, 0, 0, 0.8) transparent transparent transparent;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition:
|
|
||||||
opacity 0.2s,
|
|
||||||
visibility 0.2s;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-tooltip]:hover:before,
|
|
||||||
[data-tooltip]:hover:after {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide button tooltips on touch devices */
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
[data-tooltip]:before,
|
|
||||||
[data-tooltip]:after {
|
|
||||||
display: none !important;
|
|
||||||
content: none !important;
|
|
||||||
opacity: 0 !important;
|
|
||||||
visibility: hidden !important;
|
|
||||||
pointer-events: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.video-player-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
background: #000;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
aspect-ratio: 16/9;
|
|
||||||
/* Prevent iOS Safari from showing default video controls */
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-player-container video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
/* Force hardware acceleration */
|
|
||||||
transform: translateZ(0);
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
/* Prevent iOS Safari from showing default video controls */
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS-specific styles */
|
|
||||||
@supports (-webkit-touch-callout: none) {
|
|
||||||
.video-player-container video {
|
.video-player-container video {
|
||||||
/* Additional iOS optimizations */
|
width: 100%;
|
||||||
-webkit-tap-highlight-color: transparent;
|
height: 100%;
|
||||||
-webkit-touch-callout: none;
|
cursor: pointer;
|
||||||
|
/* Force hardware acceleration */
|
||||||
|
transform: translateZ(0);
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
/* Prevent iOS Safari from showing default video controls */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause-indicator {
|
/* iOS-specific styles */
|
||||||
position: absolute;
|
@supports (-webkit-touch-callout: none) {
|
||||||
top: 50%;
|
.video-player-container video {
|
||||||
left: 50%;
|
/* Additional iOS optimizations */
|
||||||
transform: translate(-50%, -50%);
|
-webkit-tap-highlight-color: transparent;
|
||||||
width: 60px;
|
-webkit-touch-callout: none;
|
||||||
height: 60px;
|
}
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-player-container:hover .play-pause-indicator {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause-indicator::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause-indicator.play-icon::before {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-top: 15px solid transparent;
|
|
||||||
border-bottom: 15px solid transparent;
|
|
||||||
border-left: 25px solid white;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause-indicator.pause-icon::before {
|
|
||||||
width: 20px;
|
|
||||||
height: 25px;
|
|
||||||
border-left: 6px solid white;
|
|
||||||
border-right: 6px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS First-play indicator */
|
|
||||||
.ios-first-play-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ios-play-message {
|
|
||||||
color: white;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 0.7;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
50% {
|
|
||||||
opacity: 1;
|
.play-pause-indicator {
|
||||||
transform: scale(1.05);
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
100% {
|
|
||||||
opacity: 0.7;
|
.video-player-container:hover .play-pause-indicator {
|
||||||
transform: scale(1);
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.video-controls {
|
.play-pause-indicator::before {
|
||||||
position: absolute;
|
content: "";
|
||||||
bottom: 0;
|
position: absolute;
|
||||||
left: 0;
|
top: 50%;
|
||||||
right: 0;
|
left: 50%;
|
||||||
padding: 0.75rem;
|
transform: translate(-50%, -50%);
|
||||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
}
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-player-container:hover .video-controls {
|
.play-pause-indicator.play-icon::before {
|
||||||
opacity: 1;
|
width: 0;
|
||||||
}
|
height: 0;
|
||||||
|
border-top: 15px solid transparent;
|
||||||
|
border-bottom: 15px solid transparent;
|
||||||
|
border-left: 25px solid white;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.video-current-time {
|
.play-pause-indicator.pause-icon::before {
|
||||||
color: white;
|
width: 20px;
|
||||||
font-size: 0.875rem;
|
height: 25px;
|
||||||
}
|
border-left: 6px solid white;
|
||||||
|
border-right: 6px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
.video-duration {
|
/* iOS First-play indicator */
|
||||||
color: white;
|
.ios-first-play-indicator {
|
||||||
font-size: 0.875rem;
|
position: absolute;
|
||||||
}
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.video-time-display {
|
.ios-play-message {
|
||||||
display: flex;
|
color: white;
|
||||||
justify-content: space-between;
|
font-size: 1.2rem;
|
||||||
margin-bottom: 0.5rem;
|
text-align: center;
|
||||||
color: white;
|
padding: 1rem;
|
||||||
font-size: 0.875rem;
|
background: rgba(0, 0, 0, 0.8);
|
||||||
}
|
border-radius: 0.5rem;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
.video-progress {
|
@keyframes pulse {
|
||||||
position: relative;
|
0% {
|
||||||
height: 6px;
|
opacity: 0.7;
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
transform: scale(1);
|
||||||
border-radius: 3px;
|
}
|
||||||
cursor: pointer;
|
50% {
|
||||||
margin: 0 10px;
|
opacity: 1;
|
||||||
touch-action: none; /* Prevent browser handling of drag gestures */
|
transform: scale(1.05);
|
||||||
flex-grow: 1;
|
}
|
||||||
}
|
100% {
|
||||||
|
opacity: 0.7;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.video-progress.dragging {
|
.video-controls {
|
||||||
height: 8px;
|
position: absolute;
|
||||||
}
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
.video-progress-fill {
|
.video-player-container:hover .video-controls {
|
||||||
position: absolute;
|
opacity: 1;
|
||||||
top: 0;
|
}
|
||||||
left: 0;
|
|
||||||
height: 100%;
|
|
||||||
background-color: #ff0000;
|
|
||||||
border-radius: 3px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-scrubber {
|
.video-current-time {
|
||||||
position: absolute;
|
color: white;
|
||||||
top: 50%;
|
font-size: 0.875rem;
|
||||||
transform: translate(-50%, -50%);
|
}
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
background-color: #ff0000;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: grab;
|
|
||||||
transition:
|
|
||||||
transform 0.1s ease,
|
|
||||||
width 0.1s ease,
|
|
||||||
height 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make the scrubber larger when dragging for better control */
|
.video-duration {
|
||||||
.video-progress.dragging .video-scrubber {
|
color: white;
|
||||||
transform: translate(-50%, -50%) scale(1.2);
|
font-size: 0.875rem;
|
||||||
width: 18px;
|
}
|
||||||
height: 18px;
|
|
||||||
cursor: grabbing;
|
.video-time-display {
|
||||||
box-shadow: 0 0 8px rgba(255, 0, 0, 0.6);
|
display: flex;
|
||||||
}
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-progress {
|
||||||
|
position: relative;
|
||||||
|
height: 6px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0 10px;
|
||||||
|
touch-action: none; /* Prevent browser handling of drag gestures */
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-progress.dragging {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-progress-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #ff0000;
|
||||||
|
border-radius: 3px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Enhance for touch devices */
|
|
||||||
@media (pointer: coarse) {
|
|
||||||
.video-scrubber {
|
.video-scrubber {
|
||||||
width: 20px;
|
position: absolute;
|
||||||
height: 20px;
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #ff0000;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: grab;
|
||||||
|
transition:
|
||||||
|
transform 0.1s ease,
|
||||||
|
width 0.1s ease,
|
||||||
|
height 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make the scrubber larger when dragging for better control */
|
||||||
.video-progress.dragging .video-scrubber {
|
.video-progress.dragging .video-scrubber {
|
||||||
width: 24px;
|
transform: translate(-50%, -50%) scale(1.2);
|
||||||
height: 24px;
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: grabbing;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Create a larger invisible touch target */
|
/* Enhance for touch devices */
|
||||||
.video-scrubber:before {
|
@media (pointer: coarse) {
|
||||||
content: "";
|
.video-scrubber {
|
||||||
position: absolute;
|
width: 20px;
|
||||||
top: -10px;
|
height: 20px;
|
||||||
left: -10px;
|
}
|
||||||
right: -10px;
|
|
||||||
bottom: -10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-controls-buttons {
|
.video-progress.dragging .video-scrubber {
|
||||||
display: flex;
|
width: 24px;
|
||||||
align-items: center;
|
height: 24px;
|
||||||
justify-content: flex-end;
|
}
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mute-button,
|
/* Create a larger invisible touch target */
|
||||||
.fullscreen-button {
|
.video-scrubber:before {
|
||||||
min-width: auto;
|
content: "";
|
||||||
color: white;
|
position: absolute;
|
||||||
background: none;
|
top: -10px;
|
||||||
border: none;
|
left: -10px;
|
||||||
cursor: pointer;
|
right: -10px;
|
||||||
padding: 0.25rem;
|
bottom: -10px;
|
||||||
transition: transform 0.2s;
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
.video-controls-buttons {
|
||||||
width: 1.25rem;
|
display: flex;
|
||||||
height: 1.25rem;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* Time tooltip that appears when dragging */
|
.mute-button,
|
||||||
.video-time-tooltip {
|
.fullscreen-button {
|
||||||
position: absolute;
|
min-width: auto;
|
||||||
top: -30px;
|
color: white;
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
background: none;
|
||||||
color: white;
|
border: none;
|
||||||
padding: 4px 8px;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
padding: 0.25rem;
|
||||||
font-size: 12px;
|
transition: transform 0.2s;
|
||||||
font-family: monospace;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1000;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add a small arrow to the tooltip */
|
&:hover {
|
||||||
.video-time-tooltip:after {
|
transform: scale(1.1);
|
||||||
content: "";
|
}
|
||||||
position: absolute;
|
|
||||||
bottom: -4px;
|
svg {
|
||||||
left: 50%;
|
width: 1.25rem;
|
||||||
transform: translateX(-50%);
|
height: 1.25rem;
|
||||||
width: 0;
|
}
|
||||||
height: 0;
|
}
|
||||||
border-left: 4px solid transparent;
|
|
||||||
border-right: 4px solid transparent;
|
/* Time tooltip that appears when dragging */
|
||||||
border-top: 4px solid rgba(0, 0, 0, 0.7);
|
.video-time-tooltip {
|
||||||
}
|
position: absolute;
|
||||||
|
top: -30px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: monospace;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a small arrow to the tooltip */
|
||||||
|
.video-time-tooltip:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: -4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 4px solid transparent;
|
||||||
|
border-right: 4px solid transparent;
|
||||||
|
border-top: 4px solid rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
frontend/package-lock.json
generated
12
frontend/package-lock.json
generated
@ -3788,9 +3788,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aproba": {
|
"node_modules/aproba": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
|
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
@ -12590,9 +12590,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nan": {
|
"node_modules/nan": {
|
||||||
"version": "2.22.2",
|
"version": "2.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||||
"integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==",
|
"integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
5595
frontend/yarn.lock
5595
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user