summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editor/preview-panel.tsx34
-rw-r--r--apps/web/src/components/ui/editable-timecode.tsx137
2 files changed, 158 insertions, 13 deletions
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"
>
<div className="flex items-center gap-1 text-[0.70rem] tabular-nums text-white/90">
- <span className="text-primary">
- {formatTimeCode(currentTime, "HH:MM:SS:FF", activeProject?.fps || 30)}
- </span>
+ <EditableTimecode
+ time={currentTime}
+ duration={totalDuration}
+ format="HH:MM:SS:FF"
+ fps={activeProject?.fps || 30}
+ onTimeChange={seek}
+ disabled={!hasAnyElements}
+ className="text-white/90 hover:bg-white/10"
+ />
<span className="opacity-50">/</span>
<span>
{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({
<div>
<p
className={cn(
- "text-[0.75rem] text-muted-foreground flex items-center gap-1",
+ "text-[0.75rem] text-muted-foreground flex items-center gap-1 w-[10rem]",
!hasAnyElements && "opacity-50"
)}
>
- <span className="text-primary tabular-nums">
- {formatTimeCode(
- currentTime,
- "HH:MM:SS:FF",
- activeProject?.fps || 30
- )}
- </span>
+ <EditableTimecode
+ time={currentTime}
+ duration={getTotalDuration()}
+ format="HH:MM:SS:FF"
+ fps={activeProject?.fps || 30}
+ onTimeChange={seek}
+ disabled={!hasAnyElements}
+ />
<span className="opacity-50">/</span>
<span className="tabular-nums">
{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<HTMLInputElement>(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<HTMLInputElement>) => {
+ 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 (
+ <input
+ ref={inputRef}
+ type="text"
+ value={inputValue}
+ onChange={handleInputChange}
+ onKeyDown={handleKeyDown}
+ onBlur={handleBlur}
+ className={cn(
+ "text-xs font-mono bg-transparent border-none outline-none",
+ "focus:bg-background focus:border focus:border-primary focus:px-1 focus:rounded",
+ "tabular-nums text-primary",
+ hasError && "text-destructive focus:border-destructive",
+ className
+ )}
+ style={{ width: `${formattedTime.length + 1}ch` }}
+ placeholder={formattedTime}
+ />
+ );
+ }
+
+ return (
+ <span
+ onClick={startEditing}
+ className={cn(
+ "text-xs font-mono tabular-nums text-primary cursor-pointer",
+ "hover:bg-muted/50 hover:rounded px-1 -mx-1 transition-colors",
+ disabled && "cursor-default hover:bg-transparent",
+ className
+ )}
+ title={disabled ? undefined : "Click to edit time"}
+ >
+ {formattedTime}
+ </span>
+ );
+}