diff --git a/app/components/media/TextEditor.tsx b/app/components/media/TextEditor.tsx index 2501d54..929585f 100644 --- a/app/components/media/TextEditor.tsx +++ b/app/components/media/TextEditor.tsx @@ -96,6 +96,7 @@ export default function TextEditor() { className="w-full h-8 px-2 text-sm bg-muted/50 border border-border rounded-md text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" > + diff --git a/app/components/timeline/Scrubber.tsx b/app/components/timeline/Scrubber.tsx index 067fd7e..7a93ad9 100644 --- a/app/components/timeline/Scrubber.tsx +++ b/app/components/timeline/Scrubber.tsx @@ -1,6 +1,8 @@ import React, { useState, useRef, useCallback, useEffect } from "react"; import { DEFAULT_TRACK_HEIGHT, type ScrubberState, type Transition } from "./types"; -import { Trash2 } from "lucide-react"; +import { Trash2, Edit, Volume2, VolumeX, Settings } from "lucide-react"; +import { TextPropertiesEditor } from "./TextPropertiesEditor"; +import { VolumeControl } from "./VolumeControl"; // something something for the css not gonna bother with it for now export interface SnapConfig { @@ -54,6 +56,8 @@ export const Scrubber: React.FC = ({ x: number; y: number; }>({ visible: false, x: 0, y: 0 }); + const [isEditingText, setIsEditingText] = useState(false); + const [isEditingVolume, setIsEditingVolume] = useState(false); const MINIMUM_WIDTH = 20; @@ -357,7 +361,7 @@ export const Scrubber: React.FC = ({ isDragging, isResizing, resizeMode, - scrubber, + scrubber, // Make sure scrubber is in dependencies to get fresh reference timelineWidth, checkCollisionWithTrack, onUpdate, @@ -388,7 +392,7 @@ export const Scrubber: React.FC = ({ // Handle deletion with Delete/Backspace keys useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (isSelected && (e.key === "Delete" || e.key === "Backspace")) { + if (isSelected) { // Prevent default behavior and check if we're not in an input field const target = e.target as HTMLElement; const isInputElement = @@ -397,9 +401,21 @@ export const Scrubber: React.FC = ({ target.contentEditable === "true" || target.isContentEditable; - if (!isInputElement && onDelete) { - e.preventDefault(); - onDelete(scrubber.id); + if (!isInputElement) { + if ((e.key === "Delete" || e.key === "Backspace") && onDelete) { + e.preventDefault(); + onDelete(scrubber.id); + } else if (e.key === "m" || e.key === "M") { + // Toggle mute for audio/video scrubbers with 'M' key + if (scrubber.mediaType === "video" || scrubber.mediaType === "audio") { + e.preventDefault(); + const updatedScrubber: ScrubberState = { + ...scrubber, + muted: !scrubber.muted, + }; + onUpdate(updatedScrubber); + } + } } } }; @@ -408,7 +424,7 @@ export const Scrubber: React.FC = ({ document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); } - }, [isSelected, onDelete, scrubber.id]); + }, [isSelected, onDelete, scrubber, onUpdate]); // Professional scrubber colors based on media type const getScrubberColor = () => { @@ -474,6 +490,48 @@ export const Scrubber: React.FC = ({ setContextMenu({ visible: false, x: 0, y: 0 }); }, [onDelete, scrubber.id]); + // Handle context menu edit text action + const handleContextMenuEditText = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setIsEditingText(true); + setContextMenu({ visible: false, x: 0, y: 0 }); + }, []); + + // Handle context menu mute toggle action + const handleContextMenuMuteToggle = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const updatedScrubber: ScrubberState = { + ...scrubber, + muted: !scrubber.muted, + }; + onUpdate(updatedScrubber); + + // Close context menu + setContextMenu({ visible: false, x: 0, y: 0 }); + }, [scrubber, onUpdate]); + + // Handle context menu volume settings action + const handleContextMenuVolumeSettings = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + setIsEditingVolume(true); + setContextMenu({ visible: false, x: 0, y: 0 }); + }, []); + + // Handle text properties save + const handleTextPropertiesSave = useCallback((updatedTextProperties: import("./types").TextProperties) => { + const updatedScrubber: ScrubberState = { + ...scrubber, + text: updatedTextProperties, + }; + onUpdate(updatedScrubber); + }, [scrubber, onUpdate]); + // Add click outside listener for context menu useEffect(() => { if (contextMenu.visible) { @@ -497,7 +555,7 @@ export const Scrubber: React.FC = ({ top: `${(scrubber.y || 0) * DEFAULT_TRACK_HEIGHT + 2}px`, height: `${DEFAULT_TRACK_HEIGHT - 4}px`, minWidth: "20px", - zIndex: isDragging || isResizing ? 1000 : isSelected ? 20 : 15, + zIndex: isDragging || isResizing ? 1000 : isSelected ? 100 : 50, }} onMouseDown={(e) => handleMouseDown(e, "drag")} onContextMenu={handleContextMenu} @@ -515,6 +573,13 @@ export const Scrubber: React.FC = ({ {scrubber.name} + {/* Mute indicator for audio/video */} + {(scrubber.mediaType === "video" || scrubber.mediaType === "audio") && scrubber.muted && ( +
+ +
+ )} + {/* Left resize handle - more visible */} {scrubber.mediaType !== "video" && scrubber.mediaType !== "audio" && (
= ({ top: `${contextMenu.y}px`, }} > + {/* Show Edit Text option only for text scrubbers */} + {scrubber.mediaType === "text" && scrubber.text && ( + + )} + + {/* Show Mute/Unmute option for video and audio scrubbers */} + {(scrubber.mediaType === "video" || scrubber.mediaType === "audio") && ( + + )} + + {/* Show Volume Settings option for video and audio scrubbers */} + {(scrubber.mediaType === "video" || scrubber.mediaType === "audio") && ( + + )}
)} + + {/* Text Properties Editor Modal */} + {isEditingText && scrubber.mediaType === "text" && scrubber.text && ( + setIsEditingText(false)} + onSave={handleTextPropertiesSave} + scrubberId={scrubber.id} + /> + )} + + {/* Volume Control Modal */} + {isEditingVolume && (scrubber.mediaType === "video" || scrubber.mediaType === "audio") && ( + setIsEditingVolume(false)} + onSave={onUpdate} + scrubberId={scrubber.id} + /> + )} ); }; diff --git a/app/components/timeline/TextPropertiesEditor.tsx b/app/components/timeline/TextPropertiesEditor.tsx new file mode 100644 index 0000000..4d5ec12 --- /dev/null +++ b/app/components/timeline/TextPropertiesEditor.tsx @@ -0,0 +1,264 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { Badge } from "~/components/ui/badge"; +import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; +import { Separator } from "~/components/ui/separator"; +import { + AlignLeft, + AlignCenter, + AlignRight, + Bold, + Type, + X, + Check, +} from "lucide-react"; +import type { TextProperties } from "./types"; + +interface TextPropertiesEditorProps { + textProperties: TextProperties; + isOpen: boolean; + onClose: () => void; + onSave: (updatedProperties: TextProperties) => void; + scrubberId: string; +} + +export const TextPropertiesEditor: React.FC = ({ + textProperties, + isOpen, + onClose, + onSave, + scrubberId, +}) => { + const [textContent, setTextContent] = useState(textProperties.textContent); + const [fontSize, setFontSize] = useState(textProperties.fontSize); + const [fontFamily, setFontFamily] = useState(textProperties.fontFamily); + const [color, setColor] = useState(textProperties.color); + const [textAlign, setTextAlign] = useState<"left" | "center" | "right">( + textProperties.textAlign + ); + const [fontWeight, setFontWeight] = useState<"normal" | "bold">( + textProperties.fontWeight + ); + + // Reset form when properties change + useEffect(() => { + setTextContent(textProperties.textContent); + setFontSize(textProperties.fontSize); + setFontFamily(textProperties.fontFamily); + setColor(textProperties.color); + setTextAlign(textProperties.textAlign); + setFontWeight(textProperties.fontWeight); + }, [textProperties]); + + const handleSave = () => { + const updatedProperties: TextProperties = { + textContent, + fontSize, + fontFamily, + color, + textAlign, + fontWeight, + }; + onSave(updatedProperties); + onClose(); + }; + + const handleCancel = () => { + // Reset to original values + setTextContent(textProperties.textContent); + setFontSize(textProperties.fontSize); + setFontFamily(textProperties.fontFamily); + setColor(textProperties.color); + setTextAlign(textProperties.textAlign); + setFontWeight(textProperties.fontWeight); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+
+ + +
+
+ + Edit Text Properties +
+ +
+
+ + {/* Text Content */} +
+ +