Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/viewer-state.mjs
T
Vito d9537a726c feat(workspace): complete shared inspector and collection viewer (#224)
* feat(inspector): expose bounded physical file evidence

* fix(inspector): distinguish ambiguous saved associations

* feat(inspector): derive conservative generation facts

* fix(inspector): guard fact ancestry and inspection state

* feat(inspector): share complete file details and exports

* fix(inspector): preserve fact states and touch targets

* feat(viewer): navigate the loaded media collection

Refs #216

* fix(viewer): refit after current stage geometry changes

Ref #216. Observe the current media stage after readiness and wrapped title changes without rebuilding focused controls or resetting transforms at unchanged dimensions.

* feat(viewer): add lifecycle-safe slideshow and media preferences

* fix(viewer): resume gallery continuation after close (#216)

* fix(images): guard source recovery and wrap viewer actions

Clear stale root and folder intent, cancel old pages, and wait for current source discovery before loading a refreshed collection. Preserve safe retry through failed, superseded, and disposed discovery.

Wrap narrow viewer footer actions and wait for measured gallery viewport settling in responsive checks. Refs #216; addresses PR #224 review.

* test(viewer): await gallery layout after viewport changes

Require actual inner, root client, and document widths to agree after each responsive-case viewport and CSS zoom transition. Keep the existing exact frame, overflow, focus, and coarse target assertions unchanged.

Observed PR #224 CI initial mobile resize at537px before pending card relayout settled to356px. Refs #216.
2026-10-18 09:41:53 -07:00

74 lines
3.4 KiB
JavaScript

import {imageKey} from './images-state.mjs';
import {inspectPath} from './inspection-state.mjs';
/** A viewer visits only the descriptors loaded by its opening query. */
export function createViewerSession({items, initialKey, onChange = () => {}, setTimer = setTimeout, clearTimer = clearTimeout}) {
if (!Array.isArray(items) || !items.length) throw new TypeError('A loaded media collection is required');
const keys = new Set();
const loaded = Object.freeze(items.map(item => {
inspectPath(item);
const key = imageKey(item);
if (keys.has(key)) throw new TypeError('Duplicate viewer identity');
keys.add(key);
return Object.freeze({...item});
}));
const index = loaded.findIndex(item => imageKey(item) === initialKey);
if (index < 0) throw new TypeError('Selected media is not loaded');
let state = Object.freeze({key:initialKey,index,total:loaded.length,phase:'stopped',requested:false,mediaReady:false});
let timer = null, deadline = 0;
function cancel() {
deadline++;
if (timer !== null) clearTimer(timer);
timer = null;
}
function publish(update) {
state = Object.freeze({...state,...update});
onChange(state, loaded[state.index]);
}
function phase(ready = state.mediaReady) {
return !state.requested ? 'stopped' : loaded[state.index].is_video ? 'paused-video' : ready ? 'playing-image' : 'waiting-image';
}
function schedule() {
if (state.phase !== 'playing-image' || timer !== null) return;
const key = state.key, token = ++deadline;
timer = setTimer(() => {
if (token !== deadline || state.phase !== 'playing-image' || !state.requested || state.key !== key) return;
timer = null;navigate(1);
},3500);
}
function navigate(direction) {
if (state.phase === 'disposed') return;
if (direction !== -1 && direction !== 1) throw new TypeError('Viewer direction must be -1 or 1');
if (loaded.length === 1) return;
cancel();
const next = (state.index + direction + loaded.length) % loaded.length;
publish({index:next,key:imageKey(loaded[next]),phase:state.requested ? loaded[next].is_video ? 'paused-video' : 'waiting-image' : 'stopped',mediaReady:false});
}
publish({});
return {
get state() { return state; },
navigate,
play() {
if (state.phase === 'disposed' || state.phase === 'error' || state.requested || loaded.length < 2 || loaded[state.index].is_video) return;
publish({requested:true,phase:state.mediaReady ? 'playing-image' : 'waiting-image'});
schedule();
},
stop() {
if (state.phase === 'disposed') return;
cancel();publish({requested:false,phase:state.phase === 'error' ? 'error' : 'stopped'});
},
ready(key) {
if (state.phase === 'disposed' || key !== state.key || state.mediaReady) return;
publish({mediaReady:true,phase:phase(true)});schedule();
},
failed(key) {
if (state.phase === 'disposed' || key !== state.key) return;
cancel();publish({mediaReady:false,phase:'error',requested:false});
},
dispose() {
if (state.phase === 'disposed') return;
cancel();publish({phase:'disposed',requested:false,mediaReady:false});
},
};
}