fix(clip): clip mode keeps its range, its focus and its keys through the player's own moves
- A mark on the very end seeks a moment short of it; while a clip is marked the end of the item neither closes the player nor steps the queue, and Plex's auto-skip waits. - Plex: Escape and Back peel what is over the picture (info, up-next, the countdown) before the clip; Back under a dialog is the dialog's; no range opens while the length is unknown. - YouTube: the focus goes back to the scissors the user can see, else the player; the panel keeps its name, Loop and focus when the view is maximised or restored under it. - A typed time stays in its field until taken, then shows where it landed; unreadable stays marked invalid; a decimal comma is read (ui/TimecodeField, the editor's fields too). - RangeTrack: a drag is its pointer's, a cancelled drag reports final, an adornment sits beside the slider, a caller-chosen focus ring (amber on the clip strip). CropFrame: a drag is its pointer's. - The editor's remove-cut is a real button at the cut's foot; a segment's keep toggle sits over the cut's hit area.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { useState, type ComponentProps } from "react";
|
||||
import { formatTimecode, parseTimecode } from "../../lib/timecode";
|
||||
import { Input } from "./Input";
|
||||
|
||||
// A field a cut's time is typed into (lib/timecode): the editor's segment ends, the clip panel's In /
|
||||
// Out. A typed draft is the field's until Enter or leaving the field takes it; then the field shows
|
||||
// the value as it landed — the caller may clamp it (a time past the end) — and a time it cannot read
|
||||
// stays, marked invalid, for fixing rather than vanishing or seeming taken. One element throughout,
|
||||
// so Enter leaves the focus in the field (a field re-created per value dropped it on <body>).
|
||||
|
||||
type Props = Omit<
|
||||
ComponentProps<typeof Input>,
|
||||
"value" | "defaultValue" | "onChange" | "invalid"
|
||||
> & {
|
||||
/** Seconds. */
|
||||
value: number;
|
||||
/** Show hours (pass `duration >= 3600`, so one item's fields agree). */
|
||||
hours?: boolean;
|
||||
/** A readable time was typed (not called when it equals `value`). */
|
||||
onCommit: (seconds: number) => void;
|
||||
};
|
||||
|
||||
export function TimecodeField({
|
||||
value,
|
||||
hours = false,
|
||||
onCommit,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
...rest
|
||||
}: Props) {
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
const invalid = draft != null && parseTimecode(draft) == null;
|
||||
const commit = () => {
|
||||
if (draft == null) return;
|
||||
const v = parseTimecode(draft);
|
||||
if (v == null) return;
|
||||
setDraft(null);
|
||||
if (Math.abs(v - value) > 1e-3) onCommit(v);
|
||||
};
|
||||
return (
|
||||
<Input
|
||||
{...rest}
|
||||
value={draft ?? formatTimecode(value, hours)}
|
||||
invalid={invalid}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onBlur={(e) => {
|
||||
commit();
|
||||
onBlur?.(e);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
onKeyDown?.(e);
|
||||
if (e.key !== "Enter" || e.defaultPrevented) return;
|
||||
e.preventDefault();
|
||||
commit();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user