Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/media/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<option value="Arial">Arial</option>
<option value="Arial Narrow">Arial Narrow</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times</option>
<option value="Georgia">Georgia</option>
Expand Down
144 changes: 136 additions & 8 deletions app/components/timeline/Scrubber.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -54,6 +56,8 @@ export const Scrubber: React.FC<ScrubberProps> = ({
x: number;
y: number;
}>({ visible: false, x: 0, y: 0 });
const [isEditingText, setIsEditingText] = useState(false);
const [isEditingVolume, setIsEditingVolume] = useState(false);

const MINIMUM_WIDTH = 20;

Expand Down Expand Up @@ -357,7 +361,7 @@ export const Scrubber: React.FC<ScrubberProps> = ({
isDragging,
isResizing,
resizeMode,
scrubber,
scrubber, // Make sure scrubber is in dependencies to get fresh reference
timelineWidth,
checkCollisionWithTrack,
onUpdate,
Expand Down Expand Up @@ -388,7 +392,7 @@ export const Scrubber: React.FC<ScrubberProps> = ({
// 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 =
Expand All @@ -397,9 +401,21 @@ export const Scrubber: React.FC<ScrubberProps> = ({
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);
}
}
}
}
};
Expand All @@ -408,7 +424,7 @@ export const Scrubber: React.FC<ScrubberProps> = ({
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 = () => {
Expand Down Expand Up @@ -474,6 +490,48 @@ export const Scrubber: React.FC<ScrubberProps> = ({
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) {
Expand All @@ -497,7 +555,7 @@ export const Scrubber: React.FC<ScrubberProps> = ({
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}
Expand All @@ -515,6 +573,13 @@ export const Scrubber: React.FC<ScrubberProps> = ({
{scrubber.name}
</div>

{/* Mute indicator for audio/video */}
{(scrubber.mediaType === "video" || scrubber.mediaType === "audio") && scrubber.muted && (
<div className="absolute top-0.5 right-2 text-xs opacity-80 pointer-events-none">
<VolumeX className="h-3 w-3" />
</div>
)}

{/* Left resize handle - more visible */}
{scrubber.mediaType !== "video" && scrubber.mediaType !== "audio" && (
<div
Expand Down Expand Up @@ -576,6 +641,47 @@ export const Scrubber: React.FC<ScrubberProps> = ({
top: `${contextMenu.y}px`,
}}
>
{/* Show Edit Text option only for text scrubbers */}
{scrubber.mediaType === "text" && scrubber.text && (
<button
className="flex items-center gap-2 w-full px-3 py-2 text-xs hover:bg-muted transition-colors text-left"
onClick={handleContextMenuEditText}
>
<Edit className="h-3 w-3" />
Edit Text
</button>
)}

{/* Show Mute/Unmute option for video and audio scrubbers */}
{(scrubber.mediaType === "video" || scrubber.mediaType === "audio") && (
<button
className="flex items-center gap-2 w-full px-3 py-2 text-xs hover:bg-muted transition-colors text-left"
onClick={handleContextMenuMuteToggle}
>
{scrubber.muted ? (
<>
<Volume2 className="h-3 w-3" />
Unmute
</>
) : (
<>
<VolumeX className="h-3 w-3" />
Mute
</>
)}
</button>
)}

{/* Show Volume Settings option for video and audio scrubbers */}
{(scrubber.mediaType === "video" || scrubber.mediaType === "audio") && (
<button
className="flex items-center gap-2 w-full px-3 py-2 text-xs hover:bg-muted transition-colors text-left"
onClick={handleContextMenuVolumeSettings}
>
<Settings className="h-3 w-3" />
Volume Settings
</button>
)}
<button
className="flex items-center gap-2 w-full px-3 py-2 text-xs hover:bg-muted transition-colors text-left text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
onClick={handleContextMenuDelete}
Expand All @@ -585,6 +691,28 @@ export const Scrubber: React.FC<ScrubberProps> = ({
</button>
</div>
)}

{/* Text Properties Editor Modal */}
{isEditingText && scrubber.mediaType === "text" && scrubber.text && (
<TextPropertiesEditor
textProperties={scrubber.text}
isOpen={isEditingText}
onClose={() => setIsEditingText(false)}
onSave={handleTextPropertiesSave}
scrubberId={scrubber.id}
/>
)}

{/* Volume Control Modal */}
{isEditingVolume && (scrubber.mediaType === "video" || scrubber.mediaType === "audio") && (
<VolumeControl
scrubber={scrubber}
isOpen={isEditingVolume}
onClose={() => setIsEditingVolume(false)}
onSave={onUpdate}
scrubberId={scrubber.id}
/>
)}
</>
);
};
Loading