497 lines
19 KiB
JavaScript
497 lines
19 KiB
JavaScript
// Run Timer 🆎 — a stopwatch for the whole queue, drawn as one black readout.
|
|
//
|
|
// The node IS the display: no title bar, no slots, no padding. On the
|
|
// classic canvas the readout is painted by the node itself (onDrawForeground)
|
|
// - no DOM widget, so the pointer lands on the canvas everywhere (drag from
|
|
// anywhere, native resize, right-click) and the box is exactly the node.
|
|
// The Nodes 2.0 renderer does not run canvas paint, so there the same
|
|
// readout mounts as a DOM widget instead.
|
|
//
|
|
// It listens to the api's execution events; a run starts the clock, ticks
|
|
// it, and the total holds when the run ends. Completed totals go into
|
|
// node.properties so a saved workflow reopens showing its author's time.
|
|
// Decision logic lives in js/shared/run_timer.mjs under node:test.
|
|
|
|
import { api } from "../../../scripts/api.js";
|
|
import { app } from "../../../scripts/app.js";
|
|
import { BRAND, chainCallback, keepDomWidgetWidthAuto } from "../shared/index.mjs";
|
|
import { holdNodeMinWidth } from "../shared/panel_layout.mjs";
|
|
import { isOwnRun, ownRuns } from "../shared/prompt_scope.mjs";
|
|
import {
|
|
cachedLayer,
|
|
createTimer,
|
|
formatElapsed,
|
|
layerScale,
|
|
startTimer,
|
|
stopTimer,
|
|
tickTimer,
|
|
} from "../shared/run_timer.mjs";
|
|
|
|
const NODE_CLASS = "AUSBOSS_NODES_RunTimer";
|
|
const CSS_ID = "ausboss-run-timer-css";
|
|
const WIDGET_NAME = "ausboss_run_timer_panel";
|
|
const PROPERTY = "ausboss_run_timer"; // { history: [seconds, newest first] }
|
|
const BASE_WIDTH = 176;
|
|
const BASE_HEIGHT = 56;
|
|
const MIN_WIDTH = 110;
|
|
const TICK_MS = 100;
|
|
const NO_TITLE = globalThis.LiteGraph?.NO_TITLE ?? 1;
|
|
const READOUT_COLOR = "#07090a";
|
|
const TEAL = "#2ee8dc";
|
|
const AMBER = "#ffd166";
|
|
const RED = "#ff5c4d";
|
|
const DIM = "#3d5a58";
|
|
const MONO = 'ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared state and events
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function savedHistory(node) {
|
|
const stored = node.properties?.[PROPERTY];
|
|
return Array.isArray(stored?.history) ? stored.history : [];
|
|
}
|
|
|
|
// "1:05" + ".3", or "12" + ".3" + "s": the whole seconds carry the weight,
|
|
// the tenths ride small beside them, hours fold in as h:mm:ss.
|
|
function splitDigits(seconds) {
|
|
if (!Number.isFinite(seconds) || seconds < 0) return { main: "0", tenths: ".0", unit: "s" };
|
|
const whole = Math.floor(seconds);
|
|
const tenths = `.${Math.floor((seconds - whole) * 10)}`;
|
|
if (whole < 60) return { main: String(whole), tenths, unit: "s" };
|
|
const hours = Math.floor(whole / 3600);
|
|
const minutes = Math.floor((whole % 3600) / 60);
|
|
const rest = String(whole % 60).padStart(2, "0");
|
|
if (hours) return { main: `${hours}:${String(minutes).padStart(2, "0")}:${rest}`, tenths: "", unit: "" };
|
|
return { main: `${minutes}:${rest}`, tenths, unit: "" };
|
|
}
|
|
|
|
// What the readout shows right now: the digits, the LED status and the
|
|
// digit color - one place, shared by the canvas paint and the DOM panel.
|
|
function readout(state) {
|
|
const { timer } = state;
|
|
const hasRun = timer.running || timer.outcome !== null || timer.history.length > 0;
|
|
const shown = timer.running || timer.outcome ? timer.elapsed : timer.history[0];
|
|
const parts = splitDigits(hasRun ? shown : 0);
|
|
let led = "idle";
|
|
if (timer.running) led = "running";
|
|
else if (timer.outcome) led = timer.outcome;
|
|
else if (hasRun) led = "done";
|
|
const failed = timer.outcome === "error" || timer.outcome === "interrupted";
|
|
return { ...parts, led, color: failed ? "#ff8a7d" : hasRun ? TEAL : DIM, hasRun };
|
|
}
|
|
|
|
function stopTicking(state) {
|
|
if (state.interval) clearInterval(state.interval);
|
|
state.interval = null;
|
|
}
|
|
|
|
function finish(state, outcome) {
|
|
if (!state.timer.running) return;
|
|
stopTicking(state);
|
|
state.timer = stopTimer(state.timer, performance.now(), outcome);
|
|
if (outcome === "done") {
|
|
state.node.properties ??= {};
|
|
state.node.properties[PROPERTY] = { history: state.timer.history };
|
|
}
|
|
state.render();
|
|
}
|
|
|
|
function begin(state, promptId) {
|
|
stopTicking(state);
|
|
// What the readout held before, in case this run turns out to be one a
|
|
// node started for itself (see takeBack).
|
|
state.before = state.timer;
|
|
state.runId = promptId ? String(promptId) : null;
|
|
state.timer = startTimer(state.timer, performance.now());
|
|
state.render();
|
|
state.interval = setInterval(() => {
|
|
state.timer = tickTimer(state.timer, performance.now());
|
|
state.render();
|
|
}, TICK_MS);
|
|
}
|
|
|
|
// The run on the readout was a node's own (Mask by Touch looking at its
|
|
// picture), found out only after it started: show what was there before.
|
|
function takeBack(state) {
|
|
if (!state.before) return;
|
|
stopTicking(state);
|
|
state.timer = state.before;
|
|
state.before = null;
|
|
state.runId = null;
|
|
state.node.properties ??= {};
|
|
state.node.properties[PROPERTY] = { history: state.timer.history };
|
|
state.render();
|
|
}
|
|
|
|
// A run a node starts for itself is not a run of the workflow: it is left
|
|
// out, so the readout keeps the time of the last real run.
|
|
function listen(state) {
|
|
const { signal } = state.abort;
|
|
const workflowRun = (handler) => ({ detail }) => {
|
|
if (!isOwnRun(detail?.prompt_id)) handler();
|
|
};
|
|
api.addEventListener("execution_start", ({ detail }) => {
|
|
if (!isOwnRun(detail?.prompt_id)) begin(state, detail?.prompt_id);
|
|
}, { signal });
|
|
api.addEventListener("execution_success", workflowRun(() => finish(state, "done")), { signal });
|
|
api.addEventListener("execution_error", workflowRun(() => finish(state, "error")), { signal });
|
|
api.addEventListener("execution_interrupted", workflowRun(() => finish(state, "interrupted")), { signal });
|
|
const stop = ownRuns.subscribe((promptId) => {
|
|
if (state.runId === promptId) takeBack(state);
|
|
});
|
|
signal.addEventListener("abort", stop);
|
|
}
|
|
|
|
// The frontend hangs a "which pack" badge above every custom node; on a
|
|
// title-less readout it floats in space. An empty badges list draws none.
|
|
function suppressBadges(node) {
|
|
try {
|
|
Object.defineProperty(node, "badges", {
|
|
configurable: true,
|
|
get: () => [],
|
|
set: () => {},
|
|
});
|
|
} catch {
|
|
node.badges = [];
|
|
}
|
|
}
|
|
|
|
function paintBlack(node) {
|
|
node.color = READOUT_COLOR;
|
|
node.bgcolor = READOUT_COLOR;
|
|
}
|
|
|
|
function usesVueNodes() {
|
|
try {
|
|
return app.ui?.settings?.getSettingValue?.("Comfy.VueNodes.Enabled") === true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Classic canvas: the node paints its own readout
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function ledColor(status) {
|
|
if (status === "running") return AMBER;
|
|
if (status === "done") return TEAL;
|
|
if (status === "error" || status === "interrupted") return RED;
|
|
return "#2b3336";
|
|
}
|
|
|
|
// Glow as a blurred underlay pass, not a canvas shadow: LiteGraph leaves
|
|
// shadow offsets set from painting the node body, and a shadow drawn on
|
|
// top of that lands as a displaced copy of every glyph.
|
|
function glowText(ctx, text, x, y, color, radius) {
|
|
if (radius <= 0 || !("filter" in ctx)) return;
|
|
ctx.save();
|
|
ctx.filter = `blur(${radius}px)`;
|
|
ctx.globalAlpha = 0.85;
|
|
ctx.fillStyle = color;
|
|
ctx.fillText(text, x, y);
|
|
ctx.restore();
|
|
}
|
|
|
|
// Every blurred pass - the LED halo and the digit glow - painted into the
|
|
// small cached layer (cachedLayer), never onto the graph canvas itself.
|
|
// A filter blur works in device pixels, so the radii are not scaled.
|
|
function paintGlow(ctx, view, layout, led, pulse) {
|
|
const { s, ledX, ledY, x0, baseline, mainW, fontBig, fontSmall, glow } = layout;
|
|
ctx.textBaseline = "alphabetic";
|
|
ctx.textAlign = "left";
|
|
if (view.led !== "idle") {
|
|
ctx.save();
|
|
ctx.filter = `blur(${4 * s}px)`;
|
|
ctx.globalAlpha = 0.9 * pulse;
|
|
ctx.fillStyle = led;
|
|
ctx.beginPath();
|
|
ctx.arc(ledX, ledY, 4.5 * s, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
ctx.font = fontBig;
|
|
glowText(ctx, view.main, x0, baseline, view.color, glow);
|
|
if (view.tenths) {
|
|
ctx.font = fontSmall;
|
|
glowText(ctx, view.tenths, x0 + mainW, baseline, view.color, glow * 0.8);
|
|
}
|
|
}
|
|
|
|
function drawGlow(ctx, state, view, layout, led, pulse) {
|
|
if (view.led === "idle" && !layout.glow) return;
|
|
const [w, h] = layout.size;
|
|
const m = ctx.getTransform?.();
|
|
const scale = layerScale(m ? Math.hypot(m.a, m.b) : 1, w);
|
|
const pad = Math.ceil(15 * layout.s); // three blur radii of spill room
|
|
const key = [view.main, view.tenths, view.unit, view.color, view.led, pulse, w, h, scale].join("|");
|
|
const layer = cachedLayer(state.glow, key, w * scale + 2 * pad, h * scale + 2 * pad, (g) => {
|
|
g.setTransform(scale, 0, 0, scale, pad, pad);
|
|
paintGlow(g, view, layout, led, pulse);
|
|
}, () => document.createElement("canvas"));
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.drawImage(layer, -pad / scale, -pad / scale, layer.width / scale, layer.height / scale);
|
|
}
|
|
|
|
function drawReadout(node, ctx, state) {
|
|
const [w, h] = node.size;
|
|
const s = h / BASE_HEIGHT;
|
|
const view = readout(state);
|
|
const big = Math.max(10, Math.round(28 * s));
|
|
const small = Math.max(7, Math.round(15 * s));
|
|
const tiny = Math.max(6, Math.round(10 * s));
|
|
ctx.save();
|
|
ctx.shadowColor = "transparent";
|
|
ctx.shadowBlur = 0;
|
|
ctx.shadowOffsetX = 0;
|
|
ctx.shadowOffsetY = 0;
|
|
ctx.textBaseline = "alphabetic";
|
|
ctx.textAlign = "left";
|
|
|
|
// digits, measured then centred in the space right of the LED
|
|
const fontBig = `700 ${big}px ${MONO}`;
|
|
const fontSmall = `600 ${small}px ${MONO}`;
|
|
const fontTiny = `500 ${tiny}px ${MONO}`;
|
|
ctx.font = fontBig;
|
|
const mainW = ctx.measureText(view.main).width;
|
|
ctx.font = fontSmall;
|
|
const tenthsW = view.tenths ? ctx.measureText(view.tenths).width : 0;
|
|
ctx.font = fontTiny;
|
|
const unitW = view.unit ? ctx.measureText(view.unit).width + 3 * s : 0;
|
|
const total = mainW + tenthsW + unitW;
|
|
const left = 22 * s;
|
|
const x0 = left + Math.max(0, (w - left - 8 * s - total) / 2);
|
|
const baseline = h / 2 + big * 0.36;
|
|
const glow = view.hasRun ? 5 * s : 0;
|
|
|
|
// LED and digit glow from the cached layer, then the crisp LED dot. The
|
|
// pulse is quantised so a held LED reuses the layer frame after frame.
|
|
const led = ledColor(view.led);
|
|
const pulse = view.led === "running"
|
|
? Math.round((0.55 + 0.45 * Math.sin(performance.now() / 160)) * 20) / 20
|
|
: 1;
|
|
const ledX = 12 * s;
|
|
const ledY = h / 2;
|
|
const layout = { size: [w, h], s, ledX, ledY, x0, baseline, mainW, fontBig, fontSmall, glow };
|
|
drawGlow(ctx, state, view, layout, led, pulse);
|
|
ctx.globalAlpha = pulse;
|
|
ctx.fillStyle = led;
|
|
ctx.beginPath();
|
|
ctx.arc(ledX, ledY, 3.2 * s, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
|
|
ctx.font = fontBig;
|
|
ctx.fillStyle = view.color;
|
|
ctx.fillText(view.main, x0, baseline);
|
|
let x = x0 + mainW;
|
|
if (view.tenths) {
|
|
ctx.font = fontSmall;
|
|
ctx.globalAlpha = 0.85;
|
|
ctx.fillStyle = view.color;
|
|
ctx.fillText(view.tenths, x, baseline);
|
|
ctx.globalAlpha = 1;
|
|
x += tenthsW;
|
|
}
|
|
if (view.unit) {
|
|
ctx.font = fontTiny;
|
|
ctx.globalAlpha = 0.6;
|
|
ctx.fillStyle = view.color;
|
|
ctx.fillText(view.unit, x + 3 * s, baseline);
|
|
ctx.globalAlpha = 1;
|
|
}
|
|
ctx.restore();
|
|
}
|
|
|
|
// Width is the handle, height follows. During a corner drag LiteGraph
|
|
// writes node.size directly on every move and calls onResize once at
|
|
// most, so this runs from the draw path as well: a tall or squat box is
|
|
// corrected before the next frame, the way Pixaroma's clock snaps back.
|
|
function keepAspect(node) {
|
|
const width = Math.max(MIN_WIDTH, Math.round(node.size?.[0] ?? BASE_WIDTH));
|
|
const height = Math.round((width * BASE_HEIGHT) / BASE_WIDTH);
|
|
if (node.size[0] === width && node.size[1] === height) return false;
|
|
// setSize is the frontend's sanctioned write (it runs onResize); the
|
|
// equality check above is what stops that callback from recursing.
|
|
if (typeof node.setSize === "function") node.setSize([width, height]);
|
|
else node.size = [width, height];
|
|
return true;
|
|
}
|
|
|
|
function installCanvasReadout(node) {
|
|
if (node.__ausbossRunTimer) return node.__ausbossRunTimer;
|
|
node.flags ??= {};
|
|
node.flags.no_title = true;
|
|
suppressBadges(node);
|
|
paintBlack(node);
|
|
const state = (node.__ausbossRunTimer = {
|
|
node, abort: new AbortController(), interval: null, mode: "canvas",
|
|
timer: createTimer(savedHistory(node)), glow: {},
|
|
render: () => node.setDirtyCanvas?.(true, false),
|
|
});
|
|
// Width is the handle: drag the corner and the readout scales with it.
|
|
// computeSize is the MINIMUM the canvas will let a drag reach - it must
|
|
// never echo the current size, or the node can only ever grow.
|
|
node.resizable = true;
|
|
node.computeSize = () => [MIN_WIDTH, Math.round((MIN_WIDTH * BASE_HEIGHT) / BASE_WIDTH)];
|
|
// A saved workflow overwrites this in onConfigure; a fresh node starts here.
|
|
node.size = [BASE_WIDTH, BASE_HEIGHT];
|
|
chainCallback(node, "onResize", function () { keepAspect(this); });
|
|
chainCallback(node, "onDrawForeground", function (ctx) {
|
|
if (this.flags?.collapsed) return;
|
|
// The body was already painted at whatever size the drag left; fix
|
|
// the size now and ask for one more frame so the box catches up.
|
|
if (keepAspect(this)) this.setDirtyCanvas?.(true, false);
|
|
drawReadout(this, ctx, state);
|
|
});
|
|
listen(state);
|
|
return state;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Nodes 2.0 fallback: the same readout as a DOM panel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function ensureCss() {
|
|
if (document.getElementById(CSS_ID)) return;
|
|
const style = document.createElement("style");
|
|
style.id = CSS_ID;
|
|
style.textContent = `
|
|
.ausboss-timer{box-sizing:border-box;width:100%;height:100%;pointer-events:none;overflow:hidden;}
|
|
.ausboss-timer-box{box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:10px;width:100%;height:100%;padding:0 12px;overflow:hidden;border-radius:9px;background:${READOUT_COLOR};}
|
|
.ausboss-timer-led{flex:none;width:7px;height:7px;border-radius:50%;background:#2b3336;}
|
|
.ausboss-timer-led.running{background:${AMBER};box-shadow:0 0 8px rgba(255,209,102,.9);animation:ausboss-timer-pulse 1s ease-in-out infinite;}
|
|
.ausboss-timer-led.done{background:${BRAND};box-shadow:0 0 8px rgba(0,180,170,.8);}
|
|
.ausboss-timer-led.error,.ausboss-timer-led.interrupted{background:${RED};box-shadow:0 0 8px rgba(255,92,77,.8);}
|
|
@keyframes ausboss-timer-pulse{0%,100%{opacity:1;}50%{opacity:.35;}}
|
|
.ausboss-timer-digits{display:flex;align-items:baseline;color:${TEAL};font:700 28px/1 ${MONO};font-variant-numeric:tabular-nums;letter-spacing:.02em;text-shadow:0 0 12px rgba(46,232,220,.35);white-space:nowrap;}
|
|
.ausboss-timer-digits.idle{color:${DIM};text-shadow:none;}
|
|
.ausboss-timer-digits.error{color:#ff8a7d;text-shadow:0 0 12px rgba(255,92,77,.35);}
|
|
.ausboss-timer-digits .tenths{font-size:15px;margin-left:2px;opacity:.78;}
|
|
.ausboss-timer-digits .unit{font-size:10px;margin-left:5px;letter-spacing:.12em;opacity:.55;}
|
|
/* Nodes 2.0 only: the Vue node wraps this panel in a 225px-minimum body and
|
|
hangs its own pack badge in a footer row; the readout is the whole node. */
|
|
.lg-node:has(.ausboss-timer){min-width:0!important;}
|
|
.lg-node:has(.ausboss-timer) .bg-component-node-background>div:last-child{display:none;}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function installDomReadout(node) {
|
|
if (node.__ausbossRunTimer) return node.__ausbossRunTimer;
|
|
ensureCss();
|
|
node.flags ??= {};
|
|
node.flags.no_title = true;
|
|
suppressBadges(node);
|
|
paintBlack(node);
|
|
|
|
const root = document.createElement("div");
|
|
root.className = "ausboss-timer";
|
|
const box = document.createElement("div");
|
|
box.className = "ausboss-timer-box";
|
|
const led = document.createElement("span");
|
|
led.className = "ausboss-timer-led";
|
|
const digits = document.createElement("span");
|
|
digits.className = "ausboss-timer-digits idle";
|
|
const main = document.createElement("span");
|
|
const tenths = document.createElement("span");
|
|
tenths.className = "tenths";
|
|
const unit = document.createElement("span");
|
|
unit.className = "unit";
|
|
digits.append(main, tenths, unit);
|
|
box.append(led, digits);
|
|
root.append(box);
|
|
|
|
const widget = node.addDOMWidget(WIDGET_NAME, "ausboss_run_timer", root, {
|
|
serialize: false,
|
|
hideOnZoom: false,
|
|
margin: 0,
|
|
getMinHeight: () => BASE_HEIGHT,
|
|
});
|
|
keepDomWidgetWidthAuto(widget);
|
|
// Not saved with the workflow either: options.serialize only keeps it out
|
|
// of the prompt, and saved values come back by position.
|
|
widget.serialize = false;
|
|
// A constant-height readout, not a viewport: pinned on purpose (see
|
|
// tests/panel_guards.test.mjs, fixedByDesign).
|
|
widget.computeSize = (width) => [Math.max(MIN_WIDTH, Number(width || node.size?.[0] || BASE_WIDTH)), BASE_HEIGHT];
|
|
widget.computeLayoutSize = () => ({ minWidth: MIN_WIDTH, minHeight: BASE_HEIGHT });
|
|
widget.options.minNodeSize = [MIN_WIDTH, BASE_HEIGHT];
|
|
holdNodeMinWidth(widget, MIN_WIDTH);
|
|
|
|
const state = (node.__ausbossRunTimer = {
|
|
node, abort: new AbortController(), interval: null, mode: "dom",
|
|
timer: createTimer(savedHistory(node)),
|
|
render: () => {
|
|
const view = readout(state);
|
|
main.textContent = view.main;
|
|
tenths.textContent = view.tenths;
|
|
unit.textContent = view.unit;
|
|
led.className = `ausboss-timer-led ${view.led === "idle" ? "" : view.led}`;
|
|
digits.className = "ausboss-timer-digits";
|
|
if (!view.hasRun) digits.classList.add("idle");
|
|
if (view.led === "error" || view.led === "interrupted") digits.classList.add("error");
|
|
},
|
|
});
|
|
state.render();
|
|
listen(state);
|
|
return state;
|
|
}
|
|
|
|
function install(node) {
|
|
return usesVueNodes() ? installDomReadout(node) : installCanvasReadout(node);
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "AusBoss.RunTimer",
|
|
beforeRegisterNodeDef(nodeType, nodeData) {
|
|
if (nodeData?.name !== NODE_CLASS) return;
|
|
// Set once on the TYPE at registration: the classic renderer reads it
|
|
// per draw, and the Nodes 2.0 renderer snapshots it when the node is
|
|
// created, so a per-node write would come too late there.
|
|
nodeType.title_mode = NO_TITLE;
|
|
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
|
install(this);
|
|
});
|
|
chainCallback(nodeType.prototype, "onConfigure", function () {
|
|
// Saved properties land after onNodeCreated: pick the history up then,
|
|
// and keep the readout black however the save was colored.
|
|
queueMicrotask(() => {
|
|
const state = install(this);
|
|
if (!state) return;
|
|
if (!state.timer.running) state.timer = createTimer(savedHistory(this));
|
|
this.flags.no_title = true;
|
|
paintBlack(this);
|
|
if (state.mode === "canvas") keepAspect(this);
|
|
state.render();
|
|
});
|
|
});
|
|
chainCallback(nodeType.prototype, "getExtraMenuOptions", function (_canvas, options) {
|
|
const state = this.__ausbossRunTimer;
|
|
if (!state || !Array.isArray(options)) return;
|
|
const previous = state.timer.history.slice(0, 5).map(formatElapsed).join(" · ");
|
|
options.unshift(
|
|
{ content: previous ? `Runs: ${previous}` : "No completed runs yet", disabled: true },
|
|
{
|
|
content: "Reset Run Timer history",
|
|
callback: () => {
|
|
state.timer = createTimer([]);
|
|
this.properties ??= {};
|
|
this.properties[PROPERTY] = { history: [] };
|
|
state.render();
|
|
},
|
|
},
|
|
null,
|
|
);
|
|
});
|
|
chainCallback(nodeType.prototype, "onRemoved", function () {
|
|
const state = this.__ausbossRunTimer;
|
|
if (!state) return;
|
|
stopTicking(state);
|
|
state.abort.abort();
|
|
this.__ausbossRunTimer = null;
|
|
});
|
|
},
|
|
});
|