From 4c85528b8602d994f00f8ab314256902778e8f39 Mon Sep 17 00:00:00 2001 From: Minit Date: Sun, 5 Apr 2026 00:49:02 +0530 Subject: [PATCH 1/4] Fix trim handle flickering in editor Wrap trim handle state updates in batch() so project segment and previewTime update atomically before effects fire. Reorder effects in Editor.tsx so the config-update effect is created before the render-frame effect (SolidJS fires effects in creation order). Add skipRenderFrameForConfigUpdate flag so renderFrameEvent is suppressed when updateConfigAndRender already handles the render, eliminating the race condition where a stale-config frame was emitted before the async config update completed. Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/routes/editor/Editor.tsx | 65 +++++++++++-------- .../src/routes/editor/Timeline/ClipTrack.tsx | 43 ++++++------ 2 files changed, 62 insertions(+), 46 deletions(-) diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index 4842726fb4..ab53888692 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -365,7 +365,13 @@ function Inner() { setEditorState("playbackTime", payload.playhead_position / FPS); }); + let skipRenderFrameForConfigUpdate = false; + const emitRenderFrame = (time: number) => { + if (skipRenderFrameForConfigUpdate) { + skipRenderFrameForConfigUpdate = false; + return; + } if (!editorState.playing) { events.renderFrameEvent.emit({ frame_number: Math.max(Math.floor(time * FPS), 0), @@ -390,33 +396,6 @@ function Inner() { return editorState.playbackTime; }); - createEffect( - on( - () => [frameNumberToRender(), previewResolutionBase()], - ([number]) => { - if (editorState.playing) return; - renderFrame(number as number); - }, - { defer: false }, - ), - ); - - createEffect( - on(isExportMode, (exportMode, prevExportMode) => { - if (prevExportMode === true && exportMode === false) { - emitRenderFrame(frameNumberToRender()); - } - }), - ); - - createEffect( - on(isCropMode, (cropMode, prevCropMode) => { - if (prevCropMode === true && cropMode === false) { - emitRenderFrame(frameNumberToRender()); - } - }), - ); - const doConfigUpdate = async (time: number) => { const config = getPreviewProjectConfig(project, editorState); const frameNumber = Math.max(Math.floor(time * FPS), 0); @@ -441,6 +420,7 @@ function Inner() { throttledConfigUpdate(time); trailingConfigUpdate(time); }; + createEffect( on( () => { @@ -451,12 +431,43 @@ function Inner() { }; }, () => { + skipRenderFrameForConfigUpdate = true; + queueMicrotask(() => { + skipRenderFrameForConfigUpdate = false; + }); updateConfigAndRender(frameNumberToRender()); }, { defer: true }, ), ); + createEffect( + on( + () => [frameNumberToRender(), previewResolutionBase()], + ([number]) => { + if (editorState.playing) return; + renderFrame(number as number); + }, + { defer: false }, + ), + ); + + createEffect( + on(isExportMode, (exportMode, prevExportMode) => { + if (prevExportMode === true && exportMode === false) { + emitRenderFrame(frameNumberToRender()); + } + }), + ); + + createEffect( + on(isCropMode, (cropMode, prevCropMode) => { + if (prevCropMode === true && cropMode === false) { + emitRenderFrame(frameNumberToRender()); + } + }), + ); + const fullscreenMode = () => { if (isExportMode()) return "export" as const; return null; diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 68c13f09e1..368bbf95e9 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -5,6 +5,7 @@ import { import { cx } from "cva"; import { type ComponentProps, + batch, createEffect, createMemo, createRoot, @@ -718,14 +719,16 @@ export function ClipTrack( initialStart, }); - setProject( - "timeline", - "segments", - i(), - "start", - clampedStart, - ); - setPreviewTime(prevDuration()); + batch(() => { + setProject( + "timeline", + "segments", + i(), + "start", + clampedStart, + ); + setPreviewTime(prevDuration()); + }); } const resumeHistory = projectHistory.pause(); @@ -822,17 +825,19 @@ export function ClipTrack( seg.start + minRecordedDuration, ); - setProject( - "timeline", - "segments", - i(), - "end", - clampedEnd, - ); - setPreviewTime( - prevDuration() + - (clampedEnd - seg.start) / seg.timescale, - ); + batch(() => { + setProject( + "timeline", + "segments", + i(), + "end", + clampedEnd, + ); + setPreviewTime( + prevDuration() + + (clampedEnd - seg.start) / seg.timescale, + ); + }); } const resumeHistory = projectHistory.pause(); From 20eafba6c2da896c2ef6ae2dbedacb5497e56658 Mon Sep 17 00:00:00 2001 From: Minit Date: Sun, 5 Apr 2026 11:48:01 +0530 Subject: [PATCH 2/4] fix: let queueMicrotask be sole owner of skipRenderFrameForConfigUpdate reset Removing the early reset inside emitRenderFrame prevents a second synchronous call (e.g. export/crop mode exit handlers) from bypassing the guard before updateProjectConfigInMemory completes. Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/routes/editor/Editor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index ab53888692..2c7865b75b 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -369,7 +369,6 @@ function Inner() { const emitRenderFrame = (time: number) => { if (skipRenderFrameForConfigUpdate) { - skipRenderFrameForConfigUpdate = false; return; } if (!editorState.playing) { From 97e24902f422aed67c1fe67adcdf8b50ab5fffcd Mon Sep 17 00:00:00 2001 From: Minit Date: Mon, 6 Apr 2026 16:01:30 +0530 Subject: [PATCH 3/4] fix: sort imports in ClipTrack.tsx for Biome format check Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 368bbf95e9..28f1f425d0 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -4,7 +4,6 @@ import { } from "@solid-primitives/event-listener"; import { cx } from "cva"; import { - type ComponentProps, batch, createEffect, createMemo, @@ -16,6 +15,7 @@ import { onMount, Show, Switch, + type ComponentProps, } from "solid-js"; import { produce } from "solid-js/store"; From d0f553c80dacb3a25400da59db3d577920752a54 Mon Sep 17 00:00:00 2001 From: Minit Date: Mon, 6 Apr 2026 16:04:27 +0530 Subject: [PATCH 4/4] fix: correct import sort order in ClipTrack.tsx per Biome rules Biome uses case-sensitive ASCII sort (uppercase before lowercase), so 'type ComponentProps' (C) must precede 'createEffect' (c). Co-Authored-By: Claude Sonnet 4.6 --- apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx index 28f1f425d0..69559a25da 100644 --- a/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx +++ b/apps/desktop/src/routes/editor/Timeline/ClipTrack.tsx @@ -5,6 +5,7 @@ import { import { cx } from "cva"; import { batch, + type ComponentProps, createEffect, createMemo, createRoot, @@ -15,7 +16,6 @@ import { onMount, Show, Switch, - type ComponentProps, } from "solid-js"; import { produce } from "solid-js/store";