feat: add Studio NLE playback controls

This commit is contained in:
Miguel Ángel
2026-04-28 17:51:38 -04:00
parent a18f66db18
commit a45f900af7
8 changed files with 435 additions and 33 deletions
@@ -16,6 +16,7 @@ describe("usePlayerStore", () => {
expect(state.elements).toEqual([]);
expect(state.selectedElementId).toBeNull();
expect(state.playbackRate).toBe(1);
expect(state.loopEnabled).toBe(false);
expect(state.zoomMode).toBe("fit");
expect(state.manualZoomPercent).toBe(100);
});
@@ -61,6 +62,13 @@ describe("usePlayerStore", () => {
});
});
describe("setLoopEnabled", () => {
it("updates loopEnabled", () => {
usePlayerStore.getState().setLoopEnabled(true);
expect(usePlayerStore.getState().loopEnabled).toBe(true);
});
});
describe("setTimelineReady", () => {
it("updates timelineReady", () => {
usePlayerStore.getState().setTimelineReady(true);
@@ -205,9 +213,10 @@ describe("usePlayerStore", () => {
expect(state.selectedElementId).toBeNull();
});
it("does not reset playbackRate, zoomMode, or manualZoomPercent", () => {
it("does not reset playbackRate, loopEnabled, zoomMode, or manualZoomPercent", () => {
const store = usePlayerStore.getState();
store.setPlaybackRate(2);
store.setLoopEnabled(true);
store.setZoomMode("manual");
store.setManualZoomPercent(200);
@@ -216,6 +225,7 @@ describe("usePlayerStore", () => {
const state = usePlayerStore.getState();
// reset() only resets the fields explicitly listed in the reset function
expect(state.playbackRate).toBe(2);
expect(state.loopEnabled).toBe(true);
expect(state.zoomMode).toBe("manual");
expect(state.manualZoomPercent).toBe(200);
});
@@ -34,6 +34,7 @@ interface PlayerState {
elements: TimelineElement[];
selectedElementId: string | null;
playbackRate: number;
loopEnabled: boolean;
/** Timeline zoom: 'fit' auto-scales to viewport, 'manual' uses manualZoomPercent */
zoomMode: ZoomMode;
/** Timeline zoom percent relative to the fit width when in manual mode */
@@ -43,6 +44,7 @@ interface PlayerState {
setCurrentTime: (time: number) => void;
setDuration: (duration: number) => void;
setPlaybackRate: (rate: number) => void;
setLoopEnabled: (enabled: boolean) => void;
setTimelineReady: (ready: boolean) => void;
setElements: (elements: TimelineElement[]) => void;
setSelectedElementId: (id: string | null) => void;
@@ -76,11 +78,13 @@ export const usePlayerStore = create<PlayerState>((set) => ({
elements: [],
selectedElementId: null,
playbackRate: 1,
loopEnabled: false,
zoomMode: "fit",
manualZoomPercent: 100,
setIsPlaying: (playing) => set({ isPlaying: playing }),
setPlaybackRate: (rate) => set({ playbackRate: rate }),
setLoopEnabled: (enabled) => set({ loopEnabled: enabled }),
setZoomMode: (mode) => set({ zoomMode: mode }),
setManualZoomPercent: (percent) =>
set({ manualZoomPercent: Math.max(10, Math.min(2000, Math.round(percent))) }),
@@ -96,7 +100,7 @@ export const usePlayerStore = create<PlayerState>((set) => ({
),
})),
// Resets project-specific state when switching compositions.
// playbackRate, zoomMode, and manualZoomPercent are intentionally preserved
// playbackRate, loopEnabled, zoomMode, and manualZoomPercent are intentionally preserved
// because they are user preferences that should survive project switches.
reset: () =>
set({