540 lines
19 KiB
JavaScript
540 lines
19 KiB
JavaScript
import { app } from "../../scripts/app.js";
|
|
import { api } from "../../scripts/api.js";
|
|
import { $el } from "../../scripts/ui.js";
|
|
|
|
/**
|
|
* Sketch-style Color Picker for Settings
|
|
*/
|
|
class SketchColorPicker {
|
|
constructor(initialColor = '#ff9966', onChange = () => {}) {
|
|
this.color = this.hexToHsv(initialColor);
|
|
this.hex = initialColor;
|
|
this.onChange = onChange;
|
|
this.isOpen = false;
|
|
|
|
this.presetColors = [
|
|
'#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321',
|
|
'#417505', '#BD10E0', '#9013FE', '#4A90D9', '#50E3C2',
|
|
'#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF',
|
|
];
|
|
|
|
this.createElements();
|
|
}
|
|
|
|
createElements() {
|
|
// Swatch button
|
|
this.swatch = document.createElement('div');
|
|
this.swatch.style.cssText = `
|
|
width: 50px; height: 28px; border-radius: 4px;
|
|
background: ${this.hex}; cursor: pointer;
|
|
box-shadow: 0 0 0 1px rgba(0,0,0,.2), inset 0 0 0 1px rgba(0,0,0,.1);
|
|
display: inline-block;
|
|
`;
|
|
this.swatch.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
this.toggle();
|
|
});
|
|
|
|
// Popup container
|
|
this.popup = document.createElement('div');
|
|
this.popup.style.cssText = `
|
|
position: absolute; z-index: 10000;
|
|
background: #2a2a2a; border-radius: 6px;
|
|
box-shadow: 0 0 0 1px rgba(255,255,255,.1), 0 8px 24px rgba(0,0,0,.4);
|
|
padding: 12px; display: none; width: 220px;
|
|
bottom: 100%; margin-bottom: 8px; right: 0;
|
|
`;
|
|
|
|
// Saturation/Brightness picker
|
|
this.satBright = document.createElement('div');
|
|
this.satBright.style.cssText = `
|
|
width: 196px; height: 140px; position: relative;
|
|
border-radius: 4px; cursor: crosshair; margin-bottom: 12px;
|
|
`;
|
|
this.updateSatBrightBackground();
|
|
|
|
this.satBrightPointer = document.createElement('div');
|
|
this.satBrightPointer.style.cssText = `
|
|
width: 14px; height: 14px; border-radius: 50%;
|
|
border: 2px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.3), 0 2px 4px rgba(0,0,0,.3);
|
|
position: absolute; transform: translate(-50%, -50%);
|
|
pointer-events: none;
|
|
`;
|
|
this.satBright.appendChild(this.satBrightPointer);
|
|
|
|
// Hue slider
|
|
this.hueSlider = document.createElement('div');
|
|
this.hueSlider.style.cssText = `
|
|
width: 196px; height: 14px; border-radius: 4px;
|
|
background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
|
|
position: relative; cursor: pointer; margin-bottom: 12px;
|
|
`;
|
|
this.huePointer = document.createElement('div');
|
|
this.huePointer.style.cssText = `
|
|
width: 8px; height: 18px; border-radius: 3px;
|
|
background: #fff; border: 1px solid rgba(0,0,0,.3);
|
|
position: absolute; top: -2px; transform: translateX(-50%);
|
|
pointer-events: none; box-shadow: 0 1px 3px rgba(0,0,0,.3);
|
|
`;
|
|
this.hueSlider.appendChild(this.huePointer);
|
|
|
|
// Hex input row
|
|
this.hexRow = document.createElement('div');
|
|
this.hexRow.style.cssText = 'display: flex; align-items: center; margin-bottom: 12px; gap: 8px;';
|
|
|
|
this.hexInput = document.createElement('input');
|
|
this.hexInput.type = 'text';
|
|
this.hexInput.value = this.hex;
|
|
this.hexInput.style.cssText = `
|
|
flex: 1; padding: 6px 8px; border: 1px solid #444;
|
|
border-radius: 4px; font-size: 13px; font-family: monospace;
|
|
text-transform: uppercase; background: #1a1a1a; color: #eee;
|
|
`;
|
|
this.hexInput.addEventListener('change', () => this.setHex(this.hexInput.value));
|
|
|
|
const hexLabel = document.createElement('span');
|
|
hexLabel.textContent = 'Hex';
|
|
hexLabel.style.cssText = 'font-size: 12px; color: #888; min-width: 28px;';
|
|
|
|
this.hexRow.appendChild(this.hexInput);
|
|
this.hexRow.appendChild(hexLabel);
|
|
|
|
// Preset swatches
|
|
this.presetsContainer = document.createElement('div');
|
|
this.presetsContainer.style.cssText = `
|
|
display: flex; flex-wrap: wrap; gap: 6px;
|
|
border-top: 1px solid #444; padding-top: 12px;
|
|
`;
|
|
|
|
this.presetColors.forEach(color => {
|
|
const preset = document.createElement('div');
|
|
preset.style.cssText = `
|
|
width: 18px; height: 18px; border-radius: 3px;
|
|
background: ${color}; cursor: pointer;
|
|
box-shadow: inset 0 0 0 1px rgba(0,0,0,.2);
|
|
transition: transform 0.1s;
|
|
`;
|
|
preset.addEventListener('mouseenter', () => preset.style.transform = 'scale(1.15)');
|
|
preset.addEventListener('mouseleave', () => preset.style.transform = 'scale(1)');
|
|
preset.addEventListener('click', () => this.setHex(color));
|
|
this.presetsContainer.appendChild(preset);
|
|
});
|
|
|
|
// Assemble popup
|
|
this.popup.appendChild(this.satBright);
|
|
this.popup.appendChild(this.hueSlider);
|
|
this.popup.appendChild(this.hexRow);
|
|
this.popup.appendChild(this.presetsContainer);
|
|
|
|
// Event handlers
|
|
this.setupDrag(this.satBright, this.handleSatBrightChange.bind(this));
|
|
this.setupDrag(this.hueSlider, this.handleHueChange.bind(this));
|
|
|
|
// Close on outside click
|
|
this.closeHandler = (e) => {
|
|
if (!this.popup.contains(e.target) && e.target !== this.swatch) {
|
|
this.close();
|
|
}
|
|
};
|
|
}
|
|
|
|
setupDrag(element, handler) {
|
|
const onMove = (e) => {
|
|
e.preventDefault();
|
|
const rect = element.getBoundingClientRect();
|
|
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
|
|
handler(x, y);
|
|
};
|
|
|
|
const onUp = () => {
|
|
document.removeEventListener('mousemove', onMove);
|
|
document.removeEventListener('mouseup', onUp);
|
|
};
|
|
|
|
// Click and drag behavior
|
|
element.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
onMove(e);
|
|
document.addEventListener('mousemove', onMove);
|
|
document.addEventListener('mouseup', onUp);
|
|
});
|
|
|
|
// Hover preview - color follows mouse pointer
|
|
element.addEventListener('mousemove', (e) => {
|
|
if (e.buttons === 0) { // Only on hover, not during drag
|
|
const rect = element.getBoundingClientRect();
|
|
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
|
|
handler(x, y);
|
|
}
|
|
});
|
|
}
|
|
|
|
handleSatBrightChange(x, y) {
|
|
this.color.s = x * 100;
|
|
this.color.v = (1 - y) * 100;
|
|
this.updateFromHsv();
|
|
}
|
|
|
|
handleHueChange(x) {
|
|
this.color.h = x * 360;
|
|
this.updateFromHsv();
|
|
}
|
|
|
|
updateFromHsv() {
|
|
this.hex = this.hsvToHex(this.color.h, this.color.s, this.color.v);
|
|
this.updateUI();
|
|
this.onChange(this.hex);
|
|
}
|
|
|
|
updateUI() {
|
|
this.swatch.style.background = this.hex;
|
|
this.updateSatBrightBackground();
|
|
this.satBrightPointer.style.left = `${this.color.s}%`;
|
|
this.satBrightPointer.style.top = `${100 - this.color.v}%`;
|
|
this.huePointer.style.left = `${(this.color.h / 360) * 100}%`;
|
|
this.hexInput.value = this.hex.toUpperCase();
|
|
}
|
|
|
|
updateSatBrightBackground() {
|
|
const hueColor = this.hsvToHex(this.color.h, 100, 100);
|
|
this.satBright.style.background = `
|
|
linear-gradient(to top, #000, transparent),
|
|
linear-gradient(to right, #fff, ${hueColor})
|
|
`;
|
|
}
|
|
|
|
setHex(hex) {
|
|
if (!/^#?[0-9A-Fa-f]{6}$/.test(hex)) return;
|
|
if (!hex.startsWith('#')) hex = '#' + hex;
|
|
this.hex = hex;
|
|
this.color = this.hexToHsv(hex);
|
|
this.updateUI();
|
|
this.onChange(this.hex);
|
|
}
|
|
|
|
toggle() { this.isOpen ? this.close() : this.open(); }
|
|
|
|
open() {
|
|
this.isOpen = true;
|
|
this.popup.style.display = 'block';
|
|
this.updateUI();
|
|
setTimeout(() => document.addEventListener('click', this.closeHandler), 0);
|
|
}
|
|
|
|
close() {
|
|
this.isOpen = false;
|
|
this.popup.style.display = 'none';
|
|
document.removeEventListener('click', this.closeHandler);
|
|
}
|
|
|
|
getElement() {
|
|
const container = document.createElement('div');
|
|
container.style.cssText = 'position: relative; display: inline-block;';
|
|
container.appendChild(this.swatch);
|
|
container.appendChild(this.popup);
|
|
return container;
|
|
}
|
|
|
|
hexToHsv(hex) {
|
|
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
const v = max * 100, d = max - min;
|
|
const s = max === 0 ? 0 : (d / max) * 100;
|
|
let h = 0;
|
|
if (d !== 0) {
|
|
switch (max) {
|
|
case r: h = ((g - b) / d + (g < b ? 6 : 0)) * 60; break;
|
|
case g: h = ((b - r) / d + 2) * 60; break;
|
|
case b: h = ((r - g) / d + 4) * 60; break;
|
|
}
|
|
}
|
|
return { h, s, v };
|
|
}
|
|
|
|
hsvToHex(h, s, v) {
|
|
s /= 100; v /= 100;
|
|
const c = v * s, x = c * (1 - Math.abs(((h / 60) % 2) - 1)), m = v - c;
|
|
let r = 0, g = 0, b = 0;
|
|
if (h < 60) { r = c; g = x; }
|
|
else if (h < 120) { r = x; g = c; }
|
|
else if (h < 180) { g = c; b = x; }
|
|
else if (h < 240) { g = x; b = c; }
|
|
else if (h < 300) { r = x; b = c; }
|
|
else { r = c; b = x; }
|
|
const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, '0');
|
|
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Global Timer Manager
|
|
*/
|
|
const KikoGlobalTimer = {
|
|
startTime: 0,
|
|
intervalId: null,
|
|
isRunning: false,
|
|
activeNodes: new Set(),
|
|
|
|
formatTime(ms) {
|
|
if (ms < 0) ms = 0;
|
|
const minutes = String(Math.floor(ms / 60000)).padStart(2, '0');
|
|
const seconds = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
|
|
const milliseconds = String(ms % 1000).padStart(3, '0');
|
|
return `${minutes}:${seconds}:${milliseconds}`;
|
|
},
|
|
|
|
getTimerColor() {
|
|
return app.ui.settings.getSettingValue("kikotools.workflow_timer.color") || '#ff9966';
|
|
},
|
|
|
|
isGlowEnabled() {
|
|
const val = app.ui.settings.getSettingValue("kikotools.workflow_timer.glow");
|
|
return val !== undefined ? val : true;
|
|
},
|
|
|
|
start() {
|
|
if (this.isRunning) return;
|
|
this.isRunning = true;
|
|
this.startTime = Date.now();
|
|
|
|
const timerColor = this.getTimerColor();
|
|
this.activeNodes.forEach(node => {
|
|
if (node.timerDisplay) {
|
|
node.timerDisplay.style.color = timerColor;
|
|
node.timerDisplay.classList.add('running');
|
|
}
|
|
});
|
|
|
|
this.intervalId = setInterval(() => {
|
|
const elapsed = Date.now() - this.startTime;
|
|
const timeString = this.formatTime(elapsed);
|
|
this.activeNodes.forEach(node => {
|
|
if (node.timerDisplay) {
|
|
node.timerDisplay.textContent = timeString;
|
|
}
|
|
});
|
|
}, 33);
|
|
},
|
|
|
|
stop() {
|
|
if (!this.isRunning) return;
|
|
this.isRunning = false;
|
|
clearInterval(this.intervalId);
|
|
|
|
const finalTime = Date.now() - this.startTime;
|
|
const finalTimeString = this.formatTime(finalTime);
|
|
const timerColor = this.getTimerColor();
|
|
|
|
this.activeNodes.forEach(node => {
|
|
if (node.timerDisplay) {
|
|
node.timerDisplay.textContent = finalTimeString;
|
|
node.timerDisplay.style.color = timerColor;
|
|
node.timerDisplay.classList.remove('running');
|
|
}
|
|
node.properties.elapsed_time_str = finalTimeString;
|
|
});
|
|
},
|
|
|
|
updateColor(color) {
|
|
if (this.isRunning) return;
|
|
this.activeNodes.forEach(node => {
|
|
if (node.timerDisplay) {
|
|
node.timerDisplay.style.color = color;
|
|
}
|
|
});
|
|
},
|
|
|
|
updateGlow(enabled) {
|
|
this.activeNodes.forEach(node => {
|
|
if (node.timerDisplay) {
|
|
node.timerDisplay.classList.toggle('no-glow', !enabled);
|
|
}
|
|
});
|
|
},
|
|
|
|
registerNode(node) { this.activeNodes.add(node); },
|
|
unregisterNode(node) { this.activeNodes.delete(node); },
|
|
};
|
|
|
|
/**
|
|
* ComfyUI Extension for KikoWorkflow Timer
|
|
*/
|
|
const KikoWorkflowTimerExtension = {
|
|
name: "ComfyAssets.KikoWorkflowTimer",
|
|
|
|
async init() {
|
|
// Color picker setting with custom render
|
|
app.ui.settings.addSetting({
|
|
id: "kikotools.workflow_timer.color",
|
|
name: "🫶 Workflow Timer: Color",
|
|
type: (name, setter, value) => {
|
|
const container = $el("div", {
|
|
style: { display: "flex", alignItems: "center", gap: "10px" }
|
|
});
|
|
|
|
const picker = new SketchColorPicker(value || '#ff9966', (newColor) => {
|
|
setter(newColor);
|
|
KikoGlobalTimer.updateColor(newColor);
|
|
});
|
|
|
|
container.appendChild(picker.getElement());
|
|
|
|
// Also show hex value as text
|
|
const hexDisplay = $el("span", {
|
|
textContent: value || '#ff9966',
|
|
style: { color: "#888", fontFamily: "monospace", fontSize: "12px" }
|
|
});
|
|
container.appendChild(hexDisplay);
|
|
|
|
// Update hex display when color changes
|
|
const origOnChange = picker.onChange;
|
|
picker.onChange = (newColor) => {
|
|
origOnChange(newColor);
|
|
hexDisplay.textContent = newColor.toUpperCase();
|
|
};
|
|
|
|
return container;
|
|
},
|
|
defaultValue: "#ff9966",
|
|
tooltip: "Color of the timer display when not running",
|
|
});
|
|
|
|
// Glow toggle
|
|
app.ui.settings.addSetting({
|
|
id: "kikotools.workflow_timer.glow",
|
|
name: "🫶 Workflow Timer: Enable Glow",
|
|
type: "boolean",
|
|
defaultValue: true,
|
|
tooltip: "Enable pulsing glow effect on the timer",
|
|
onChange: (value) => {
|
|
KikoGlobalTimer.updateGlow(value);
|
|
},
|
|
});
|
|
},
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name === "KikoWorkflowTimer") {
|
|
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function() {
|
|
origOnNodeCreated?.apply(this, arguments);
|
|
|
|
this.bgcolor = "#1a1a2e";
|
|
this.color = "#16213e";
|
|
this.title = "Workflow Timer";
|
|
this.properties = this.properties || {};
|
|
this.size = [300, 100];
|
|
|
|
const container = document.createElement("div");
|
|
container.className = "kiko-timer-container";
|
|
|
|
this.timerDisplay = document.createElement("div");
|
|
this.timerDisplay.className = "kiko-timer-display";
|
|
this.timerDisplay.textContent = this.properties.elapsed_time_str || "00:00:000";
|
|
|
|
const timerColor = KikoGlobalTimer.getTimerColor();
|
|
this.timerDisplay.style.color = timerColor;
|
|
|
|
if (!KikoGlobalTimer.isGlowEnabled()) {
|
|
this.timerDisplay.classList.add('no-glow');
|
|
}
|
|
|
|
container.appendChild(this.timerDisplay);
|
|
this.addDOMWidget("kikoTimer", "Kiko Timer", container, { serialize: false });
|
|
|
|
KikoGlobalTimer.registerNode(this);
|
|
};
|
|
|
|
nodeType.prototype.onRemoved = function() {
|
|
KikoGlobalTimer.unregisterNode(this);
|
|
};
|
|
|
|
const origOnSerialize = nodeType.prototype.onSerialize;
|
|
nodeType.prototype.onSerialize = function(o) {
|
|
origOnSerialize?.apply(this, arguments);
|
|
o.properties = this.properties;
|
|
};
|
|
|
|
const origOnConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function(info) {
|
|
origOnConfigure?.apply(this, arguments);
|
|
this.properties = info.properties || {};
|
|
|
|
if (this.timerDisplay) {
|
|
this.timerDisplay.textContent = this.properties.elapsed_time_str || "00:00:000";
|
|
const timerColor = KikoGlobalTimer.getTimerColor();
|
|
this.timerDisplay.style.color = timerColor;
|
|
|
|
if (!KikoGlobalTimer.isGlowEnabled()) {
|
|
this.timerDisplay.classList.add('no-glow');
|
|
}
|
|
}
|
|
};
|
|
}
|
|
},
|
|
|
|
setup() {
|
|
const style = document.createElement("style");
|
|
style.textContent = `
|
|
@keyframes kiko-timer-pulse {
|
|
0%, 100% { text-shadow: 0 0 15px currentColor; opacity: 0.9; }
|
|
50% { text-shadow: 0 0 25px currentColor; opacity: 1; }
|
|
}
|
|
|
|
@keyframes kiko-timer-running-pulse {
|
|
0%, 100% { text-shadow: 0 0 15px currentColor; opacity: 0.9; }
|
|
50% { text-shadow: 0 0 25px currentColor; opacity: 1; }
|
|
}
|
|
|
|
.kiko-timer-container {
|
|
width: 100%; height: 100%;
|
|
position: relative; display: flex;
|
|
align-items: center; justify-content: center;
|
|
}
|
|
|
|
.kiko-timer-display {
|
|
text-align: center; width: 100%; height: 100%;
|
|
position: absolute; top: 0; left: 0;
|
|
background: transparent; border: none;
|
|
font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Monaco', monospace;
|
|
box-sizing: border-box; outline: none; margin: 0;
|
|
overflow: hidden; display: flex;
|
|
justify-content: center; align-items: center;
|
|
font-size: 42px;
|
|
animation: kiko-timer-pulse 4s infinite ease-in-out;
|
|
transition: color 0.3s ease-in-out;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.08em;
|
|
white-space: nowrap; font-weight: 600;
|
|
}
|
|
|
|
.kiko-timer-display.no-glow {
|
|
animation: none;
|
|
text-shadow: none;
|
|
}
|
|
|
|
.kiko-timer-display.running {
|
|
animation: kiko-timer-running-pulse 2s infinite ease-in-out;
|
|
}
|
|
|
|
.kiko-timer-display.running.no-glow {
|
|
animation: none;
|
|
text-shadow: none;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
api.addEventListener("execution_start", () => KikoGlobalTimer.start());
|
|
api.addEventListener("executing", ({ detail }) => {
|
|
if (detail === null) KikoGlobalTimer.stop();
|
|
});
|
|
api.addEventListener("execution_error", () => KikoGlobalTimer.stop());
|
|
api.addEventListener("execution_interrupted", () => KikoGlobalTimer.stop());
|
|
}
|
|
};
|
|
|
|
app.registerExtension(KikoWorkflowTimerExtension);
|