270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { t } from "../../app/i18n.js";
|
|
import { appendTooltipHint } from "../../utils/tooltipShortcuts.js";
|
|
import {
|
|
MFV_LIVE_HINT,
|
|
MFV_MODE_HINT,
|
|
MFV_MODES,
|
|
MFV_NODESTREAM_HINT,
|
|
MFV_PREVIEW_HINT,
|
|
} from "./floatingViewerConstants.js";
|
|
import { NODE_STREAM_FEATURE_ENABLED } from "./nodeStream/nodeStreamFeatureFlag.js";
|
|
|
|
export function notifyFloatingViewerModeChanged(viewer: any): void {
|
|
try {
|
|
viewer._controller?.onModeChanged?.(viewer._mode);
|
|
} catch (e: any) {
|
|
console.debug?.(e);
|
|
}
|
|
}
|
|
|
|
export function cycleFloatingViewerMode(viewer: any): void {
|
|
const order = [MFV_MODES.SIMPLE, MFV_MODES.AB, MFV_MODES.SIDE, MFV_MODES.GRID, MFV_MODES.GRAPH];
|
|
viewer._mode = order[(order.indexOf(viewer._mode) + 1) % order.length];
|
|
viewer._updateModeBtnUI();
|
|
viewer._refresh();
|
|
viewer._notifyModeChanged();
|
|
}
|
|
|
|
export function setFloatingViewerMode(viewer: any, mode: string): void {
|
|
if (!Object.values(MFV_MODES).includes(mode as any)) return;
|
|
viewer._mode = mode;
|
|
viewer._updateModeBtnUI();
|
|
viewer._refresh();
|
|
viewer._notifyModeChanged();
|
|
}
|
|
|
|
export function getFloatingViewerPinnedSlots(viewer: any): unknown[] {
|
|
return viewer._pinnedSlots;
|
|
}
|
|
|
|
export function updateFloatingViewerPinUI(viewer: any): void {
|
|
if (!viewer._pinBtns) return;
|
|
for (const slot of ["A", "B", "C", "D"]) {
|
|
const btn = viewer._pinBtns[slot];
|
|
if (!btn) continue;
|
|
const active = viewer._pinnedSlots.has(slot);
|
|
btn.classList.toggle("is-pinned", active);
|
|
btn.setAttribute("aria-pressed", String(active));
|
|
btn.title = active ? `Unpin Asset ${slot}` : `Pin Asset ${slot}`;
|
|
}
|
|
// Highlight pin trigger when any slot is pinned
|
|
viewer._pinBtn?.classList.toggle("is-on", (viewer._pinnedSlots?.size ?? 0) > 0);
|
|
}
|
|
|
|
export function updateFloatingViewerModeButtonUI(viewer: any): void {
|
|
if (!viewer._modeBtn) return;
|
|
const cfg = {
|
|
[MFV_MODES.SIMPLE]: { icon: "pi-image", label: "Mode: Simple - click to switch" },
|
|
[MFV_MODES.AB]: { icon: "pi-clone", label: "Mode: A/B Compare - click to switch" },
|
|
[MFV_MODES.SIDE]: { icon: "pi-table", label: "Mode: Side-by-Side - click to switch" },
|
|
[MFV_MODES.GRID]: {
|
|
icon: "pi-th-large",
|
|
label: "Mode: Grid Compare (up to 4) - click to switch",
|
|
},
|
|
[MFV_MODES.GRAPH]: {
|
|
icon: "pi-sitemap",
|
|
label: "Mode: Graph Map - click to switch",
|
|
},
|
|
};
|
|
const { icon = "pi-image", label = "" } = (cfg as Record<string, any>)[viewer._mode] || {};
|
|
const tooltip = appendTooltipHint(label, MFV_MODE_HINT);
|
|
const modeBtnIcon = document.createElement("i");
|
|
modeBtnIcon.className = `pi ${icon}`;
|
|
modeBtnIcon.setAttribute("aria-hidden", "true");
|
|
viewer._modeBtn.replaceChildren(modeBtnIcon);
|
|
viewer._modeBtn.title = tooltip;
|
|
viewer._modeBtn.setAttribute("aria-label", tooltip);
|
|
viewer._modeBtn.removeAttribute("aria-pressed");
|
|
// Highlight when any comparison mode is active
|
|
viewer._modeBtn.classList.toggle("is-on", viewer._mode !== MFV_MODES.SIMPLE);
|
|
viewer._modeDrop?.selectItem?.(viewer._mode);
|
|
}
|
|
|
|
export function setFloatingViewerLiveActive(viewer: any, active: any) {
|
|
if (!viewer._liveBtn) return;
|
|
const isActive = Boolean(active);
|
|
viewer._liveBtn.classList.toggle("mjr-live-active", isActive);
|
|
const label = isActive
|
|
? t(
|
|
"tooltip.liveStreamOn",
|
|
"Live Stream: ON - follows final generation outputs after execution",
|
|
)
|
|
: t("tooltip.liveStreamOff", "Live Stream: OFF - click to follow final generation outputs");
|
|
const tooltip = appendTooltipHint(label, MFV_LIVE_HINT);
|
|
viewer._liveBtn.setAttribute("aria-pressed", String(isActive));
|
|
viewer._liveBtn.setAttribute("aria-label", tooltip);
|
|
const liveIcon = document.createElement("i");
|
|
liveIcon.className = isActive ? "pi pi-circle-fill" : "pi pi-circle";
|
|
liveIcon.setAttribute("aria-hidden", "true");
|
|
viewer._liveBtn.replaceChildren(liveIcon);
|
|
viewer._liveBtn.title = tooltip;
|
|
}
|
|
|
|
export function setFloatingViewerPreviewActive(viewer: any, active: any) {
|
|
viewer._previewActive = Boolean(active);
|
|
if (!viewer._previewBtn) return;
|
|
viewer._previewBtn.classList.toggle("mjr-preview-active", viewer._previewActive);
|
|
const label = viewer._previewActive
|
|
? t(
|
|
"tooltip.previewStreamOn",
|
|
"KSampler Preview: ON - streams sampler denoising frames during execution",
|
|
)
|
|
: t(
|
|
"tooltip.previewStreamOff",
|
|
"KSampler Preview: OFF - click to stream sampler denoising frames",
|
|
);
|
|
const tooltip = appendTooltipHint(label, MFV_PREVIEW_HINT);
|
|
viewer._previewBtn.setAttribute("aria-pressed", String(viewer._previewActive));
|
|
viewer._previewBtn.setAttribute("aria-label", tooltip);
|
|
const icon = document.createElement("i");
|
|
icon.className = viewer._previewActive ? "pi pi-eye" : "pi pi-eye-slash";
|
|
icon.setAttribute("aria-hidden", "true");
|
|
viewer._previewBtn.replaceChildren(icon);
|
|
viewer._previewBtn.title = tooltip;
|
|
}
|
|
|
|
function _getPreviewBlobUrl(media: any): string {
|
|
if (!media?._isPreview) return "";
|
|
return String(media?._previewBlobUrl || media?.url || "").trim();
|
|
}
|
|
|
|
export function revokeDetachedFloatingViewerPreviewBlobs(viewer: any, previousMedia: any[]): void {
|
|
const liveUrls = new Set(
|
|
["A", "B", "C", "D"]
|
|
.map((slot) => _getPreviewBlobUrl(viewer[`_media${slot}`]))
|
|
.filter(Boolean),
|
|
);
|
|
for (const media of previousMedia) {
|
|
const url = _getPreviewBlobUrl(media);
|
|
if (!url || liveUrls.has(url)) continue;
|
|
try {
|
|
URL.revokeObjectURL(url);
|
|
} catch {
|
|
/* noop */
|
|
}
|
|
if (String(viewer._previewBlobUrl || "") === url) viewer._previewBlobUrl = null;
|
|
}
|
|
}
|
|
|
|
export function loadFloatingViewerPreviewBlob(
|
|
viewer: any,
|
|
blob: any,
|
|
opts: Record<string, any> = {},
|
|
) {
|
|
if (!blob || !(blob instanceof Blob)) return;
|
|
|
|
const inCompare =
|
|
viewer._mode === MFV_MODES.AB ||
|
|
viewer._mode === MFV_MODES.SIDE ||
|
|
viewer._mode === MFV_MODES.GRID;
|
|
let target = "A";
|
|
if (inCompare) {
|
|
const pins = viewer.getPinnedSlots();
|
|
if (viewer._mode === MFV_MODES.GRID) {
|
|
target = ["A", "B", "C", "D"].find((slot: any) => !pins.has(slot)) || "";
|
|
} else {
|
|
target = pins.has("B") ? (pins.has("A") ? "" : "A") : "B";
|
|
}
|
|
// A fully pinned comparison is immutable; drop the transient frame.
|
|
if (!target) return;
|
|
}
|
|
|
|
const previousMedia = viewer[`_media${target}`] || null;
|
|
const url = URL.createObjectURL(blob);
|
|
viewer._previewBlobUrl = url;
|
|
const mime = String(opts?.mime || blob.type || "image/jpeg")
|
|
.trim()
|
|
.toLowerCase();
|
|
const isVideo = mime === "video/mp4";
|
|
const filename =
|
|
opts?.filename ||
|
|
(isVideo
|
|
? "preview.mp4"
|
|
: mime === "image/webp"
|
|
? "preview.webp"
|
|
: mime === "image/png"
|
|
? "preview.png"
|
|
: "preview.jpg");
|
|
const fileData = {
|
|
url,
|
|
filename,
|
|
kind: isVideo ? "video" : "image",
|
|
mime,
|
|
width: Number(opts?.width) || undefined,
|
|
height: Number(opts?.height) || undefined,
|
|
fps: Number(opts?.fps) || undefined,
|
|
_isPreview: true,
|
|
_sourceLabel: opts?.sourceLabel || null,
|
|
_previewSource: opts?.source || "comfy",
|
|
_previewNodeId: opts?.nodeId != null ? String(opts.nodeId) : null,
|
|
_previewStep: Number.isFinite(Number(opts?.step)) ? Number(opts.step) : null,
|
|
_previewTotal: Number.isFinite(Number(opts?.total)) ? Number(opts.total) : null,
|
|
_previewBlobUrl: url,
|
|
};
|
|
|
|
if (inCompare) {
|
|
viewer[`_media${target}`] = fileData;
|
|
} else {
|
|
viewer._mediaA = fileData;
|
|
viewer._resetMfvZoom();
|
|
if (viewer._mode !== MFV_MODES.SIMPLE) {
|
|
viewer._mode = MFV_MODES.SIMPLE;
|
|
viewer._updateModeBtnUI();
|
|
}
|
|
}
|
|
|
|
revokeDetachedFloatingViewerPreviewBlobs(viewer, [previousMedia]);
|
|
++viewer._refreshGen;
|
|
viewer._refresh();
|
|
}
|
|
|
|
export function revokeFloatingViewerPreviewBlob(viewer: any) {
|
|
const urls = new Set<string>();
|
|
if (viewer._previewBlobUrl) urls.add(String(viewer._previewBlobUrl));
|
|
for (const slot of ["A", "B", "C", "D"]) {
|
|
const url = _getPreviewBlobUrl(viewer[`_media${slot}`]);
|
|
if (url) urls.add(url);
|
|
}
|
|
for (const url of urls) {
|
|
try {
|
|
URL.revokeObjectURL(url);
|
|
} catch {
|
|
/* noop */
|
|
}
|
|
}
|
|
viewer._previewBlobUrl = null;
|
|
}
|
|
|
|
export function setFloatingViewerNodeStreamActive(viewer: any, active: any) {
|
|
if (!NODE_STREAM_FEATURE_ENABLED) {
|
|
void active;
|
|
viewer._nodeStreamActive = false;
|
|
return;
|
|
}
|
|
|
|
viewer._nodeStreamActive = Boolean(active);
|
|
if (!viewer._nodeStreamActive) {
|
|
// Hide the selected-node overlay when the feature is turned off.
|
|
viewer.setNodeStreamSelection?.(null);
|
|
}
|
|
if (!viewer._nodeStreamBtn) return;
|
|
viewer._nodeStreamBtn.classList.toggle("mjr-nodestream-active", viewer._nodeStreamActive);
|
|
const label = viewer._nodeStreamActive
|
|
? t(
|
|
"tooltip.nodeStreamOn",
|
|
"Node Stream: ON - follows the selected node preview when frontend media exists",
|
|
)
|
|
: t(
|
|
"tooltip.nodeStreamOff",
|
|
"Node Stream: OFF - click to follow selected node previews, including ImageOps live canvases",
|
|
);
|
|
const tooltip = appendTooltipHint(label, MFV_NODESTREAM_HINT);
|
|
viewer._nodeStreamBtn.setAttribute("aria-pressed", String(viewer._nodeStreamActive));
|
|
viewer._nodeStreamBtn.setAttribute("aria-label", tooltip);
|
|
const icon = document.createElement("i");
|
|
icon.className = "pi pi-sitemap";
|
|
icon.setAttribute("aria-hidden", "true");
|
|
viewer._nodeStreamBtn.replaceChildren(icon);
|
|
viewer._nodeStreamBtn.title = tooltip;
|
|
}
|