From 15a4fbf7d71c68c3055c3d04a0693e96cd0cb07f Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:22:02 +0300 Subject: [PATCH] Movie Editor: preserve scroll, stop per-second autosave (save on blur instead) - Inspector preserves scrollTop across rebuilds, so editing no longer jumps the panel to the top. - Autosave only persists to disk; it doesn't need to fire every keystroke. Free-text/ number fields now debounce 6s and are FLUSHED on blur (flushSave); discrete actions save ~1.2s after the last change. generate()/generateMontage() flush first so the server has the latest edits before reading the project. Co-Authored-By: Claude Opus 4.8 --- movie_editor/frontend/inspector.js | 11 +++++++++-- movie_editor/frontend/store.js | 24 ++++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/movie_editor/frontend/inspector.js b/movie_editor/frontend/inspector.js index ea7ef265..bd33f2e0 100644 --- a/movie_editor/frontend/inspector.js +++ b/movie_editor/frontend/inspector.js @@ -462,6 +462,7 @@ function render(st) { if (_editing) return; // editing in progress — leave the DOM (and the user's caret) alone + const scrollTop = body.scrollTop; // preserve scroll across the rebuild clear(body); if (!st.project) { title.textContent = "Inspector"; body.append(el("div", "pj-meta", "No project open.")); return; } const scene = st.selectedSceneId ? S.scene(st.selectedSceneId) : null; @@ -470,6 +471,7 @@ if (scene) renderScene(st, scene); else renderProject(st); renderExposed(st); renderSplit(st); + body.scrollTop = scrollTop; // restore so editing doesn't jump to the top } // Track edit state so autosave re-renders never interrupt the focused field. @@ -481,8 +483,13 @@ const t = e.target; if (!(t && t.dataset && t.dataset.k)) return; _editing = false; - // If focus didn't move to another of our fields, re-sync the inspector to state. - setTimeout(() => { if (!_editing) render(S.get()); }, 60); + // Persist the edit now (no more per-second autosave), then re-sync if focus didn't + // move to another field. flushSave commits + notifies (which renders); fall back to a + // manual render when there was nothing pending. + setTimeout(() => { + if (_editing) return; + if (!S.flushSave()) render(S.get()); + }, 60); }); S.subscribe(render); diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index e0f0710c..2f6b436d 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -106,15 +106,29 @@ } // ── editing ──────────────────────────────────────────────────────────────── + // Autosave just persists the project to disk so edits aren't lost — it does NOT need + // to fire every keystroke. Discrete actions (dropdowns, checkboxes) save ~1s after the + // last change; free typing waits longer and is flushed on blur (see flushSave). function scheduleSave() { state.saving = true; notify(); clearTimeout(saveTimer); - saveTimer = setTimeout(commit, 900); + saveTimer = setTimeout(() => { saveTimer = null; commit(); }, 1200); } // For free-text / number fields: update state and queue a save WITHOUT re-rendering, - // so typing isn't interrupted. The eventual commit() re-renders once, after a pause. - function scheduleSaveSilent() { clearTimeout(saveTimer); saveTimer = setTimeout(commit, 1100); } + // so typing isn't interrupted. Long debounce; blur flushes it (flushSave). + function scheduleSaveSilent() { + clearTimeout(saveTimer); + saveTimer = setTimeout(() => { saveTimer = null; commit(); }, 6000); + } + + // Commit any pending save immediately (field blur / before generate). Returns the + // commit promise when it flushed, else null. + function flushSave() { + if (!saveTimer) return null; + clearTimeout(saveTimer); saveTimer = null; + return commit(); + } async function commit() { if (!state.project) return; @@ -395,6 +409,7 @@ async function generate(onlyScene) { if (!state.project) return; + await flushSave(); // ensure the server has the latest edits before generating if (!onlyScene) return generateMontage(); await _generateRun([onlyScene], onlyScene, "Generating scene"); } @@ -403,6 +418,7 @@ // (one GPU at a time). Each run's first scene supplies its i2v anchor. async function generateMontage() { if (!state.project) return; + await flushSave(); // persist pending edits before reading scenes/runs const runs = _runs(); if (!runs.length) { set({ gen: { state: "error", promptId: null, media: [], msg: "No active scenes to generate." } }); return; } for (let i = 0; i < runs.length; i++) { @@ -568,7 +584,7 @@ window.Store = { get, set, subscribe, init, refreshProjectList, loadProject, newProject, deleteProject, downloadProject, importProject, - patchProject, patchProjectQuiet, patchScene, patchSceneQuiet, selectScene, addScene, removeScene, moveScene, scene, + patchProject, patchProjectQuiet, patchScene, patchSceneQuiet, flushSave, selectScene, addScene, removeScene, moveScene, scene, resizeScene, splitScene, snapFrames, refreshPreview, syncFromPreview, applyGlobalPrompt, generate, generateMontage, renderFinal, exportSelected, loadModels, loadImageTargets, setModelInput, setModelLink, setConditioningSlot, setSamplerSlot, setSamplerInput, setSamplerInputNow, setStudioInput, setStudioInputNow,