mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-12 20:47:30 +00:00
feat(studio): timeline clip thumbnails off by default with a toolbar toggle
Background thumbnail generation (per-clip composition/video frame capture) lags timeline scrubbing on long videos (#2428). Adds a persisted thumbnailsEnabled setting, default OFF, so the timeline is snappy out of the box; a toggle next to Fit in the zoom toolbar turns thumbnails on. When off, clips render as plain bars (audio still shows its waveform).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Magnet, MagnifyingGlassMinus, MagnifyingGlassPlus } from "@phosphor-icons/react";
|
||||
import { Image, Magnet, MagnifyingGlassMinus, MagnifyingGlassPlus } from "@phosphor-icons/react";
|
||||
import {
|
||||
useEnableKeyframes,
|
||||
isPlayheadWithinTween,
|
||||
@@ -80,6 +80,8 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
||||
const setTimelineSnapEnabled = usePlayerStore((s) => s.setTimelineSnapEnabled);
|
||||
const autoKeyframeEnabled = usePlayerStore((s) => s.autoKeyframeEnabled);
|
||||
const setAutoKeyframeEnabled = usePlayerStore((s) => s.setAutoKeyframeEnabled);
|
||||
const thumbnailsEnabled = usePlayerStore((s) => s.thumbnailsEnabled);
|
||||
const setThumbnailsEnabled = usePlayerStore((s) => s.setThumbnailsEnabled);
|
||||
// Subscribe so the add-beat button reacts to playhead movement and analysis load.
|
||||
const currentTime = usePlayerStore((s) => s.currentTime);
|
||||
const beatAnalysisReady = usePlayerStore((s) => s.beatAnalysis !== null);
|
||||
@@ -357,6 +359,21 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Tooltip label={thumbnailsEnabled ? "Hide clip thumbnails" : "Show clip thumbnails"}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={thumbnailsEnabled ? "Hide clip thumbnails" : "Show clip thumbnails"}
|
||||
aria-pressed={thumbnailsEnabled}
|
||||
onClick={() => setThumbnailsEnabled(!thumbnailsEnabled)}
|
||||
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
|
||||
thumbnailsEnabled
|
||||
? "bg-studio-accent/10 text-studio-accent"
|
||||
: "text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200"
|
||||
}`}
|
||||
>
|
||||
<Image size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip label="Fit timeline to width">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { TimelineElement } from "../player";
|
||||
import { AudioWaveform } from "../player/components/AudioWaveform";
|
||||
import { ImageThumbnail } from "../player/components/ImageThumbnail";
|
||||
import { encodePreviewPath, resolveMediaPreviewUrl } from "../player/components/thumbnailUtils";
|
||||
import { usePlayerStore } from "../player/store/playerStore";
|
||||
|
||||
export function normalizeCompositionSrc(
|
||||
compSrc: string,
|
||||
@@ -87,6 +88,9 @@ export function useRenderClipContent({
|
||||
activePreviewUrl,
|
||||
effectiveTimelineDuration,
|
||||
}: UseRenderClipContentOptions) {
|
||||
// Self-sourced (not threaded via App) so the toggle gates generation without
|
||||
// App.tsx plumbing. Off by default -> plain clip bars, snappy timeline (#2428).
|
||||
const thumbnailsEnabled = usePlayerStore((s) => s.thumbnailsEnabled);
|
||||
return useCallback(
|
||||
// Pre-existing clip-content dispatcher; reduced by extracting renderAudioClip.
|
||||
// fallow-ignore-next-line complexity
|
||||
@@ -94,6 +98,12 @@ export function useRenderClipContent({
|
||||
const pid = projectIdRef.current;
|
||||
if (!pid) return null;
|
||||
|
||||
// Thumbnail generation disabled (perf) -> plain clip bars. Audio still shows
|
||||
// its waveform (cheap, not a frame thumbnail). Toggle: timeline toolbar.
|
||||
if (!thumbnailsEnabled) {
|
||||
return el.tag === "audio" ? renderAudioClip(el, pid, style.label) : null;
|
||||
}
|
||||
|
||||
let compSrc = el.compositionSrc;
|
||||
if (compSrc) {
|
||||
compSrc = normalizeCompositionSrc(compSrc, pid, window.location.origin);
|
||||
@@ -182,6 +192,6 @@ export function useRenderClipContent({
|
||||
|
||||
return null;
|
||||
},
|
||||
[projectIdRef, compIdToSrc, activePreviewUrl, effectiveTimelineDuration],
|
||||
[projectIdRef, compIdToSrc, activePreviewUrl, effectiveTimelineDuration, thumbnailsEnabled],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ interface PlayerState extends KeyframeSlice {
|
||||
loopEnabled: boolean;
|
||||
/** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses manualZoomPercent */
|
||||
zoomMode: ZoomMode;
|
||||
thumbnailsEnabled: boolean;
|
||||
/** Timeline zoom percent relative to the fit width when in manual mode */
|
||||
manualZoomPercent: number;
|
||||
/**
|
||||
@@ -196,6 +197,7 @@ interface PlayerState extends KeyframeSlice {
|
||||
>,
|
||||
) => void;
|
||||
setZoomMode: (mode: ZoomMode) => void;
|
||||
setThumbnailsEnabled: (enabled: boolean) => void;
|
||||
setManualZoomPercent: (percent: number) => void;
|
||||
bumpZEditVersion: () => void;
|
||||
setInPoint: (time: number | null) => void;
|
||||
@@ -294,6 +296,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
audioMuted: readStudioUiPreferences().audioMuted ?? false,
|
||||
loopEnabled: false,
|
||||
zoomMode: "fit",
|
||||
thumbnailsEnabled: readStudioUiPreferences().thumbnailsEnabled ?? false,
|
||||
manualZoomPercent: 100,
|
||||
zEditVersion: 0,
|
||||
timelinePps: 100,
|
||||
@@ -409,6 +412,10 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
writeStudioUiPreferences({ audioMuted: muted });
|
||||
set({ audioMuted: muted });
|
||||
},
|
||||
setThumbnailsEnabled: (enabled) => {
|
||||
writeStudioUiPreferences({ thumbnailsEnabled: enabled });
|
||||
set({ thumbnailsEnabled: enabled });
|
||||
},
|
||||
setLoopEnabled: (enabled) => set({ loopEnabled: enabled }),
|
||||
setZoomMode: (mode) => set({ zoomMode: mode }),
|
||||
clearSelectedElementIds: () => set({ selectedElementIds: new Set() }),
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface StudioUiPreferences {
|
||||
timelineHeight?: number;
|
||||
playbackRate?: number;
|
||||
audioMuted?: boolean;
|
||||
thumbnailsEnabled?: boolean;
|
||||
previewZoom?: StoredPreviewZoomState;
|
||||
recentBlocks?: string[];
|
||||
snapEnabled?: boolean;
|
||||
@@ -70,6 +71,9 @@ function readStorage(storage: Storage | null): StudioUiPreferences {
|
||||
if (typeof parsed.audioMuted === "boolean") {
|
||||
preferences.audioMuted = parsed.audioMuted;
|
||||
}
|
||||
if (typeof parsed.thumbnailsEnabled === "boolean") {
|
||||
preferences.thumbnailsEnabled = parsed.thumbnailsEnabled;
|
||||
}
|
||||
if (isRecord(parsed.previewZoom)) {
|
||||
const { zoomPercent, panX, panY } = parsed.previewZoom;
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user