mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-12-24 04:12:33 -05:00
feat: Improve Visual Distinction Between Trim and Chapters Editors (#1445)
* Update .gitignore * feat: Improve Visual Distinction Between Trim and Chapters Editors * fix: Convert timeline header styles to CSS classes Moved inline styles for timeline headers in chapters and video editors to dedicated CSS classes for better maintainability and consistency. * Bump version to 7.3.0 Update the VERSION in cms/version.py to 7.3.0 for the new release. * build assets * Update segment color schemes in video and chapters editor. * build assets * build assets * fix: Prevent Safari from resetting segments after drag operations Prevent Safari from resetting segments when loadedmetadata fires multiple times and fix stale state issues in click handlers by using refs instead of closure variables. * build assets * Bump version to 7.3.0-beta.3 Update the VERSION string in cms/version.py to reflect the new pre-release version 7.3.0-beta.3.
This commit is contained in:
committed by
GitHub
parent
aeef8284bf
commit
d9b1d6cab1
@@ -150,6 +150,11 @@ const App = () => {
|
||||
canRedo={historyPosition < history.length - 1}
|
||||
/>
|
||||
|
||||
{/* Timeline Header */}
|
||||
<div className="timeline-header-container">
|
||||
<h2 className="timeline-header-title">Add Chapters</h2>
|
||||
</div>
|
||||
|
||||
{/* Timeline Controls */}
|
||||
<TimelineControls
|
||||
currentTime={currentTime}
|
||||
|
||||
@@ -28,9 +28,9 @@ const ClipSegments = ({ segments, selectedSegmentId }: ClipSegmentsProps) => {
|
||||
|
||||
// Generate the same color background for a segment as shown in the timeline
|
||||
const getSegmentColorClass = (index: number) => {
|
||||
// Return CSS class based on index modulo 8
|
||||
// This matches the CSS nth-child selectors in the timeline
|
||||
return `segment-default-color segment-color-${(index % 8) + 1}`;
|
||||
// Return CSS class based on index modulo 20
|
||||
// This matches the CSS classes for up to 20 segments
|
||||
return `segment-default-color segment-color-${(index % 20) + 1}`;
|
||||
};
|
||||
|
||||
// Get selected segment
|
||||
@@ -65,8 +65,8 @@ const ClipSegments = ({ segments, selectedSegmentId }: ClipSegmentsProps) => {
|
||||
<div className="segment-actions">
|
||||
<button
|
||||
className="delete-button"
|
||||
aria-label="Delete Segment"
|
||||
data-tooltip="Delete this segment"
|
||||
aria-label="Delete Chapter"
|
||||
data-tooltip="Delete this chapter"
|
||||
onClick={() => handleDeleteSegment(segment.id)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
|
||||
@@ -177,7 +177,16 @@ const TimelineControls = ({
|
||||
const [isAutoSaving, setIsAutoSaving] = useState(false);
|
||||
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const clipSegmentsRef = useRef(clipSegments);
|
||||
|
||||
// Track when a drag just ended to prevent Safari from triggering clicks after drag
|
||||
const dragJustEndedRef = useRef<boolean>(false);
|
||||
const dragEndTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Helper function to detect Safari browser
|
||||
const isSafari = () => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
|
||||
return /Safari/.test(userAgent) && !/Chrome/.test(userAgent) && !/Chromium/.test(userAgent);
|
||||
};
|
||||
|
||||
// Keep clipSegmentsRef updated
|
||||
useEffect(() => {
|
||||
@@ -867,6 +876,12 @@ const TimelineControls = ({
|
||||
logger.debug('Clearing auto-save timer in cleanup:', autoSaveTimerRef.current);
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
}
|
||||
|
||||
// Clear any pending drag end timeout
|
||||
if (dragEndTimeoutRef.current) {
|
||||
clearTimeout(dragEndTimeoutRef.current);
|
||||
dragEndTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [scheduleAutoSave]);
|
||||
|
||||
@@ -1084,16 +1099,20 @@ const TimelineControls = ({
|
||||
};
|
||||
|
||||
// Helper function to calculate available space for a new segment
|
||||
const calculateAvailableSpace = (startTime: number): number => {
|
||||
const calculateAvailableSpace = (startTime: number, segmentsOverride?: Segment[]): number => {
|
||||
// Always return at least 0.1 seconds to ensure tooltip shows
|
||||
const MIN_SPACE = 0.1;
|
||||
|
||||
// Use override segments if provided, otherwise use ref to get latest segments
|
||||
// This ensures we always have the most up-to-date segments, especially important for Safari
|
||||
const segmentsToUse = segmentsOverride || clipSegmentsRef.current;
|
||||
|
||||
// Determine the amount of available space:
|
||||
// 1. Check remaining space until the end of video
|
||||
const remainingDuration = Math.max(0, duration - startTime);
|
||||
|
||||
// 2. Find the next segment (if any)
|
||||
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||
const sortedSegments = [...segmentsToUse].sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
// Find the next and previous segments
|
||||
const nextSegment = sortedSegments.find((seg) => seg.startTime > startTime);
|
||||
@@ -1109,14 +1128,6 @@ const TimelineControls = ({
|
||||
availableSpace = duration - startTime;
|
||||
}
|
||||
|
||||
// Log the space calculation for debugging
|
||||
logger.debug('Space calculation:', {
|
||||
position: formatDetailedTime(startTime),
|
||||
nextSegment: nextSegment ? formatDetailedTime(nextSegment.startTime) : 'none',
|
||||
prevSegment: prevSegment ? formatDetailedTime(prevSegment.endTime) : 'none',
|
||||
availableSpace: formatDetailedTime(Math.max(MIN_SPACE, availableSpace)),
|
||||
});
|
||||
|
||||
// Always return at least MIN_SPACE to ensure tooltip shows
|
||||
return Math.max(MIN_SPACE, availableSpace);
|
||||
};
|
||||
@@ -1125,8 +1136,11 @@ const TimelineControls = ({
|
||||
const updateTooltipForPosition = (currentPosition: number) => {
|
||||
if (!timelineRef.current) return;
|
||||
|
||||
// Use ref to get latest segments to avoid stale state issues
|
||||
const currentSegments = clipSegmentsRef.current;
|
||||
|
||||
// Find if we're in a segment at the current position with a small tolerance
|
||||
const segmentAtPosition = clipSegments.find((seg) => {
|
||||
const segmentAtPosition = currentSegments.find((seg) => {
|
||||
const isWithinSegment = currentPosition >= seg.startTime && currentPosition <= seg.endTime;
|
||||
const isVeryCloseToStart = Math.abs(currentPosition - seg.startTime) < 0.001;
|
||||
const isVeryCloseToEnd = Math.abs(currentPosition - seg.endTime) < 0.001;
|
||||
@@ -1134,7 +1148,7 @@ const TimelineControls = ({
|
||||
});
|
||||
|
||||
// Find the next and previous segments
|
||||
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||
const sortedSegments = [...currentSegments].sort((a, b) => a.startTime - b.startTime);
|
||||
const nextSegment = sortedSegments.find((seg) => seg.startTime > currentPosition);
|
||||
const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < currentPosition);
|
||||
|
||||
@@ -1144,21 +1158,13 @@ const TimelineControls = ({
|
||||
setShowEmptySpaceTooltip(false);
|
||||
} else {
|
||||
// We're in a cutaway area
|
||||
// Calculate available space for new segment
|
||||
const availableSpace = calculateAvailableSpace(currentPosition);
|
||||
// Calculate available space for new segment using current segments
|
||||
const availableSpace = calculateAvailableSpace(currentPosition, currentSegments);
|
||||
setAvailableSegmentDuration(availableSpace);
|
||||
|
||||
// Always show empty space tooltip
|
||||
setSelectedSegmentId(null);
|
||||
setShowEmptySpaceTooltip(true);
|
||||
|
||||
// Log position info for debugging
|
||||
logger.debug('Cutaway position:', {
|
||||
current: formatDetailedTime(currentPosition),
|
||||
prevSegmentEnd: prevSegment ? formatDetailedTime(prevSegment.endTime) : 'none',
|
||||
nextSegmentStart: nextSegment ? formatDetailedTime(nextSegment.startTime) : 'none',
|
||||
availableSpace: formatDetailedTime(availableSpace),
|
||||
});
|
||||
}
|
||||
|
||||
// Update tooltip position
|
||||
@@ -1188,6 +1194,12 @@ const TimelineControls = ({
|
||||
|
||||
if (!timelineRef.current || !scrollContainerRef.current) return;
|
||||
|
||||
// Safari-specific fix: Ignore clicks that happen immediately after a drag operation
|
||||
// Safari fires click events after drag ends, which can cause issues with stale state
|
||||
if (isSafari() && dragJustEndedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If on mobile device and video hasn't been initialized, don't handle timeline clicks
|
||||
if (isIOSUninitialized) {
|
||||
return;
|
||||
@@ -1195,7 +1207,6 @@ const TimelineControls = ({
|
||||
|
||||
// Check if video is globally playing before the click
|
||||
const wasPlaying = videoRef.current && !videoRef.current.paused;
|
||||
logger.debug('Video was playing before timeline click:', wasPlaying);
|
||||
|
||||
// Reset continuation flag when clicking on timeline - ensures proper boundary detection
|
||||
setContinuePastBoundary(false);
|
||||
@@ -1216,14 +1227,6 @@ const TimelineControls = ({
|
||||
|
||||
const newTime = position * duration;
|
||||
|
||||
// Log the position for debugging
|
||||
logger.debug(
|
||||
'Timeline clicked at:',
|
||||
formatDetailedTime(newTime),
|
||||
'distance from end:',
|
||||
formatDetailedTime(duration - newTime)
|
||||
);
|
||||
|
||||
// Store position globally for iOS Safari (this is critical for first-time visits)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.lastSeekedPosition = newTime;
|
||||
@@ -1236,8 +1239,12 @@ const TimelineControls = ({
|
||||
setClickedTime(newTime);
|
||||
setDisplayTime(newTime);
|
||||
|
||||
// Use ref to get latest segments to avoid stale state issues, especially in Safari
|
||||
// Safari can fire click events immediately after drag before React re-renders
|
||||
const currentSegments = clipSegmentsRef.current;
|
||||
|
||||
// Find if we clicked in a segment with a small tolerance for boundaries
|
||||
const segmentAtClickedTime = clipSegments.find((seg) => {
|
||||
const segmentAtClickedTime = currentSegments.find((seg) => {
|
||||
// Standard check for being inside a segment
|
||||
const isInside = newTime >= seg.startTime && newTime <= seg.endTime;
|
||||
// Additional checks for being exactly at the start or end boundary (with small tolerance)
|
||||
@@ -1258,7 +1265,7 @@ const TimelineControls = ({
|
||||
if (isPlayingSegments && wasPlaying) {
|
||||
// Update the current segment index if we clicked into a segment
|
||||
if (segmentAtClickedTime) {
|
||||
const orderedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||
const orderedSegments = [...currentSegments].sort((a, b) => a.startTime - b.startTime);
|
||||
const targetSegmentIndex = orderedSegments.findIndex((seg) => seg.id === segmentAtClickedTime.id);
|
||||
|
||||
if (targetSegmentIndex !== -1) {
|
||||
@@ -1311,8 +1318,9 @@ const TimelineControls = ({
|
||||
// We're in a cutaway area - always show tooltip
|
||||
setSelectedSegmentId(null);
|
||||
|
||||
// Calculate the available space for a new segment
|
||||
const availableSpace = calculateAvailableSpace(newTime);
|
||||
// Calculate the available space for a new segment using current segments from ref
|
||||
// This ensures we use the latest segments even if React hasn't re-rendered yet
|
||||
const availableSpace = calculateAvailableSpace(newTime, currentSegments);
|
||||
setAvailableSegmentDuration(availableSpace);
|
||||
|
||||
// Calculate and set tooltip position correctly for zoomed timeline
|
||||
@@ -1334,18 +1342,6 @@ const TimelineControls = ({
|
||||
|
||||
// Always show the empty space tooltip in cutaway areas
|
||||
setShowEmptySpaceTooltip(true);
|
||||
|
||||
// Log the cutaway area details
|
||||
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
||||
const prevSegment = [...sortedSegments].reverse().find((seg) => seg.endTime < newTime);
|
||||
const nextSegment = sortedSegments.find((seg) => seg.startTime > newTime);
|
||||
|
||||
logger.debug('Clicked in cutaway area:', {
|
||||
position: formatDetailedTime(newTime),
|
||||
availableSpace: formatDetailedTime(availableSpace),
|
||||
prevSegmentEnd: prevSegment ? formatDetailedTime(prevSegment.endTime) : 'none',
|
||||
nextSegmentStart: nextSegment ? formatDetailedTime(nextSegment.startTime) : 'none',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1498,6 +1494,10 @@ const TimelineControls = ({
|
||||
return seg;
|
||||
});
|
||||
|
||||
// Update the ref immediately during drag to ensure we always have latest segments
|
||||
// This is critical for Safari which may fire events before React re-renders
|
||||
clipSegmentsRef.current = updatedSegments;
|
||||
|
||||
// Create a custom event to update the segments WITHOUT recording in history during drag
|
||||
const updateEvent = new CustomEvent('update-segments', {
|
||||
detail: {
|
||||
@@ -1582,6 +1582,26 @@ const TimelineControls = ({
|
||||
return seg;
|
||||
});
|
||||
|
||||
// CRITICAL: Update the ref immediately with the new segments
|
||||
// This ensures that if Safari fires a click event before React re-renders,
|
||||
// the click handler will use the updated segments instead of stale ones
|
||||
clipSegmentsRef.current = finalSegments;
|
||||
|
||||
// Safari-specific fix: Set flag to ignore clicks immediately after drag
|
||||
// Safari fires click events after drag ends, which can interfere with state updates
|
||||
if (isSafari()) {
|
||||
dragJustEndedRef.current = true;
|
||||
// Clear the flag after a delay to allow React to re-render with updated segments
|
||||
// Increased timeout to ensure state has propagated
|
||||
if (dragEndTimeoutRef.current) {
|
||||
clearTimeout(dragEndTimeoutRef.current);
|
||||
}
|
||||
dragEndTimeoutRef.current = setTimeout(() => {
|
||||
dragJustEndedRef.current = false;
|
||||
dragEndTimeoutRef.current = null;
|
||||
}, 200); // 200ms to ensure React has processed the state update and re-rendered
|
||||
}
|
||||
|
||||
// Now we can create a history record for the complete drag operation
|
||||
const actionType = isLeft ? 'adjust_segment_start' : 'adjust_segment_end';
|
||||
document.dispatchEvent(
|
||||
@@ -1594,6 +1614,13 @@ const TimelineControls = ({
|
||||
})
|
||||
);
|
||||
|
||||
// Dispatch segment-drag-end event for other listeners
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('segment-drag-end', {
|
||||
detail: { segmentId },
|
||||
})
|
||||
);
|
||||
|
||||
// After drag is complete, do a final check to see if playhead is inside the segment
|
||||
if (selectedSegmentId === segmentId && videoRef.current) {
|
||||
const currentTime = videoRef.current.currentTime;
|
||||
@@ -3943,9 +3970,7 @@ const TimelineControls = ({
|
||||
<button
|
||||
onClick={() => setShowSaveChaptersModal(true)}
|
||||
className="save-chapters-button"
|
||||
data-tooltip={clipSegments.length === 0
|
||||
? "Clear all chapters"
|
||||
: "Save chapters"}
|
||||
{...(clipSegments.length === 0 && { 'data-tooltip': 'Clear all chapters' })}
|
||||
>
|
||||
{clipSegments.length === 0
|
||||
? 'Clear Chapters'
|
||||
|
||||
@@ -60,6 +60,9 @@ const useVideoChapters = () => {
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
|
||||
// Track if editor has been initialized to prevent re-initialization on Safari metadata events
|
||||
const isInitializedRef = useRef<boolean>(false);
|
||||
|
||||
// Timeline state
|
||||
const [trimStart, setTrimStart] = useState(0);
|
||||
@@ -108,11 +111,7 @@ const useVideoChapters = () => {
|
||||
// Detect Safari browser
|
||||
const isSafari = () => {
|
||||
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
|
||||
const isSafariBrowser = /Safari/.test(userAgent) && !/Chrome/.test(userAgent) && !/Chromium/.test(userAgent);
|
||||
if (isSafariBrowser) {
|
||||
logger.debug('Safari browser detected, enabling audio support fallbacks');
|
||||
}
|
||||
return isSafariBrowser;
|
||||
return /Safari/.test(userAgent) && !/Chrome/.test(userAgent) && !/Chromium/.test(userAgent);
|
||||
};
|
||||
|
||||
// Initialize video event listeners
|
||||
@@ -121,7 +120,15 @@ const useVideoChapters = () => {
|
||||
if (!video) return;
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
logger.debug('Video loadedmetadata event fired, duration:', video.duration);
|
||||
// CRITICAL: Prevent re-initialization if editor has already been initialized
|
||||
// Safari fires loadedmetadata multiple times, which was resetting segments
|
||||
if (isInitializedRef.current) {
|
||||
// Still update duration and trimEnd in case they changed
|
||||
setDuration(video.duration);
|
||||
setTrimEnd(video.duration);
|
||||
return;
|
||||
}
|
||||
|
||||
setDuration(video.duration);
|
||||
setTrimEnd(video.duration);
|
||||
|
||||
@@ -173,7 +180,7 @@ const useVideoChapters = () => {
|
||||
setHistory([initialState]);
|
||||
setHistoryPosition(0);
|
||||
setClipSegments(initialSegments);
|
||||
logger.debug('Editor initialized with segments:', initialSegments.length);
|
||||
isInitializedRef.current = true; // Mark as initialized
|
||||
};
|
||||
|
||||
initializeEditor();
|
||||
@@ -181,20 +188,18 @@ const useVideoChapters = () => {
|
||||
|
||||
// Safari-specific fallback for audio files
|
||||
const handleCanPlay = () => {
|
||||
logger.debug('Video canplay event fired');
|
||||
// If loadedmetadata hasn't fired yet but we have duration, trigger initialization
|
||||
if (video.duration && duration === 0) {
|
||||
logger.debug('Safari fallback: Using canplay event to initialize');
|
||||
// Also check if already initialized to prevent re-initialization
|
||||
if (video.duration && duration === 0 && !isInitializedRef.current) {
|
||||
handleLoadedMetadata();
|
||||
}
|
||||
};
|
||||
|
||||
// Additional Safari fallback for audio files
|
||||
const handleLoadedData = () => {
|
||||
logger.debug('Video loadeddata event fired');
|
||||
// If we still don't have duration, try again
|
||||
if (video.duration && duration === 0) {
|
||||
logger.debug('Safari fallback: Using loadeddata event to initialize');
|
||||
// Also check if already initialized to prevent re-initialization
|
||||
if (video.duration && duration === 0 && !isInitializedRef.current) {
|
||||
handleLoadedMetadata();
|
||||
}
|
||||
};
|
||||
@@ -226,14 +231,12 @@ const useVideoChapters = () => {
|
||||
|
||||
// Safari-specific fallback event listeners for audio files
|
||||
if (isSafari()) {
|
||||
logger.debug('Adding Safari-specific event listeners for audio support');
|
||||
video.addEventListener('canplay', handleCanPlay);
|
||||
video.addEventListener('loadeddata', handleLoadedData);
|
||||
|
||||
// Additional timeout fallback for Safari audio files
|
||||
const safariTimeout = setTimeout(() => {
|
||||
if (video.duration && duration === 0) {
|
||||
logger.debug('Safari timeout fallback: Force initializing editor');
|
||||
if (video.duration && duration === 0 && !isInitializedRef.current) {
|
||||
handleLoadedMetadata();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -82,27 +82,24 @@
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--foreground, #333);
|
||||
margin: 0;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.save-chapters-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
background: #059669;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.2s;
|
||||
min-width: fit-content;
|
||||
|
||||
&:hover {
|
||||
background-color: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
|
||||
background-color: #059669;
|
||||
box-shadow: 0 4px 6px -1px rgba(5, 150, 105, 0.3);
|
||||
}
|
||||
|
||||
&.has-changes {
|
||||
@@ -205,9 +202,9 @@
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
background-color: rgba(59, 130, 246, 0.05);
|
||||
border-color: #059669;
|
||||
box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
|
||||
background-color: rgba(5, 150, 105, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,29 +284,68 @@
|
||||
color: rgba(51, 51, 51, 0.7);
|
||||
}
|
||||
|
||||
/* Generate 20 shades of #059669 (rgb(5, 150, 105)) */
|
||||
/* Base color: #059669 = rgb(5, 150, 105) */
|
||||
/* Creating variations from lighter to darker */
|
||||
.segment-color-1 {
|
||||
background-color: rgba(59, 130, 246, 0.15);
|
||||
background-color: rgba(167, 243, 208, 0.2);
|
||||
}
|
||||
.segment-color-2 {
|
||||
background-color: rgba(16, 185, 129, 0.15);
|
||||
background-color: rgba(134, 239, 172, 0.2);
|
||||
}
|
||||
.segment-color-3 {
|
||||
background-color: rgba(245, 158, 11, 0.15);
|
||||
background-color: rgba(101, 235, 136, 0.2);
|
||||
}
|
||||
.segment-color-4 {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
background-color: rgba(68, 231, 100, 0.2);
|
||||
}
|
||||
.segment-color-5 {
|
||||
background-color: rgba(139, 92, 246, 0.15);
|
||||
background-color: rgba(35, 227, 64, 0.2);
|
||||
}
|
||||
.segment-color-6 {
|
||||
background-color: rgba(236, 72, 153, 0.15);
|
||||
background-color: rgba(20, 207, 54, 0.2);
|
||||
}
|
||||
.segment-color-7 {
|
||||
background-color: rgba(6, 182, 212, 0.15);
|
||||
background-color: rgba(15, 187, 48, 0.2);
|
||||
}
|
||||
.segment-color-8 {
|
||||
background-color: rgba(250, 204, 21, 0.15);
|
||||
background-color: rgba(10, 167, 42, 0.2);
|
||||
}
|
||||
.segment-color-9 {
|
||||
background-color: rgba(5, 150, 105, 0.2);
|
||||
}
|
||||
.segment-color-10 {
|
||||
background-color: rgba(4, 135, 95, 0.2);
|
||||
}
|
||||
.segment-color-11 {
|
||||
background-color: rgba(3, 120, 85, 0.2);
|
||||
}
|
||||
.segment-color-12 {
|
||||
background-color: rgba(2, 105, 75, 0.2);
|
||||
}
|
||||
.segment-color-13 {
|
||||
background-color: rgba(2, 90, 65, 0.2);
|
||||
}
|
||||
.segment-color-14 {
|
||||
background-color: rgba(1, 75, 55, 0.2);
|
||||
}
|
||||
.segment-color-15 {
|
||||
background-color: rgba(1, 66, 48, 0.2);
|
||||
}
|
||||
.segment-color-16 {
|
||||
background-color: rgba(1, 57, 41, 0.2);
|
||||
}
|
||||
.segment-color-17 {
|
||||
background-color: rgba(1, 48, 34, 0.2);
|
||||
}
|
||||
.segment-color-18 {
|
||||
background-color: rgba(0, 39, 27, 0.2);
|
||||
}
|
||||
.segment-color-19 {
|
||||
background-color: rgba(0, 30, 20, 0.2);
|
||||
}
|
||||
.segment-color-20 {
|
||||
background-color: rgba(0, 21, 13, 0.2);
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
.ios-notification-icon {
|
||||
flex-shrink: 0;
|
||||
color: #0066cc;
|
||||
color: #059669;
|
||||
margin-right: 15px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
|
||||
.ios-desktop-mode-btn {
|
||||
background-color: #0066cc;
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -92,12 +92,12 @@
|
||||
}
|
||||
|
||||
.modal-button-primary {
|
||||
background-color: #0066cc;
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-button-primary:hover {
|
||||
background-color: #0055aa;
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.modal-button-secondary {
|
||||
@@ -138,7 +138,7 @@
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top: 4px solid #0066cc;
|
||||
border-top: 4px solid #059669;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
@@ -224,7 +224,7 @@
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #0066cc;
|
||||
background-color: #059669;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -258,12 +258,12 @@
|
||||
margin: 0 auto;
|
||||
width: auto;
|
||||
min-width: 220px;
|
||||
background-color: #0066cc;
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.centered-choice:hover {
|
||||
background-color: #0055aa;
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@@ -300,7 +300,7 @@
|
||||
|
||||
.countdown {
|
||||
font-weight: bold;
|
||||
color: #0066cc;
|
||||
color: #059669;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
#chapters-editor-root {
|
||||
.timeline-header-container {
|
||||
margin-left: 1rem;
|
||||
margin-top: -0.5rem;
|
||||
}
|
||||
|
||||
.timeline-header-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-container-card {
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
@@ -11,6 +23,8 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
@@ -20,7 +34,7 @@
|
||||
}
|
||||
|
||||
.timeline-title-text {
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
@@ -48,10 +62,11 @@
|
||||
.timeline-container {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
background-color: #fafbfc;
|
||||
background-color: #e2ede4;
|
||||
height: 70px;
|
||||
border-radius: 0.25rem;
|
||||
overflow: visible !important;
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
@@ -194,7 +209,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0.4rem;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
background-color: rgba(16, 185, 129, 0.6);
|
||||
color: white;
|
||||
opacity: 1;
|
||||
transition: background-color 0.2s;
|
||||
@@ -202,15 +217,15 @@
|
||||
}
|
||||
|
||||
.clip-segment:hover .clip-segment-info {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-color: rgba(16, 185, 129, 0.7);
|
||||
}
|
||||
|
||||
.clip-segment.selected .clip-segment-info {
|
||||
background-color: rgba(59, 130, 246, 0.5);
|
||||
background-color: rgba(5, 150, 105, 0.8);
|
||||
}
|
||||
|
||||
.clip-segment.selected:hover .clip-segment-info {
|
||||
background-color: rgba(59, 130, 246, 0.4);
|
||||
background-color: rgba(5, 150, 105, 0.75);
|
||||
}
|
||||
|
||||
.clip-segment-name {
|
||||
@@ -540,7 +555,7 @@
|
||||
.save-copy-button,
|
||||
.save-segments-button {
|
||||
color: #ffffff;
|
||||
background: #0066cc;
|
||||
background: #059669;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
@@ -713,7 +728,7 @@
|
||||
height: 50px;
|
||||
border: 5px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top-color: #0066cc;
|
||||
border-top-color: #059669;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -753,7 +768,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background-color: #0066cc;
|
||||
background-color: #059669;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
@@ -766,7 +781,7 @@
|
||||
}
|
||||
|
||||
.modal-choice-button:hover {
|
||||
background-color: #0056b3;
|
||||
background-color:rgb(7, 119, 84);
|
||||
}
|
||||
|
||||
.modal-choice-button svg {
|
||||
@@ -941,7 +956,6 @@
|
||||
|
||||
.save-chapters-button:hover {
|
||||
background-color: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user