From 3265aa3530a0f933cd5f0f32b73c0c681fa7e3fb Mon Sep 17 00:00:00 2001 From: loga115 Date: Mon, 28 Jul 2025 23:06:53 +0530 Subject: [PATCH 1/2] adding more stuff :D vibe coded feature adding/qol stuff, PLEASE review my edits before merging, I've looked over it to make sure nothing's too breaking and I've tested my changes but regardless, can't be too sure yeah? -added arial narrow as a font option (the brat font) -added a mute shortcut/option -added an edit text option to change text properties on the timeline so you don't have to remake the text -attempted to fix scrubber drag stuff -fixed audio replication on split -fixed weird bug when deleting text -video/text/audio marking on timeline (qol thing) note: -scrubber drag still has issues, need to look over it --- app/components/media/TextEditor.tsx | 1 + app/components/timeline/Scrubber.tsx | 144 +++++++++- .../timeline/TextPropertiesEditor.tsx | 264 ++++++++++++++++++ app/components/timeline/TimelineTracks.tsx | 32 +-- app/components/timeline/VolumeControl.tsx | 140 ++++++++++ app/components/timeline/types.ts | 8 + app/hooks/useMediaBin.ts | 21 +- app/hooks/useTimeline.ts | 8 + app/routes/home.tsx | 106 ++++++- app/utils/api.ts | 4 +- app/video-compositions/DragDrop.tsx | 62 +++- app/video-compositions/VideoPlayer.tsx | 18 +- pnpm-workspace.yaml | 3 + 13 files changed, 763 insertions(+), 48 deletions(-) create mode 100644 app/components/timeline/TextPropertiesEditor.tsx create mode 100644 app/components/timeline/VolumeControl.tsx create mode 100644 pnpm-workspace.yaml 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 d35addd..7bc19a9 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 } 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 { @@ -51,6 +53,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; @@ -335,7 +339,7 @@ export const Scrubber: React.FC = ({ isDragging, isResizing, resizeMode, - scrubber, + scrubber, // Make sure scrubber is in dependencies to get fresh reference timelineWidth, checkCollisionWithTrack, onUpdate, @@ -366,7 +370,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 = @@ -375,9 +379,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); + } + } } } }; @@ -386,7 +402,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 = () => { @@ -452,6 +468,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) { @@ -475,7 +533,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} @@ -493,6 +551,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 */} +
+ +