mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
feat(studio): default to adaptive thumbnails
This commit is contained in:
@@ -10,7 +10,7 @@ import { TimelineToolbar } from "./TimelineToolbar";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
usePlayerStore.setState({ autoKeyframeEnabled: true });
|
||||
usePlayerStore.setState({ autoKeyframeEnabled: true, thumbnailMode: "adaptive" });
|
||||
});
|
||||
|
||||
function renderToolbar() {
|
||||
@@ -54,3 +54,21 @@ describe("TimelineToolbar — auto-keyframe toggle (#1808)", () => {
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
describe("TimelineToolbar — adaptive thumbnails", () => {
|
||||
it("keeps a user-controlled hidden mode as the rollback path", () => {
|
||||
const { host, root } = renderToolbar();
|
||||
const button = host.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Hide thumbnails — labels only"]',
|
||||
);
|
||||
if (!button) throw new Error("thumbnail toggle not rendered");
|
||||
|
||||
act(() => button.click());
|
||||
|
||||
expect(usePlayerStore.getState().thumbnailMode).toBe("hidden");
|
||||
expect(button.getAttribute("aria-label")).toBe(
|
||||
"Show thumbnails — posters stay visible; richer previews appear on interaction",
|
||||
);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,8 +80,9 @@ 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);
|
||||
const thumbnailMode = usePlayerStore((s) => s.thumbnailMode);
|
||||
const setThumbnailMode = usePlayerStore((s) => s.setThumbnailMode);
|
||||
const thumbnailsVisible = thumbnailMode === "adaptive";
|
||||
// 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);
|
||||
@@ -359,14 +360,24 @@ export function TimelineToolbar({ domEditSession, onSplitElement }: TimelineTool
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<Tooltip label={thumbnailsEnabled ? "Hide clip thumbnails" : "Show clip thumbnails"}>
|
||||
<Tooltip
|
||||
label={
|
||||
thumbnailsVisible
|
||||
? "Hide thumbnails — labels only"
|
||||
: "Show thumbnails — posters stay visible; richer previews appear on interaction"
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={thumbnailsEnabled ? "Hide clip thumbnails" : "Show clip thumbnails"}
|
||||
aria-pressed={thumbnailsEnabled}
|
||||
onClick={() => setThumbnailsEnabled(!thumbnailsEnabled)}
|
||||
aria-label={
|
||||
thumbnailsVisible
|
||||
? "Hide thumbnails — labels only"
|
||||
: "Show thumbnails — posters stay visible; richer previews appear on interaction"
|
||||
}
|
||||
aria-pressed={thumbnailsVisible}
|
||||
onClick={() => setThumbnailMode(thumbnailsVisible ? "hidden" : "adaptive")}
|
||||
className={`h-7 px-2 rounded-md text-[11px] font-medium transition-colors ${
|
||||
thumbnailsEnabled
|
||||
thumbnailsVisible
|
||||
? "bg-studio-accent/10 text-studio-accent"
|
||||
: "text-neutral-400 hover:bg-white/[0.06] hover:text-neutral-200"
|
||||
}`}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useRenderClipContent } from "./useRenderClipContent";
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
afterEach(() => {
|
||||
usePlayerStore.setState({ thumbnailsEnabled: false });
|
||||
usePlayerStore.setState({ thumbnailMode: "hidden" });
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("useRenderClipContent", () => {
|
||||
});
|
||||
|
||||
it("passes empty labels to thumbnail content so TimelineClip owns clip names", () => {
|
||||
usePlayerStore.setState({ thumbnailsEnabled: true });
|
||||
usePlayerStore.setState({ thumbnailMode: "adaptive" });
|
||||
|
||||
const cases: Array<{ content: ReactNode; type: unknown }> = [
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ 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";
|
||||
import { effectiveThumbnailMode } from "../player/lib/thumbnailPolicy";
|
||||
|
||||
export function normalizeCompositionSrc(
|
||||
compSrc: string,
|
||||
@@ -90,7 +91,8 @@ export function useRenderClipContent({
|
||||
}: 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);
|
||||
const thumbnailMode = usePlayerStore((s) => s.thumbnailMode);
|
||||
const effectiveMode = effectiveThumbnailMode(thumbnailMode);
|
||||
return useCallback(
|
||||
// Pre-existing clip-content dispatcher; reduced by extracting renderAudioClip.
|
||||
// fallow-ignore-next-line complexity
|
||||
@@ -100,7 +102,7 @@ export function useRenderClipContent({
|
||||
|
||||
// Thumbnail generation disabled (perf) -> plain clip bars. Audio still shows
|
||||
// its waveform (cheap, not a frame thumbnail). Toggle: timeline toolbar.
|
||||
if (!thumbnailsEnabled) {
|
||||
if (effectiveMode === "hidden") {
|
||||
return el.tag === "audio" ? renderAudioClip(el, pid, style.label) : null;
|
||||
}
|
||||
|
||||
@@ -192,6 +194,6 @@ export function useRenderClipContent({
|
||||
|
||||
return null;
|
||||
},
|
||||
[projectIdRef, compIdToSrc, activePreviewUrl, effectiveTimelineDuration, thumbnailsEnabled],
|
||||
[projectIdRef, compIdToSrc, activePreviewUrl, effectiveTimelineDuration, effectiveMode],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { readStudioUiPreferences, writeStudioUiPreferences } from "../../utils/s
|
||||
import { computePinnedZoomPercent } from "../components/timelineZoom";
|
||||
import { createKeyframeSlice, type KeyframeSlice } from "./keyframeSlice";
|
||||
import { createTimelineFocusRequest, type TimelineFocusRequest } from "./timelineFocusState";
|
||||
import { defaultThumbnailMode, type ThumbnailMode } from "../lib/thumbnailPolicy";
|
||||
|
||||
export type { KeyframeCacheEntry } from "./keyframeSlice";
|
||||
|
||||
@@ -114,7 +115,7 @@ interface PlayerState extends KeyframeSlice {
|
||||
loopEnabled: boolean;
|
||||
/** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses manualZoomPercent */
|
||||
zoomMode: ZoomMode;
|
||||
thumbnailsEnabled: boolean;
|
||||
thumbnailMode: ThumbnailMode;
|
||||
/** Timeline zoom percent relative to the fit width when in manual mode */
|
||||
manualZoomPercent: number;
|
||||
/**
|
||||
@@ -202,7 +203,7 @@ interface PlayerState extends KeyframeSlice {
|
||||
>,
|
||||
) => void;
|
||||
setZoomMode: (mode: ZoomMode) => void;
|
||||
setThumbnailsEnabled: (enabled: boolean) => void;
|
||||
setThumbnailMode: (mode: ThumbnailMode) => void;
|
||||
setManualZoomPercent: (percent: number) => void;
|
||||
bumpZEditVersion: () => void;
|
||||
setInPoint: (time: number | null) => void;
|
||||
@@ -329,7 +330,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
audioMuted: readStudioUiPreferences().audioMuted ?? false,
|
||||
loopEnabled: false,
|
||||
zoomMode: "fit",
|
||||
thumbnailsEnabled: readStudioUiPreferences().thumbnailsEnabled ?? false,
|
||||
thumbnailMode: defaultThumbnailMode(readStudioUiPreferences().thumbnailMode),
|
||||
manualZoomPercent: 100,
|
||||
zEditVersion: 0,
|
||||
timelinePps: 100,
|
||||
@@ -456,9 +457,9 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
writeStudioUiPreferences({ audioMuted: muted });
|
||||
set({ audioMuted: muted });
|
||||
},
|
||||
setThumbnailsEnabled: (enabled) => {
|
||||
writeStudioUiPreferences({ thumbnailsEnabled: enabled });
|
||||
set({ thumbnailsEnabled: enabled });
|
||||
setThumbnailMode: (mode) => {
|
||||
writeStudioUiPreferences({ thumbnailMode: mode });
|
||||
set({ thumbnailMode: mode });
|
||||
},
|
||||
setLoopEnabled: (enabled) => set({ loopEnabled: enabled }),
|
||||
setZoomMode: (mode) => set({ zoomMode: mode }),
|
||||
@@ -587,7 +588,6 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
reset: () => set(createTimelineResetState()),
|
||||
}));
|
||||
|
||||
// Dev-only bug-bash handle; guard import.meta.env for non-Vite bundlers.
|
||||
function isDevBuild(): boolean {
|
||||
try {
|
||||
return import.meta.env.DEV === true;
|
||||
|
||||
@@ -65,6 +65,20 @@ describe("timelineSnapEnabled preference", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("thumbnailMode preference", () => {
|
||||
it("round-trips the adaptive mode", () => {
|
||||
const storage = createStorage();
|
||||
writeStudioUiPreferences({ thumbnailMode: "adaptive" }, storage);
|
||||
expect(readStudioUiPreferences(storage).thumbnailMode).toBe("adaptive");
|
||||
});
|
||||
|
||||
it("migrates the legacy boolean without retaining two owners", () => {
|
||||
const storage = createStorage();
|
||||
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ thumbnailsEnabled: false }));
|
||||
expect(readStudioUiPreferences(storage).thumbnailMode).toBe("hidden");
|
||||
});
|
||||
});
|
||||
|
||||
describe("timeline zoom pin persistence", () => {
|
||||
it("round-trips a pinned manual zoom (survives the post-edit reload)", () => {
|
||||
const storage = createStorage();
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface StudioUiPreferences {
|
||||
timelineHeight?: number;
|
||||
playbackRate?: number;
|
||||
audioMuted?: boolean;
|
||||
thumbnailsEnabled?: boolean;
|
||||
thumbnailMode?: "adaptive" | "hidden";
|
||||
previewZoom?: StoredPreviewZoomState;
|
||||
recentBlocks?: string[];
|
||||
snapEnabled?: boolean;
|
||||
@@ -71,8 +71,10 @@ 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 (parsed.thumbnailMode === "adaptive" || parsed.thumbnailMode === "hidden") {
|
||||
preferences.thumbnailMode = parsed.thumbnailMode;
|
||||
} else if (typeof parsed.thumbnailsEnabled === "boolean") {
|
||||
preferences.thumbnailMode = parsed.thumbnailsEnabled ? "adaptive" : "hidden";
|
||||
}
|
||||
if (isRecord(parsed.previewZoom)) {
|
||||
const { zoomPercent, panX, panY } = parsed.previewZoom;
|
||||
|
||||
Reference in New Issue
Block a user