diff --git a/apps/frontend/src/components/editor/EditorControls.tsx b/apps/frontend/src/components/editor/EditorControls.tsx index c5a928a5..dae877a4 100644 --- a/apps/frontend/src/components/editor/EditorControls.tsx +++ b/apps/frontend/src/components/editor/EditorControls.tsx @@ -1,4 +1,9 @@ -import { useMemo, type CSSProperties, type ReactElement } from "react"; +import { + useMemo, + type CSSProperties, + type ReactElement, + type ReactNode, +} from "react"; import { Camera, Pause, @@ -24,6 +29,7 @@ import { DrawerTrigger, } from "@/components/ui/drawer"; import { EditorEditableTimecode } from "@/components/editor/EditorEditableTimecode"; +import { handleHorizontalScrollKeyDown } from "@/components/ui/scroll-area"; import { EditorPreviewTimecode } from "@/components/editor/EditorPreviewTimecode"; import { formatTime, @@ -34,6 +40,7 @@ type EditorControlsVariant = "desktop" | "mobile"; interface EditorControlsProperties { variant?: EditorControlsVariant; + playbackSourcePanel: ReactNode; playing: boolean; loadingPreview: boolean; togglePlay: () => void; @@ -87,6 +94,7 @@ function ControlTooltip({ export function EditorControls({ variant = "desktop", + playbackSourcePanel, playing, loadingPreview, togglePlay, @@ -131,6 +139,9 @@ export function EditorControls({ }), [duration], ); + const mobileClipMetricStyle: CSSProperties = { + minWidth: `max(8rem, ${formatTimecodeInput(duration).length + 2}ch)`, + }; const volumeRangeFillPercent = `${ Math.min(Math.max(muted ? 0 : volume, 0), 1) * 100 }%`; @@ -170,6 +181,11 @@ export function EditorControls({ > @@ -277,7 +293,7 @@ export function EditorControls({ className={rangeActionButtonClassName} aria-label="Set in point at the playhead" > - In + Set in - Out + Set out - {clipMetrics.map((metric) => ( -
- + {clipMetrics.map((metric) => { + const label = ( + {metric.label} - {metric.onCommit ? ( - - + ); + return ( +
+ {(variant === "desktop" || !metric.onCommit) && label} + {metric.onCommit ? ( + + {variant === "mobile" && label} + + {formatTime(metric.value)} + + + ) : ( + {formatTime(metric.value)} - - ) : ( - - {formatTime(metric.value)} - - )} -
- ))} + )} +
+ ); + })} ); @@ -360,7 +403,7 @@ export function EditorControls({
{playControl}
-
+
{previewTimeControl}
@@ -380,7 +423,8 @@ export function EditorControls({ Additional clip controls -
+
+
{playbackSourcePanel}
Playback @@ -413,8 +457,14 @@ export function EditorControls({
-
- {editableClipMetrics} +
+
{editableClipMetrics}
{rangeActions} diff --git a/apps/frontend/src/components/editor/EditorEditableTimecode.tsx b/apps/frontend/src/components/editor/EditorEditableTimecode.tsx index fb373e2b..2aa3136f 100644 --- a/apps/frontend/src/components/editor/EditorEditableTimecode.tsx +++ b/apps/frontend/src/components/editor/EditorEditableTimecode.tsx @@ -12,6 +12,8 @@ import { formatTimecodeInput, parseTimecodeInput, } from "@/components/editor/editorUtilities"; +import { cn } from "@/lib/utilities"; +import { Popover } from "radix-ui"; interface EditorEditableTimecodeProperties { ariaLabel: string; @@ -139,30 +141,52 @@ export function EditorEditableTimecode({ }} > {editing ? ( - commitDraft({ restoreFocus: false })} - onChange={(event) => { - setDraftValue(event.target.value); - setInvalid(false); - }} - onKeyDown={handleInputKeyDown} - spellCheck={false} - style={{ width: inputWidth ?? "100%" }} - type="text" - value={draftValue} - /> + + + commitDraft({ restoreFocus: false })} + onChange={(event) => { + setDraftValue(event.target.value); + setInvalid(false); + }} + onKeyDown={handleInputKeyDown} + spellCheck={false} + style={{ width: inputWidth ?? "100%" }} + type="text" + value={draftValue} + /> + + + event.preventDefault()} + onCloseAutoFocus={(event) => event.preventDefault()} + className="z-[60] w-52 max-w-[70vw] rounded-md border border-destructive bg-popover p-2 text-xs font-normal text-popover-foreground shadow-md" + > + Enter seconds (12.5), m:ss (1:23), or h:mm:ss (1:02:03). Press + Escape to discard this edit. + + + ) : ( + ); + const resetButton = ( + + + + + Reset draft + + ); + const resetDescription = + "This removes the saved clip range and subtitle edits from this device. Your original video is unchanged."; + const resetActions = ( +
+ + +
+ ); return ( -
+
- + + {draftNotice} + + {mobile ? ( + + + + + + + Editor options + + Manage this device’s draft and view keyboard shortcuts. + + + {shortcutsButton} + {resetButton} + + + ) : ( + <> + {shortcutsButton} + {resetButton} + + )} + {mobile ? ( + + { + event.preventDefault(); + cancelResetReference.current?.focus(); + }} + onCloseAutoFocus={(event) => { + event.preventDefault(); + menuTriggerReference.current?.focus(); + }} + > + + Reset draft? + {resetDescription} + + {resetActions} + + + ) : ( + setResetOpen(false)} + title="Reset draft?" + description={resetDescription} + closeLabel="Close reset confirmation" + initialFocus={cancelResetReference} + > + {resetActions} + + )} setHelpOpen(false)} diff --git a/apps/frontend/src/components/editor/EditorLayout.tsx b/apps/frontend/src/components/editor/EditorLayout.tsx index f7b78fc3..68518978 100644 --- a/apps/frontend/src/components/editor/EditorLayout.tsx +++ b/apps/frontend/src/components/editor/EditorLayout.tsx @@ -11,6 +11,7 @@ import { EDITOR_RESIZE_TARGET_MINIMUM_SIZE, } from "@/components/editor/editorLayoutSizing"; import { EditorSidebar } from "@/components/editor/EditorSidebar"; +import { BarsLoader } from "@/components/ui/bars-loader"; import { cliparrMotionTransitions } from "@/lib/motionPresets"; export type EditorLayoutVariant = "desktop" | "mobile"; @@ -46,13 +47,20 @@ export function EditorPreviewPane({ : cliparrMotionTransitions.fast; const previewStage = (
- {children} + {variant === "mobile" ? ( +
+ {children} +
+ ) : ( + children + )}
); @@ -93,11 +101,6 @@ export function EditorTimelinePane({ hasDuration: boolean; timeline: ReactNode; }) { - const reduceMotion = useReducedMotion(); - const stateTransition = reduceMotion - ? { duration: 0 } - : cliparrMotionTransitions.standard; - return (
{controls} - - {hasDuration ? ( - - {timeline} - - ) : ( - +
+ {timeline} +
+ {!hasDuration && ( +
- Waiting for media duration. - + +
)} -
+
); } @@ -271,13 +267,11 @@ export function EditorDesktopLayout({ export function EditorMobileLayout({ error, - playbackSourcePanel, previewPane, timelinePane, subtitlePanel, }: { error: string | null; - playbackSourcePanel: ReactNode; previewPane: ReactNode; timelinePane: ReactNode; subtitlePanel: ReactNode; @@ -290,7 +284,6 @@ export function EditorMobileLayout({
)} - {playbackSourcePanel} {previewPane} {timelinePane} {subtitlePanel} diff --git a/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx b/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx index 6272c083..3be93aea 100644 --- a/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx +++ b/apps/frontend/src/components/editor/EditorPlaybackSourcePanel.tsx @@ -1,89 +1,44 @@ -import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { EditorPropertyRow } from "@/components/editor/EditorPropertyControls"; import { cn } from "@/lib/utilities"; -import { cliparrMotionTransitions } from "@/lib/motionPresets"; interface EditorPlaybackSourcePanelProperties { previewSourceLabel: string; fallbackMessage: string | null; - hasHlsSource: boolean; className?: string; } function displaySourceLabel(label: string) { - if (label === "Direct source") { - return "Direct media"; + switch (label) { + case "HLS stream": + case "HLS URL": { + return "HLS"; + } + case "Direct source": { + return "Direct"; + } + default: { + return label.trim() || "Resolving stream"; + } } - - if (!label.trim()) { - return "Resolving stream"; - } - - return label; } -const SOURCE_NOTE_INITIAL = { - opacity: 0, - y: 4, - filter: "blur(6px)", -}; -const SOURCE_NOTE_VISIBLE = { - opacity: 1, - y: 0, - filter: "blur(0px)", -}; -const SOURCE_NOTE_EXIT = { - opacity: 0, - y: -3, - filter: "blur(6px)", -}; - export function EditorPlaybackSourcePanel({ previewSourceLabel, fallbackMessage, - hasHlsSource, className, }: EditorPlaybackSourcePanelProperties) { - const reduceMotion = useReducedMotion(); - const transition = reduceMotion - ? { duration: 0 } - : cliparrMotionTransitions.fast; - const sourceNote = - fallbackMessage ?? - (!hasHlsSource && previewSourceLabel === "Direct source" - ? "Direct media only." - : null); - return ( -
-
-
-
- Preview Source -
-
- -
-
- {displaySourceLabel(previewSourceLabel)} -
- - - {sourceNote && ( - - {sourceNote} - - )} - -
-
+
+ + + {displaySourceLabel(previewSourceLabel)} + + + {fallbackMessage && ( +

+ {fallbackMessage} +

+ )}
); } diff --git a/apps/frontend/src/components/editor/EditorPreview.tsx b/apps/frontend/src/components/editor/EditorPreview.tsx index 6afae705..03047e5c 100644 --- a/apps/frontend/src/components/editor/EditorPreview.tsx +++ b/apps/frontend/src/components/editor/EditorPreview.tsx @@ -1,4 +1,5 @@ -import { LoaderCircle, Play } from "lucide-react"; +import { Play } from "lucide-react"; +import { BarsLoader } from "@/components/ui/bars-loader"; import type { RefCallback } from "react"; import type { MediaDimensions } from "@/lib/editorMedia"; import type { ReactNode } from "react"; @@ -44,6 +45,7 @@ export function EditorPreview({
- - {loadingStatus} + {showLoadingOverlay && }
); diff --git a/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx b/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx index 5e686f7c..74aa02c4 100644 --- a/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx +++ b/apps/frontend/src/components/editor/EditorPreviewTimecode.tsx @@ -1,7 +1,9 @@ import { memo, useLayoutEffect, useMemo, useRef, type RefObject } from "react"; +import { cn } from "@/lib/utilities"; interface EditorPreviewTimecodeProperties { ariaHidden?: boolean; + className?: string; currentTime: number; duration: number; } @@ -123,6 +125,7 @@ function arePreviewTimecodePropertiesEqual( ) { return ( previous.ariaHidden === next.ariaHidden && + previous.className === next.className && getPreviewCentiseconds(previous.currentTime) === getPreviewCentiseconds(next.currentTime) && getPreviewCentiseconds(previous.duration) === @@ -171,6 +174,7 @@ const TimecodeShell = memo(function TimecodeShell({ export const EditorPreviewTimecode = memo(function EditorPreviewTimecode({ ariaHidden = false, + className, currentTime, duration, }: EditorPreviewTimecodeProperties) { @@ -222,7 +226,10 @@ export const EditorPreviewTimecode = memo(function EditorPreviewTimecode({