diff --git a/movie_editor/frontend/player.js b/movie_editor/frontend/player.js index dd4f6f06..073f973d 100644 --- a/movie_editor/frontend/player.js +++ b/movie_editor/frontend/player.js @@ -257,6 +257,20 @@ let _fpsCur = 25; let _tcEl = null; // timecode span (updated live, not rebuilt) let _playBtnEl = null; // play/pause button (glyph updated live, not rebuilt) + let _genMsgEl = null, _genBarEl = null, _genFillEl = null; // gen readout, updated live + + // Update the generation readout in place on progress ticks (no store re-render, so the + // editor stays interactive — frames can be saved, effects added — while a job runs). + window.addEventListener("funpack-gen-progress", (e) => { + const g = (e && e.detail) || {}; + if (_genMsgEl && g.msg != null) _genMsgEl.textContent = g.msg || g.state || ""; + if (_genFillEl && _genBarEl) { + if (g.maxStep > 0) { + _genBarEl.style.visibility = ""; + _genFillEl.style.width = Math.min(100, Math.round((g.step / g.maxStep) * 100)) + "%"; + } + } + }); // Lightweight per-tick update: never rebuilds the buttons (rebuilding mid-click ate the // mousedown/mouseup, so play/pause/stop stopped responding during playback). @@ -404,22 +418,21 @@ empty.append(el("div", "pj-meta", "Generate from the menu or timeline")); canvas.append(empty); } - // generation progress overlay (visible even when there's already media) + // generation progress overlay (visible even when there's already media). Built once per + // store render; live progress ticks update it in place (see the gen-progress listener), + // so the rest of the editor isn't rebuilt while a generation runs. + _genMsgEl = _genBarEl = _genFillEl = null; if (["queuing", "running", "pending", "error"].includes(gen.state)) { const busy = gen.state !== "error"; const ro = el("div", "gen-readout" + (gen.state === "error" ? " error" : "")); ro.append(el("span", "pulse")); - ro.append(el("span", null, gen.msg || gen.state)); - // step progress bar - if (busy && gen.maxStep > 0) { - const bar = el("div", "gen-bar"); - const fill = el("div", "gen-bar-fill"); - fill.style.width = Math.min(100, Math.round((gen.step / gen.maxStep) * 100)) + "%"; - bar.append(fill); - ro.append(bar); - } - // interrupt + _genMsgEl = el("span", null, gen.msg || gen.state); ro.append(_genMsgEl); if (busy) { + const bar = el("div", "gen-bar"); _genBarEl = bar; + _genFillEl = el("div", "gen-bar-fill"); + _genFillEl.style.width = gen.maxStep > 0 ? Math.min(100, Math.round((gen.step / gen.maxStep) * 100)) + "%" : "0%"; + if (!(gen.maxStep > 0)) bar.style.visibility = "hidden"; + bar.append(_genFillEl); ro.append(bar); const stop = el("button", "gen-interrupt", "■ Interrupt"); stop.title = "Stop the current generation"; stop.onclick = () => S.interrupt(); diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js index 3905cf4f..1a0487a5 100644 --- a/movie_editor/frontend/store.js +++ b/movie_editor/frontend/store.js @@ -28,6 +28,15 @@ function notify() { listeners.forEach((fn) => { try { fn(state); } catch (e) { console.error(e); } }); } function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); } function set(patch) { Object.assign(state, patch); notify(); } + + // Update generation progress WITHOUT a full notify(): a store notify rebuilds the whole + // editor (inspector/timeline/player), which during a long generation kept interrupting the + // user (couldn't save frames to the bin or add effects). Progress ticks instead mutate + // state.gen quietly and fire a DOM event the player listens to, updating only the readout. + function updateGenProgress(patch) { + Object.assign(state.gen, patch); + try { window.dispatchEvent(new CustomEvent("funpack-gen-progress", { detail: state.gen })); } catch (_) {} + } function get() { return state; } // ── project lifecycle ────────────────────────────────────────────────────── @@ -388,7 +397,7 @@ // "Interrupted" and the run loop stops. async function interrupt() { _interrupted = true; - set({ gen: { ...state.gen, msg: "Interrupting…" } }); + updateGenProgress({ msg: "Interrupting…" }); try { await API.interrupt(); } catch (_) {} } @@ -445,7 +454,7 @@ if (_interrupted) return; try { const pr = await API.progress(); - if (pr && pr.max > 0) set({ gen: { ...state.gen, step: pr.value, maxStep: pr.max } }); + if (pr && pr.max > 0) updateGenProgress({ step: pr.value, maxStep: pr.max }); } catch (_) {} }, 700); pollTimer = setInterval(async () => { @@ -478,7 +487,7 @@ return; } const step = (state.gen.maxStep > 0) ? ` · step ${state.gen.step}/${state.gen.maxStep}` : ""; - set({ gen: { ...state.gen, state: s.state, msg: `${prefix} ${_elapsed()}${step}` } }); + updateGenProgress({ state: s.state, msg: `${prefix} ${_elapsed()}${step}` }); } } catch (e) { _clearGenTimers();