- Move the read-only OpenAPI compat surface from /api/v2/assets to /mjr/am/compat/assets so it never squats ComfyUI core's /api namespace; the legacy alias is opt-in via MJR_AM_LEGACY_API_V2_ASSETS=1. - Allow Python 3.14 (requires-python <3.15) and add 3.14 to CI and tox. - Stop wrapping api.queuePrompt: progress tracking now resolves the running prompt from the core Jobs API (getJobDetail), then /queue and /history, driven by websocket events. Remove wrapHostQueuePrompt. - Add opt-in Majoor.Integration.HideNativeAssetsTab setting that hides ComfyUI's native Assets sidebar tab through the official getSidebarTabs/unregisterSidebarTab API and restores it when disabled. - Fix all 913 type errors in tsconfig.test.json and type-check tests in CI; drop a duplicated `stubs` key in asset_card_inner test; make addToastHistory's type argument optional for the object form. - Update audit report, changelog, API reference and env var docs.
645 lines
23 KiB
TypeScript
645 lines
23 KiB
TypeScript
import {
|
|
centerHostCanvasNodeById,
|
|
getRawHostApi,
|
|
getRawHostApp,
|
|
waitForRawHostApi,
|
|
} from "../../app/hostAdapter.js";
|
|
import { findGraphNodeById, getHostRootGraph } from "../../app/graphTraversal.js";
|
|
|
|
const PROGRESS_UPDATE_EVENT = "progress-update";
|
|
const GLOBAL_SERVICE_KEY = "__MJR_MFV_PROGRESS_SERVICE__";
|
|
|
|
function getGlobalHost() {
|
|
if (typeof globalThis !== "undefined") return globalThis;
|
|
if (typeof window !== "undefined") return window;
|
|
return {};
|
|
}
|
|
|
|
function makeCustomEvent(type: any, detail: any) {
|
|
if (typeof CustomEvent === "function") {
|
|
return new CustomEvent(type, { detail });
|
|
}
|
|
const event: any = typeof Event === "function" ? new Event(type) : { type };
|
|
event.detail = detail;
|
|
return event;
|
|
}
|
|
|
|
export class FloatingViewerPromptExecution {
|
|
[key: string]: any;
|
|
constructor(id: any, getApp = () => getRawHostApp()) {
|
|
this.id = String(id || "");
|
|
this.promptApi = null;
|
|
this.executedNodeIds = [];
|
|
this.totalNodes = 0;
|
|
this.currentlyExecuting = null;
|
|
this.errorDetails = null;
|
|
this._getApp = typeof getApp === "function" ? getApp : () => null;
|
|
}
|
|
|
|
setPrompt(prompt: any) {
|
|
const output = prompt && typeof prompt === "object" ? prompt.output : null;
|
|
this.promptApi = output && typeof output === "object" ? output : null;
|
|
this.totalNodes = this.promptApi ? Object.keys(this.promptApi).length : 0;
|
|
}
|
|
|
|
getApiNode(nodeId: any) {
|
|
return this.promptApi?.[String(nodeId)] || null;
|
|
}
|
|
|
|
getNodeLabel(nodeId: any) {
|
|
const apiNode = this.getApiNode(nodeId);
|
|
let label = apiNode?._meta?.title || apiNode?.class_type || "";
|
|
if (!label) {
|
|
const app = this._getApp?.();
|
|
const graphNode = findGraphNodeById(getHostRootGraph(app), nodeId);
|
|
label = graphNode?.title || graphNode?.type || "";
|
|
}
|
|
return String(label || "").trim();
|
|
}
|
|
|
|
markExecuted(nodeId: any) {
|
|
const safeNodeId = String(nodeId || "").trim();
|
|
if (!safeNodeId) return;
|
|
if (!this.executedNodeIds.includes(safeNodeId)) {
|
|
this.executedNodeIds.push(safeNodeId);
|
|
}
|
|
}
|
|
|
|
executing(nodeId: any, step: any, maxSteps: any) {
|
|
if (nodeId == null) {
|
|
this.currentlyExecuting = null;
|
|
return;
|
|
}
|
|
const nextNodeId = String(nodeId || "").trim();
|
|
if (!nextNodeId) return;
|
|
|
|
if (this.currentlyExecuting?.nodeId !== nextNodeId) {
|
|
if (this.currentlyExecuting != null) {
|
|
this.markExecuted(this.currentlyExecuting.nodeId);
|
|
}
|
|
this.currentlyExecuting = {
|
|
nodeId: nextNodeId,
|
|
nodeLabel: this.getNodeLabel(nextNodeId),
|
|
pass: 0,
|
|
};
|
|
|
|
const apiNode = this.getApiNode(nextNodeId);
|
|
if (!this.currentlyExecuting.nodeLabel) {
|
|
this.currentlyExecuting.nodeLabel = this.getNodeLabel(nextNodeId);
|
|
}
|
|
if (apiNode?.class_type === "UltimateSDUpscale") {
|
|
this.currentlyExecuting.pass -= 1;
|
|
this.currentlyExecuting.maxPasses = -1;
|
|
} else if (apiNode?.class_type === "IterativeImageUpscale") {
|
|
this.currentlyExecuting.maxPasses = Number(apiNode?.inputs?.steps) || -1;
|
|
}
|
|
}
|
|
|
|
if (step != null) {
|
|
const numericStep = Number(step);
|
|
const numericMax = Number(maxSteps);
|
|
if (!Number.isFinite(numericStep)) return;
|
|
if (
|
|
!this.currentlyExecuting.step ||
|
|
numericStep < Number(this.currentlyExecuting.step)
|
|
) {
|
|
this.currentlyExecuting.pass += 1;
|
|
}
|
|
this.currentlyExecuting.step = numericStep;
|
|
this.currentlyExecuting.maxSteps = Number.isFinite(numericMax) ? numericMax : null;
|
|
}
|
|
}
|
|
|
|
error(details: any) {
|
|
this.errorDetails = details || null;
|
|
}
|
|
}
|
|
|
|
export class FloatingViewerProgressService extends EventTarget {
|
|
[key: string]: any;
|
|
constructor({
|
|
getApi = (app: any) => getRawHostApi(app),
|
|
getApp = () => getRawHostApp(),
|
|
waitForApi = (options: any) => waitForRawHostApi(options),
|
|
} = {}) {
|
|
super();
|
|
this._getApi = getApi;
|
|
this._getApp = getApp;
|
|
this._waitForApi = waitForApi;
|
|
this.promptsMap = new Map();
|
|
this.currentExecution = null;
|
|
this.lastQueueRemaining = 0;
|
|
this._api = null;
|
|
this._listenerEntries = [];
|
|
this._initPromise = null;
|
|
this._hydrations = new Map();
|
|
}
|
|
|
|
getSnapshot() {
|
|
return {
|
|
queue: this.lastQueueRemaining,
|
|
prompt: this.currentExecution,
|
|
};
|
|
}
|
|
|
|
getCurrentNodeId() {
|
|
const prompt = this.currentExecution;
|
|
return String(
|
|
prompt?.errorDetails?.node_id || prompt?.currentlyExecuting?.nodeId || "",
|
|
).trim();
|
|
}
|
|
|
|
getOrMakePrompt(id: any) {
|
|
const safeId = String(id || "").trim() || "unknown";
|
|
let prompt = this.promptsMap.get(safeId);
|
|
if (!prompt) {
|
|
prompt = new FloatingViewerPromptExecution(safeId, this._getApp);
|
|
this.promptsMap.set(safeId, prompt);
|
|
}
|
|
return prompt;
|
|
}
|
|
|
|
async ensureInitialized({ api = null, app = null, timeoutMs = 0 } = {}) {
|
|
if (api && this._api === api) return api;
|
|
if (!api && this._api) return this._api;
|
|
if (this._initPromise) return this._initPromise;
|
|
|
|
this._initPromise = this._ensureInitializedInternal({ api, app, timeoutMs }).finally(() => {
|
|
this._initPromise = null;
|
|
});
|
|
return this._initPromise;
|
|
}
|
|
|
|
async _ensureInitializedInternal({ api = null, app = null, timeoutMs = 0 } = {}) {
|
|
let resolvedApi = api || this._getApi?.(app) || null;
|
|
if (!resolvedApi && timeoutMs > 0 && typeof this._waitForApi === "function") {
|
|
try {
|
|
resolvedApi = await this._waitForApi({ app, timeoutMs });
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
if (!resolvedApi) return null;
|
|
this._attachApi(resolvedApi);
|
|
return resolvedApi;
|
|
}
|
|
|
|
_attachApi(api: any) {
|
|
if (!api || typeof api.addEventListener !== "function") return;
|
|
if (this._api === api) return;
|
|
this.dispose({ keepState: true });
|
|
this._api = api;
|
|
this._attachApiListeners(api);
|
|
}
|
|
|
|
/**
|
|
* Resolve the API-format prompt (node ids -> class_type/_meta) for a
|
|
* prompt that the server just started, using ComfyUI's own read-only
|
|
* endpoints instead of intercepting `api.queuePrompt`. Wrapping
|
|
* queuePrompt is fragile: other extensions wrap it too and the order is
|
|
* not guaranteed.
|
|
*
|
|
* Order: core Jobs API (`api.getJobDetail` -> `GET /api/jobs/{id}`, whose
|
|
* `workflow` carries `{ prompt, extra_data }`), then `/queue` for older
|
|
* cores, then `/history/{id}` for prompts that already finished.
|
|
*/
|
|
async _hydratePrompt(api: any, promptExecution: any) {
|
|
const promptId = String(promptExecution?.id || "").trim();
|
|
if (!promptId || promptId === "unknown" || promptExecution.promptApi) return;
|
|
if (!api) return;
|
|
if (this._hydrations.has(promptId)) return this._hydrations.get(promptId);
|
|
|
|
const task = (async () => {
|
|
const output =
|
|
(await this._findPromptInJob(api, promptId)) ||
|
|
(await this._findPromptInQueue(api, promptId)) ||
|
|
(await this._findPromptInHistory(api, promptId));
|
|
if (output && !promptExecution.promptApi) {
|
|
promptExecution.setPrompt({ output });
|
|
const current = promptExecution.currentlyExecuting;
|
|
if (current?.nodeId) {
|
|
current.nodeLabel =
|
|
promptExecution.getNodeLabel(current.nodeId) || current.nodeLabel;
|
|
}
|
|
if (this.promptsMap.get(promptId) === promptExecution) {
|
|
this.dispatchProgressUpdate();
|
|
}
|
|
}
|
|
})()
|
|
.catch((e: any) => console.debug?.(e))
|
|
.finally(() => this._hydrations.delete(promptId));
|
|
this._hydrations.set(promptId, task);
|
|
return task;
|
|
}
|
|
|
|
async _findPromptInJob(api: any, promptId: string) {
|
|
if (typeof api.getJobDetail !== "function") return null;
|
|
try {
|
|
const job = await api.getJobDetail(promptId);
|
|
const prompt = job?.workflow?.prompt;
|
|
return prompt && typeof prompt === "object" && !Array.isArray(prompt) ? prompt : null;
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async _fetchJson(api: any, route: string) {
|
|
if (typeof api.fetchApi !== "function") return null;
|
|
const resp = await api.fetchApi(route, { cache: "no-store" });
|
|
if (!resp?.ok || typeof resp.json !== "function") return null;
|
|
return resp.json();
|
|
}
|
|
|
|
async _findPromptInQueue(api: any, promptId: string) {
|
|
const data = await this._fetchJson(api, "/queue");
|
|
const items = [
|
|
...(Array.isArray(data?.queue_running) ? data.queue_running : []),
|
|
...(Array.isArray(data?.queue_pending) ? data.queue_pending : []),
|
|
];
|
|
for (const item of items) {
|
|
// Queue entries are [number, prompt_id, prompt, extra_data, outputs_to_execute, ...]
|
|
if (Array.isArray(item) && String(item[1] || "") === promptId) {
|
|
return item[2] && typeof item[2] === "object" ? item[2] : null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async _findPromptInHistory(api: any, promptId: string) {
|
|
const data = await this._fetchJson(api, `/history/${encodeURIComponent(promptId)}`);
|
|
const prompt = data?.[promptId]?.prompt;
|
|
return Array.isArray(prompt) && prompt[2] && typeof prompt[2] === "object" ? prompt[2] : null;
|
|
}
|
|
|
|
_attachApiListeners(api: any) {
|
|
this._attachListener(api, "status", (event: any) => {
|
|
if (!event?.detail?.exec_info) return;
|
|
this.lastQueueRemaining = Number(event.detail.exec_info.queue_remaining) || 0;
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
this._attachListener(api, "execution_start", (event: any) => {
|
|
const promptId = String(
|
|
event?.detail?.prompt_id || event?.detail?.promptId || "",
|
|
).trim();
|
|
if (!promptId) return;
|
|
this.currentExecution = this.getOrMakePrompt(promptId);
|
|
this.dispatchProgressUpdate();
|
|
void this._hydratePrompt(api, this.currentExecution);
|
|
});
|
|
|
|
this._attachListener(api, "executing", (event: any) => {
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt("unknown");
|
|
}
|
|
this.currentExecution.executing(event?.detail);
|
|
if (event?.detail == null) {
|
|
this.currentExecution = null;
|
|
}
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
this._attachListener(api, "progress", (event: any) => {
|
|
const detail = event?.detail || {};
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(detail.prompt_id || detail.promptId);
|
|
}
|
|
this.currentExecution.executing(detail.node, detail.value, detail.max);
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
this._attachListener(api, "execution_cached", (event: any) => {
|
|
const detail = event?.detail || {};
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(detail.prompt_id || detail.promptId);
|
|
}
|
|
for (const cached of Array.isArray(detail.nodes) ? detail.nodes : []) {
|
|
this.currentExecution.markExecuted(cached);
|
|
}
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
this._attachListener(api, "executed", (event: any) => {
|
|
const detail = event?.detail || {};
|
|
if (!this.currentExecution) {
|
|
const promptId = String(detail.prompt_id || detail.promptId || "").trim();
|
|
if (promptId) {
|
|
this.currentExecution = this.getOrMakePrompt(promptId);
|
|
}
|
|
}
|
|
});
|
|
|
|
this._attachListener(api, "execution_error", (event: any) => {
|
|
const detail = event?.detail || {};
|
|
if (!this.currentExecution) {
|
|
this.currentExecution = this.getOrMakePrompt(detail.prompt_id || detail.promptId);
|
|
}
|
|
this.currentExecution?.error(detail);
|
|
this.dispatchProgressUpdate();
|
|
});
|
|
|
|
const clearCurrentExecution = () => {
|
|
if (this.currentExecution) {
|
|
this.currentExecution.executing(null);
|
|
}
|
|
this.currentExecution = null;
|
|
this.dispatchProgressUpdate();
|
|
};
|
|
this._attachListener(api, "execution_success", clearCurrentExecution);
|
|
this._attachListener(api, "execution_interrupted", clearCurrentExecution);
|
|
}
|
|
|
|
_attachListener(api: any, type: any, handler: any) {
|
|
api.addEventListener(type, handler);
|
|
this._listenerEntries.push({ api, type, handler });
|
|
}
|
|
|
|
dispatchProgressUpdate() {
|
|
this.dispatchEvent(makeCustomEvent(PROGRESS_UPDATE_EVENT, this.getSnapshot()));
|
|
}
|
|
|
|
dispose({ keepState = false }: { keepState?: boolean } = {}) {
|
|
for (const { api, type, handler } of this._listenerEntries.splice(0)) {
|
|
try {
|
|
api?.removeEventListener?.(type, handler);
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
this._api = null;
|
|
if (!keepState) {
|
|
this.promptsMap.clear();
|
|
this.currentExecution = null;
|
|
this.lastQueueRemaining = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
const globalHost = getGlobalHost() as Record<string, any>;
|
|
|
|
export const floatingViewerProgressService =
|
|
globalHost[GLOBAL_SERVICE_KEY] || new FloatingViewerProgressService();
|
|
|
|
if (!globalHost[GLOBAL_SERVICE_KEY]) {
|
|
globalHost[GLOBAL_SERVICE_KEY] = floatingViewerProgressService;
|
|
}
|
|
|
|
export function ensureFloatingViewerProgressTracking(options: Record<string, any> = {}): Promise<any> {
|
|
return floatingViewerProgressService.ensureInitialized(options);
|
|
}
|
|
|
|
function formatProgressText(detail: any) {
|
|
const queue = Math.max(0, Number(detail?.queue) || 0);
|
|
const prompt = detail?.prompt || null;
|
|
|
|
if (prompt?.errorDetails) {
|
|
const exceptionType = String(
|
|
prompt.errorDetails?.exception_type || "Execution error",
|
|
).trim();
|
|
const nodeId = String(prompt.errorDetails?.node_id || "").trim();
|
|
const nodeType = String(prompt.errorDetails?.node_type || "").trim();
|
|
return [exceptionType, nodeId, nodeType].filter(Boolean).join(" ");
|
|
}
|
|
|
|
if (prompt?.currentlyExecuting) {
|
|
const current = prompt.currentlyExecuting;
|
|
let progressText = `(${queue}) `;
|
|
if (!prompt.totalNodes) {
|
|
progressText += "??%";
|
|
} else {
|
|
const percent = (prompt.executedNodeIds.length / prompt.totalNodes) * 100;
|
|
progressText += `${Math.round(percent)}%`;
|
|
}
|
|
|
|
let stepsLabel = "";
|
|
if (current.step != null && current.maxSteps) {
|
|
const stepPercent = (Number(current.step) / Number(current.maxSteps)) * 100;
|
|
if (current.pass > 1 || current.maxPasses != null) {
|
|
stepsLabel += `#${current.pass}`;
|
|
if (current.maxPasses && current.maxPasses > 0) {
|
|
stepsLabel += `/${current.maxPasses}`;
|
|
}
|
|
stepsLabel += " - ";
|
|
}
|
|
stepsLabel += `${Math.round(stepPercent)}%`;
|
|
}
|
|
|
|
const nodeLabel = String(current.nodeLabel || "").trim();
|
|
if (nodeLabel || stepsLabel) {
|
|
progressText += ` - ${nodeLabel || "Unknown node"}${stepsLabel ? ` (${stepsLabel})` : ""}`;
|
|
}
|
|
return progressText;
|
|
}
|
|
|
|
if (queue > 0) {
|
|
return `(${queue}) Running... in another tab`;
|
|
}
|
|
return "Idle";
|
|
}
|
|
|
|
export function formatFloatingViewerMediaProgressText(detail: any): string {
|
|
const prompt = detail?.prompt || null;
|
|
|
|
if (prompt?.errorDetails) {
|
|
const errorDetails = prompt?.errorDetails || {};
|
|
const nodeLabel = String(
|
|
prompt?.currentlyExecuting?.nodeLabel ||
|
|
errorDetails?.node_type ||
|
|
errorDetails?.node_id ||
|
|
"Execution",
|
|
).trim();
|
|
const rawMessage =
|
|
errorDetails?.exception_message ??
|
|
errorDetails?.error ??
|
|
errorDetails?.message ??
|
|
errorDetails?.detail ??
|
|
errorDetails?.reason ??
|
|
"";
|
|
const normalizedMessage = Array.isArray(rawMessage)
|
|
? rawMessage
|
|
.map((item: any) => String(item || "").trim())
|
|
.filter(Boolean)
|
|
.join(" | ")
|
|
: String(rawMessage || "").trim();
|
|
const compactMessage = normalizedMessage.replace(/\s+/g, " ").trim();
|
|
if (compactMessage) {
|
|
return `${nodeLabel} - ${compactMessage}`;
|
|
}
|
|
return `${nodeLabel} - Error`;
|
|
}
|
|
|
|
const current = prompt?.currentlyExecuting || null;
|
|
if (!current) return "";
|
|
|
|
const nodeLabel = String(current.nodeLabel || current.nodeId || "Node").trim();
|
|
const step = Number(current.step);
|
|
const maxSteps = Number(current.maxSteps);
|
|
if (Number.isFinite(step) && Number.isFinite(maxSteps) && maxSteps > 0) {
|
|
if (current.pass > 1) {
|
|
return `${nodeLabel} #${current.pass} - ${step}/${maxSteps}`;
|
|
}
|
|
return `${nodeLabel} - ${step}/${maxSteps}`;
|
|
}
|
|
return nodeLabel;
|
|
}
|
|
|
|
function applyFloatingViewerProgressState(viewer: any, detail: any) {
|
|
if (!viewer?._progressEl && !viewer?._mediaProgressEl) return;
|
|
|
|
const prompt = detail?.prompt || null;
|
|
const currentNodeId = String(
|
|
prompt?.errorDetails?.node_id || prompt?.currentlyExecuting?.nodeId || "",
|
|
).trim();
|
|
|
|
let nodesWidth = "0%";
|
|
let stepsWidth = "0%";
|
|
const isError = !!prompt?.errorDetails;
|
|
|
|
if (prompt?.currentlyExecuting) {
|
|
if (prompt.totalNodes > 0) {
|
|
const percent = (prompt.executedNodeIds.length / prompt.totalNodes) * 100;
|
|
nodesWidth = `${Math.max(2, Math.round(percent * 100) / 100)}%`;
|
|
}
|
|
|
|
const step = Number(prompt.currentlyExecuting?.step);
|
|
const maxSteps = Number(prompt.currentlyExecuting?.maxSteps);
|
|
if (Number.isFinite(step) && Number.isFinite(maxSteps) && maxSteps > 0) {
|
|
stepsWidth = `${Math.max(0, Math.min(100, (step / maxSteps) * 100))}%`;
|
|
}
|
|
} else if (isError) {
|
|
nodesWidth = "100%";
|
|
stepsWidth = "100%";
|
|
}
|
|
|
|
viewer._progressCurrentNodeId = currentNodeId || null;
|
|
if (viewer._progressEl) {
|
|
viewer._progressNodesEl.style.width = nodesWidth;
|
|
viewer._progressStepsEl.style.width = stepsWidth;
|
|
viewer._progressTextEl.textContent = formatProgressText(detail);
|
|
viewer._progressEl.classList.toggle("is-error", isError);
|
|
viewer._progressEl.classList.toggle("is-clickable", Boolean(currentNodeId));
|
|
viewer._progressEl.title = currentNodeId
|
|
? "Execution progress - click to center active node"
|
|
: "Execution progress";
|
|
}
|
|
|
|
if (viewer._mediaProgressEl) {
|
|
const mediaLabel = formatFloatingViewerMediaProgressText(detail);
|
|
viewer._mediaProgressTextEl.textContent = mediaLabel;
|
|
viewer._mediaProgressEl.title = mediaLabel || "";
|
|
viewer._mediaProgressEl.classList.toggle("is-error", isError);
|
|
viewer._mediaProgressEl.classList.toggle("is-visible", Boolean(mediaLabel));
|
|
}
|
|
}
|
|
|
|
function centerFloatingViewerProgressNode(viewer: any, nodeId: any) {
|
|
const safeNodeId = String(nodeId || "").trim();
|
|
if (!safeNodeId) return false;
|
|
try {
|
|
return centerHostCanvasNodeById(safeNodeId, getRawHostApp());
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function buildFloatingViewerProgressBar(viewer: any): HTMLElement {
|
|
const root = document.createElement("div");
|
|
root.className = "mjr-mfv-progress";
|
|
root.setAttribute("role", "status");
|
|
root.setAttribute("aria-live", "polite");
|
|
|
|
const nodesBar = document.createElement("div");
|
|
nodesBar.className = "mjr-mfv-progress-bar mjr-mfv-progress-bar--nodes";
|
|
|
|
const stepsBar = document.createElement("div");
|
|
stepsBar.className = "mjr-mfv-progress-bar mjr-mfv-progress-bar--steps";
|
|
|
|
const overlay = document.createElement("div");
|
|
overlay.className = "mjr-mfv-progress-overlay";
|
|
overlay.setAttribute("aria-hidden", "true");
|
|
|
|
const text = document.createElement("span");
|
|
text.className = "mjr-mfv-progress-text";
|
|
text.textContent = "Idle";
|
|
|
|
root.appendChild(nodesBar);
|
|
root.appendChild(stepsBar);
|
|
root.appendChild(overlay);
|
|
root.appendChild(text);
|
|
|
|
root.addEventListener("pointerdown", (event: any) => {
|
|
if (event.button !== 0) return;
|
|
if (!centerFloatingViewerProgressNode(viewer, viewer._progressCurrentNodeId)) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
});
|
|
|
|
viewer._progressEl = root;
|
|
viewer._progressNodesEl = nodesBar;
|
|
viewer._progressStepsEl = stepsBar;
|
|
viewer._progressTextEl = text;
|
|
|
|
if (viewer._progressUpdateHandler) {
|
|
floatingViewerProgressService.removeEventListener(
|
|
PROGRESS_UPDATE_EVENT,
|
|
viewer._progressUpdateHandler,
|
|
);
|
|
}
|
|
viewer._progressUpdateHandler = (event: any) => {
|
|
applyFloatingViewerProgressState(
|
|
viewer,
|
|
event?.detail || floatingViewerProgressService.getSnapshot(),
|
|
);
|
|
};
|
|
|
|
floatingViewerProgressService.addEventListener(
|
|
PROGRESS_UPDATE_EVENT,
|
|
viewer._progressUpdateHandler,
|
|
);
|
|
void ensureFloatingViewerProgressTracking({ timeoutMs: 4000 }).catch((e: any) => {
|
|
console.debug?.(e);
|
|
});
|
|
applyFloatingViewerProgressState(viewer, floatingViewerProgressService.getSnapshot());
|
|
|
|
return root;
|
|
}
|
|
|
|
export function buildFloatingViewerMediaProgressOverlay(viewer: any) {
|
|
const root = document.createElement("div");
|
|
root.className = "mjr-mfv-media-progress";
|
|
root.setAttribute("aria-hidden", "true");
|
|
|
|
const text = document.createElement("span");
|
|
text.className = "mjr-mfv-media-progress-text";
|
|
root.appendChild(text);
|
|
|
|
viewer._mediaProgressEl = root;
|
|
viewer._mediaProgressTextEl = text;
|
|
applyFloatingViewerProgressState(viewer, floatingViewerProgressService.getSnapshot());
|
|
return root;
|
|
}
|
|
|
|
export function disposeFloatingViewerProgressBar(viewer: any) {
|
|
if (viewer?._progressUpdateHandler) {
|
|
try {
|
|
floatingViewerProgressService.removeEventListener(
|
|
PROGRESS_UPDATE_EVENT,
|
|
viewer._progressUpdateHandler,
|
|
);
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
viewer._progressUpdateHandler = null;
|
|
viewer._progressCurrentNodeId = null;
|
|
viewer._progressEl = null;
|
|
viewer._progressNodesEl = null;
|
|
viewer._progressStepsEl = null;
|
|
viewer._progressTextEl = null;
|
|
viewer._mediaProgressEl = null;
|
|
viewer._mediaProgressTextEl = null;
|
|
}
|