From f15508df62ca407522de448f21711c0711601c91 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 30 Jul 2025 17:44:19 +0200 Subject: feat: make timestamp editable --- apps/web/src/components/editor/preview-panel.tsx | 34 +++--- apps/web/src/components/ui/editable-timecode.tsx | 137 +++++++++++++++++++++++ 2 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/components/ui/editable-timecode.tsx (limited to 'apps/web/src/components') diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 5d038d0f..4cc35bb5 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -20,6 +20,7 @@ import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react"; import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; +import { EditableTimecode } from "@/components/ui/editable-timecode"; import { FONT_CLASS_MAP } from "@/lib/font-config"; import { BackgroundSettings } from "../background-settings"; import { useProjectStore } from "@/stores/project-store"; @@ -562,7 +563,7 @@ function FullscreenToolbar({ toggle: () => void; getTotalDuration: () => number; }) { - const { isPlaying } = usePlaybackStore(); + const { isPlaying, seek } = usePlaybackStore(); const { activeProject } = useProjectStore(); const [isDragging, setIsDragging] = useState(false); @@ -622,9 +623,15 @@ function FullscreenToolbar({ className="flex items-center gap-2 p-1 pt-2 w-full text-white" >
- - {formatTimeCode(currentTime, "HH:MM:SS:FF", activeProject?.fps || 30)} - + / {formatTimeCode( @@ -799,7 +806,7 @@ function PreviewToolbar({ toggle: () => void; getTotalDuration: () => number; }) { - const { isPlaying } = usePlaybackStore(); + const { isPlaying, seek } = usePlaybackStore(); const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore(); const { activeProject } = useProjectStore(); const { @@ -842,17 +849,18 @@ function PreviewToolbar({

- - {formatTimeCode( - currentTime, - "HH:MM:SS:FF", - activeProject?.fps || 30 - )} - + / {formatTimeCode( diff --git a/apps/web/src/components/ui/editable-timecode.tsx b/apps/web/src/components/ui/editable-timecode.tsx new file mode 100644 index 00000000..f6d29f49 --- /dev/null +++ b/apps/web/src/components/ui/editable-timecode.tsx @@ -0,0 +1,137 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { cn } from "@/lib/utils"; +import { formatTimeCode, parseTimeCode } from "@/lib/time"; + +interface EditableTimecodeProps { + time: number; + duration?: number; + format?: "MM:SS" | "HH:MM:SS" | "HH:MM:SS:CS" | "HH:MM:SS:FF"; + fps?: number; + onTimeChange?: (time: number) => void; + className?: string; + disabled?: boolean; +} + +export function EditableTimecode({ + time, + duration, + format = "HH:MM:SS:FF", + fps = 30, + onTimeChange, + className, + disabled = false, +}: EditableTimecodeProps) { + const [isEditing, setIsEditing] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [hasError, setHasError] = useState(false); + const inputRef = useRef(null); + const enterPressedRef = useRef(false); + + const formattedTime = formatTimeCode(time, format, fps); + + const startEditing = () => { + if (disabled) return; + setIsEditing(true); + setInputValue(formattedTime); + setHasError(false); + enterPressedRef.current = false; + }; + + const cancelEditing = () => { + setIsEditing(false); + setInputValue(""); + setHasError(false); + enterPressedRef.current = false; + }; + + const applyEdit = () => { + const parsedTime = parseTimeCode(inputValue, format, fps); + + if (parsedTime === null) { + setHasError(true); + return; + } + + // Clamp time to valid range + const clampedTime = Math.max( + 0, + duration ? Math.min(duration, parsedTime) : parsedTime + ); + + onTimeChange?.(clampedTime); + setIsEditing(false); + setInputValue(""); + setHasError(false); + enterPressedRef.current = false; + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + enterPressedRef.current = true; + applyEdit(); + } else if (e.key === "Escape") { + e.preventDefault(); + cancelEditing(); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value); + setHasError(false); + }; + + const handleBlur = () => { + // Only apply edit if Enter wasn't pressed (to avoid double processing) + if (!enterPressedRef.current && isEditing) { + applyEdit(); + } + }; + + // Focus input when entering edit mode + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + inputRef.current.select(); + } + }, [isEditing]); + + if (isEditing) { + return ( + + ); + } + + return ( + + {formattedTime} + + ); +} -- cgit v1.3.1