fix: upload progress was rebuilding the whole media panel per tick

The previous fix (67b1324) put mediaUpload straight into the mediabrowser
zone's fingerprint, so every XHR progress tick did a full rebuild of the
media grid, thumbnails, and project list -- and closed any open <select>
in there (e.g. "Sort by"), the same class of bug just fixed in Engine
settings. Only the file boundary (which file, 1-of-how-many) is in the
fingerprint now; byte-level progress goes through a quiet
updateMediaUploadProgress + a funpack-media-upload-progress event that
mediabrowser.js uses to patch the bar/percent in place, mirroring the
existing updateGenProgress/funpack-gen-progress pattern for the player.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-22 18:48:47 +03:00
co-authored by Claude Sonnet 5
parent 1205c20255
commit a456a856e6
3 changed files with 42 additions and 13 deletions
+19 -1
View File
@@ -17,6 +17,19 @@
for (const id of thumbCache.keys()) if (!ids.has(id)) thumbCache.delete(id);
}
// Drop-zone upload bar, patched in place on funpack-media-upload-progress (below) instead
// of rebuilding — a full render only happens once per file, at the fpMediabrowser fingerprint
// in view_bus.js's granularity (see mediaTab). Cleared whenever the zone isn't currently the
// "uploading" one, so a stale reference from a since-discarded render never gets written to.
let _uploadFillEl = null, _uploadPctEl = null;
window.addEventListener("funpack-media-upload-progress", (e) => {
if (!_uploadFillEl || !_uploadFillEl.isConnected) return;
const up = (e && e.detail) || {};
const pct = up.size > 0 ? Math.min(100, Math.round((up.loaded / up.size) * 100)) : null;
_uploadFillEl.style.width = (pct == null ? 0 : pct) + "%";
if (_uploadPctEl) _uploadPctEl.textContent = pct == null ? "uploading…" : `${pct}%`;
});
const mediaSelected = new Set(); // media bin multi-select
let mediaSelectMode = false; // when off, click = preview; when on, click = toggle selection
const MF_KEY = "fp_media_filter";
@@ -473,8 +486,13 @@
bar.style.width = (pct == null ? 0 : pct) + "%";
barWrap.append(bar);
drop.append(barWrap);
drop.append(el("div", "pj-meta", pct == null ? "uploading…" : `${pct}%`));
const pctEl = el("div", "pj-meta", pct == null ? "uploading…" : `${pct}%`);
drop.append(pctEl);
// Live-patched by the funpack-media-upload-progress listener above for every tick
// within this file — this render only happens once per file (see fpMediabrowser).
_uploadFillEl = bar; _uploadPctEl = pctEl;
} else {
_uploadFillEl = null; _uploadPctEl = null;
drop.append(el("div", "big", "🎞"));
drop.append(el("div", null, "Drop images, video & audio here"));
drop.append(el("div", "pj-meta", "or click to browse · drag onto a clip to set its anchor"));
+14 -8
View File
@@ -211,6 +211,16 @@
Object.assign(state.gen, patch);
try { window.dispatchEvent(new CustomEvent("funpack-gen-progress", { detail: state.gen })); } catch (_) {}
}
// Same idea as updateGenProgress: XHR upload progress fires many times a second, and a
// full notify() would rebuild the whole media bin (thumbnails, project list, any open
// <select>) on every tick. Mutate quietly and let mediabrowser.js patch its own bar/percent
// in place instead.
function updateMediaUploadProgress(patch) {
if (!state.mediaUpload) return;
Object.assign(state.mediaUpload, patch);
try { window.dispatchEvent(new CustomEvent("funpack-media-upload-progress", { detail: state.mediaUpload })); } catch (_) {}
}
function get() { return state; }
function _promptPreviewKey(project) {
@@ -4427,19 +4437,15 @@
// uploading over a slow connection just looked like nothing was happening.
async function uploadMedia(files) {
const list = [...files];
let lastNotify = 0;
for (let i = 0; i < list.length; i++) {
const f = list[i];
// A full notify() here (once per file) is what builds the "uploading" drop zone in the
// first place -- see fpMediabrowser in view_bus.js. Progress WITHIN a file goes through
// updateMediaUploadProgress instead, which never touches the store's subscribers.
set({ mediaUpload: { current: i + 1, total: list.length, name: f.name, loaded: 0, size: f.size } });
lastNotify = Date.now();
try {
await API.uploadMedia(f, (loaded, total) => {
state.mediaUpload = { ...state.mediaUpload, loaded, size: total || f.size };
// XHR fires this many times a second -- notify() rebuilds the whole media grid,
// so this throttles to a still-smooth ~8fps instead of hammering a full re-render
// on every tick. The final `loaded === total` (upload done) always gets through.
const now = Date.now();
if (now - lastNotify >= 120 || loaded >= total) { lastNotify = now; notify(); }
updateMediaUploadProgress({ loaded, size: total || f.size });
});
} catch (e) { console.error("upload failed", e); }
}
+9 -4
View File
@@ -176,10 +176,15 @@
projects: st.projects?.length,
media: st.mediaBin?.length,
mediaPreview: st.mediaPreviewId,
// The upload progress bar/percent live in this zone and change on every XHR
// progress tick without touching mediaBin's length -- without this the drop
// zone never repaints and looks stale for the whole upload.
upload: st.mediaUpload,
// Only the file boundary (which file, 1-of-how-many) belongs in the fingerprint --
// that's rare enough to justify a full rebuild, which is what creates the "uploading"
// drop zone in the first place. `loaded` ticks many times a second and is deliberately
// left out: a full rebuild on every tick would tear down and rebuild the whole bin
// (thumbs, project list, any open <select> like "Sort by"). See the
// funpack-media-upload-progress listener in mediabrowser.js, which patches the drop
// zone's bar/percent in place instead, the same way funpack-gen-progress does for the
// player's readout.
upload: st.mediaUpload ? `${st.mediaUpload.current}/${st.mediaUpload.total}:${st.mediaUpload.name}` : null,
// Continuity pin renders on gallery cards (📌 button state + thumb badge).
pin: st.project?.continuity_settings?.identity_pin_ref,
// Reference marks render the same way (R button + numbered badge), and their ORDER