diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-31 04:03:52 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-31 04:03:52 +0200 |
| commit | dcf54c1250431f5a4a2848f7c07781ef5eebacaf (patch) | |
| tree | 64270f82d879ce0b0a028384e513ea87940da630 /apps/web/src/components/editable-timecode.tsx | |
| parent | 5cc6abc3a44c9d046a1cb71a8c916f1d35827e8f (diff) | |
| download | opencut-dcf54c1250431f5a4a2848f7c07781ef5eebacaf.tar.gz | |
feat: migrate time utilities from typescript to rust WASM
Diffstat (limited to 'apps/web/src/components/editable-timecode.tsx')
| -rw-r--r-- | apps/web/src/components/editable-timecode.tsx | 299 |
1 files changed, 149 insertions, 150 deletions
diff --git a/apps/web/src/components/editable-timecode.tsx b/apps/web/src/components/editable-timecode.tsx index c346ad53..6a323a1c 100644 --- a/apps/web/src/components/editable-timecode.tsx +++ b/apps/web/src/components/editable-timecode.tsx @@ -1,150 +1,149 @@ -"use client";
-
-import { useEffect, useRef, useState } from "react";
-import { formatTimeCode, parseTimeCode } from "@/lib/time";
-import type { TTimeCode } from "@/lib/time";
-import { cn } from "@/utils/ui";
-
-interface EditableTimecodeProps {
- time: number;
- duration: number;
- format?: TTimeCode;
- fps: number;
- onTimeChange?: ({ time }: { time: number }) => void;
- className?: string;
- disabled?: boolean;
-}
-
-export function EditableTimecode({
- time,
- duration,
- format = "HH:MM:SS:FF",
- fps,
- 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({ timeInSeconds: 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({ timeCode: inputValue, format, fps });
-
- if (parsedTime === null) {
- setHasError(true);
- return;
- }
-
- const clampedTime = Math.max(
- 0,
- duration ? Math.min(duration, parsedTime) : parsedTime,
- );
-
- onTimeChange?.({ time: clampedTime });
- setIsEditing(false);
- setInputValue("");
- setHasError(false);
- enterPressedRef.current = false;
- };
-
- const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
- if (event.key === "Enter") {
- event.preventDefault();
- enterPressedRef.current = true;
- applyEdit();
- } else if (event.key === "Escape") {
- event.preventDefault();
- cancelEditing();
- }
- };
-
- const handleInputChange = ({
- target,
- }: React.ChangeEvent<HTMLInputElement>) => {
- setInputValue(target.value);
- setHasError(false);
- };
-
- const handleBlur = () => {
- if (!enterPressedRef.current && isEditing) {
- applyEdit();
- }
- };
-
- const handleDisplayKeyDown = (
- event: React.KeyboardEvent<HTMLButtonElement>,
- ) => {
- if (disabled) return;
-
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault();
- startEditing();
- }
- };
-
- 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(
- "-mx-1 border border-transparent bg-transparent px-1 font-mono text-xs outline-none",
- "focus:bg-background focus:border-primary focus:rounded",
- "text-primary tabular-nums",
- hasError && "text-destructive focus:border-destructive",
- className,
- )}
- style={{ width: `${formattedTime.length + 1}ch` }}
- placeholder={formattedTime}
- />
- );
- }
-
- return (
- <button
- type="button"
- onClick={startEditing}
- onKeyDown={handleDisplayKeyDown}
- disabled={disabled}
- className={cn(
- "text-primary cursor-pointer font-mono text-xs tabular-nums",
- "hover:bg-muted/50 -mx-1 px-1 hover:rounded",
- disabled && "cursor-default hover:bg-transparent",
- className,
- )}
- title={disabled ? undefined : "Click to edit time"}
- >
- {formattedTime}
- </button>
- );
-}
+"use client"; + +import { useEffect, useRef, useState } from "react"; +import { formatTimeCode, parseTimeCode, type TimeCodeFormat } from "opencut-wasm"; +import { cn } from "@/utils/ui"; + +interface EditableTimecodeProps { + time: number; + duration: number; + format?: TimeCodeFormat; + fps: number; + onTimeChange?: ({ time }: { time: number }) => void; + className?: string; + disabled?: boolean; +} + +export function EditableTimecode({ + time, + duration, + format = "HH:MM:SS:FF", + fps, + 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({ timeInSeconds: 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({ timeCode: inputValue, format, fps }); + + if (parsedTime == null) { + setHasError(true); + return; + } + + const clampedTime = Math.max( + 0, + duration ? Math.min(duration, parsedTime) : parsedTime, + ); + + onTimeChange?.({ time: clampedTime }); + setIsEditing(false); + setInputValue(""); + setHasError(false); + enterPressedRef.current = false; + }; + + const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { + if (event.key === "Enter") { + event.preventDefault(); + enterPressedRef.current = true; + applyEdit(); + } else if (event.key === "Escape") { + event.preventDefault(); + cancelEditing(); + } + }; + + const handleInputChange = ({ + target, + }: React.ChangeEvent<HTMLInputElement>) => { + setInputValue(target.value); + setHasError(false); + }; + + const handleBlur = () => { + if (!enterPressedRef.current && isEditing) { + applyEdit(); + } + }; + + const handleDisplayKeyDown = ( + event: React.KeyboardEvent<HTMLButtonElement>, + ) => { + if (disabled) return; + + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + startEditing(); + } + }; + + 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( + "-mx-1 border border-transparent bg-transparent px-1 font-mono text-xs outline-none", + "focus:bg-background focus:border-primary focus:rounded", + "text-primary tabular-nums", + hasError && "text-destructive focus:border-destructive", + className, + )} + style={{ width: `${formattedTime.length + 1}ch` }} + placeholder={formattedTime} + /> + ); + } + + return ( + <button + type="button" + onClick={startEditing} + onKeyDown={handleDisplayKeyDown} + disabled={disabled} + className={cn( + "text-primary cursor-pointer font-mono text-xs tabular-nums", + "hover:bg-muted/50 -mx-1 px-1 hover:rounded", + disabled && "cursor-default hover:bg-transparent", + className, + )} + title={disabled ? undefined : "Click to edit time"} + > + {formattedTime} + </button> + ); +} |
