From 84aa5ce1898135ca70c5dcfa1342741fe393b9f5 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Fri, 23 Oct 2026 03:18:18 -0700 Subject: [PATCH] fix(images): describe transforms by the Viewer.js an image was built with 1086436 creates Viewer.js only once the image has loaded, so the "Viewer.js could not load" reason moved from the instance to the live window.Viewer. An image shown natively while Viewer.js was missing then offered transforms as soon as Viewer.js reappeared, which full-inspector.spec.mjs:854 ("collection viewer preserves original download and rooted retries") caught on every project. Each rebuild now captures the Viewer.js it builds with. The reason, the native fallback and the later construction all use that capture. --- web/js/workspace/media-viewer.mjs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/web/js/workspace/media-viewer.mjs b/web/js/workspace/media-viewer.mjs index a0a1ccf6..bd1b3233 100644 --- a/web/js/workspace/media-viewer.mjs +++ b/web/js/workspace/media-viewer.mjs @@ -35,7 +35,7 @@ export function createMediaViewer({api,container,parentScope,inspectionControlle if (scope.signal.aborted) return; active?.close(false); const {items,selectedKey,trigger = document.activeElement} = options; - let mounted = false, initial, entry, currentKey, viewer = null, mediaScope = null, closed = false; + let mounted = false, initial, entry, currentKey, viewer = null, withoutViewer = false, mediaScope = null, closed = false; let originalDownload = null, inspectionState = inspectionController.state; const session = createViewerSession({adapter,items,initialKey:selectedKey,onChange:(state,selected) => { if (!mounted) { initial = [state,selected]; return; } @@ -115,7 +115,9 @@ export function createMediaViewer({api,container,parentScope,inspectionControlle mediaScope?.dispose();mediaScope = createScope();viewer = null; for (const child of [...stage.children]) if (child !== failure) child.remove(); failure.hidden = true; - const mediaLocal = mediaScope, key = session.state.key; + const mediaLocal = mediaScope, key = session.state.key, ViewerJs = window.Viewer; + // Transforms follow the Viewer.js this media was built with, which builds once the image has loaded. + withoutViewer = !ViewerJs; const current = () => !local.signal.aborted && !mediaLocal.signal.aborted && session.state.key === key; // An adapter's own media URL (a session-owned blob:) is used exactly, so a retry reuses it; a server // original gets a fresh cache-buster on every build. @@ -142,15 +144,15 @@ export function createMediaViewer({api,container,parentScope,inspectionControlle },{once:true}); } else { node.alt = selected.filename;node.decoding = 'async'; - node.hidden = !!window.Viewer; + node.hidden = !!ViewerJs; // With Viewer.js the src is released after the viewer (see release below). - if (!window.Viewer) { + if (!ViewerJs) { mediaLocal.listen(node,'load',() => {if (current()) session.ready(key);}); mediaLocal.cleanup(() => node.removeAttribute('src')); } } node.src = src;stage.append(node); - if (!selected.is_video && window.Viewer) { + if (!selected.is_video && ViewerJs) { // Viewer.js is built on an image that has loaded. Its own cancel removes its image listeners only // while `image.complete` is false, so an image left with its load event still queued would build a // viewer into the released image after destroy() (#293). Until then no viewer exists to cancel. @@ -174,7 +176,7 @@ export function createMediaViewer({api,container,parentScope,inspectionControlle fitWidth = width;fitHeight = height;instance.resize(); }); }; - const instance = new window.Viewer(node,{inline:true,toolbar:false,navbar:false,title:false,button:false, + const instance = new ViewerJs(node,{inline:true,toolbar:false,navbar:false,title:false,button:false, keyboard:false,fullscreen:false,transition:false,viewed() { viewSettled = true; if (mediaLocal.signal.aborted) { release(); return; } @@ -198,7 +200,7 @@ export function createMediaViewer({api,container,parentScope,inspectionControlle } function updateControls(state) { const unavailable = inspectionState.refreshRequired ? inspectionState.message : state.phase === 'error' ? 'Media could not be loaded. Retry this file.' : entry.is_video ? - 'Image transforms are unavailable for video.' : !window.Viewer ? + 'Image transforms are unavailable for video.' : withoutViewer ? 'Image transforms are unavailable because Viewer.js could not load.' : !state.mediaReady ? 'Image transforms are available when the image is ready.' : ''; reason.textContent = unavailable;reason.hidden = !unavailable; for (const node of transforms) {