|
|
|
@@ -146,45 +146,70 @@ export function setupSidebarUI() {
|
|
|
|
|
titleGroup.appendChild(title);
|
|
|
|
|
standardHeader.appendChild(titleGroup);
|
|
|
|
|
|
|
|
|
|
const clearHistoryBtn = document.createElement("button");
|
|
|
|
|
clearHistoryBtn.textContent = "Clear History";
|
|
|
|
|
Object.assign(clearHistoryBtn.style, {
|
|
|
|
|
background: "transparent", color: "var(--desc-color, #aaa)", border: "1px solid var(--border-color, #555)",
|
|
|
|
|
borderRadius: "3px", padding: "3px 8px", cursor: "pointer", fontSize: "11px", fontWeight: "bold", transition: "all 0.15s ease-in-out"
|
|
|
|
|
});
|
|
|
|
|
// Dynamic Compact Actions Group
|
|
|
|
|
const actionsGroup = document.createElement("div");
|
|
|
|
|
Object.assign(actionsGroup.style, { display: "flex", gap: "6px", alignItems: "center" });
|
|
|
|
|
|
|
|
|
|
let clearHistoryTimeout = null, isClearHistoryPending = false;
|
|
|
|
|
const resetClearHistoryBtn = () => {
|
|
|
|
|
isClearHistoryPending = false;
|
|
|
|
|
clearHistoryBtn.textContent = "Clear History";
|
|
|
|
|
Object.assign(clearHistoryBtn.style, { color: "var(--desc-color, #aaa)", background: "transparent", borderColor: "var(--border-color, #555)", boxShadow: "none" });
|
|
|
|
|
if (clearHistoryTimeout) { clearTimeout(clearHistoryTimeout); clearHistoryTimeout = null; }
|
|
|
|
|
const createActionBtn = (iconClass, tooltip, hoverColor, onClickFn) => {
|
|
|
|
|
const btn = document.createElement("button");
|
|
|
|
|
btn.className = iconClass;
|
|
|
|
|
btn.title = tooltip;
|
|
|
|
|
Object.assign(btn.style, {
|
|
|
|
|
background: "transparent", color: "var(--desc-color, #aaa)", border: "1px solid var(--border-color, #555)",
|
|
|
|
|
borderRadius: "3px", padding: "4px 8px", cursor: "pointer", fontSize: "13px", transition: "all 0.15s ease-in-out"
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let timeout = null, isPending = false;
|
|
|
|
|
const reset = () => {
|
|
|
|
|
isPending = false;
|
|
|
|
|
Object.assign(btn.style, { color: "var(--desc-color, #aaa)", background: "transparent", borderColor: "var(--border-color, #555)", boxShadow: "none" });
|
|
|
|
|
if (timeout) { clearTimeout(timeout); timeout = null; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
btn.onmouseenter = () => { if (!isPending) { btn.style.borderColor = "var(--fg-color, #eee)"; btn.style.color = "var(--fg-color, #eee)"; } };
|
|
|
|
|
btn.onmouseleave = () => { if (!isPending) { btn.style.borderColor = "var(--border-color, #555)"; btn.style.color = "var(--desc-color, #aaa)"; } };
|
|
|
|
|
|
|
|
|
|
btn.onclick = async (ev) => {
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
if (!isPending) {
|
|
|
|
|
isPending = true;
|
|
|
|
|
Object.assign(btn.style, { color: "#fff", background: hoverColor, borderColor: hoverColor, boxShadow: `0 0 8px ${hoverColor}80` });
|
|
|
|
|
timeout = setTimeout(reset, 1500); // 1.5s to confirm
|
|
|
|
|
} else {
|
|
|
|
|
reset();
|
|
|
|
|
await onClickFn();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
return btn;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
clearHistoryBtn.addEventListener("mouseenter", () => {
|
|
|
|
|
if (!isClearHistoryPending) { clearHistoryBtn.style.borderColor = "var(--fg-color, #eee)"; clearHistoryBtn.style.color = "var(--fg-color, #eee)"; }
|
|
|
|
|
});
|
|
|
|
|
clearHistoryBtn.addEventListener("mouseleave", () => {
|
|
|
|
|
if (!isClearHistoryPending) { clearHistoryBtn.style.borderColor = "var(--border-color, #555)"; clearHistoryBtn.style.color = "var(--desc-color, #aaa)"; }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
clearHistoryBtn.onclick = async (ev) => {
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
if (!isClearHistoryPending) {
|
|
|
|
|
isClearHistoryPending = true;
|
|
|
|
|
clearHistoryBtn.textContent = "Confirm?";
|
|
|
|
|
Object.assign(clearHistoryBtn.style, { color: "#fff", background: "#dc3545", borderColor: "#dc3545", boxShadow: "0 0 8px rgba(220, 53, 69, 0.6)" });
|
|
|
|
|
clearHistoryTimeout = setTimeout(resetClearHistoryBtn, 1500);
|
|
|
|
|
} else {
|
|
|
|
|
resetClearHistoryBtn();
|
|
|
|
|
for (const [pid, state] of promptStates.entries()) {
|
|
|
|
|
if (state.status !== "pending" && state.status !== "active") { promptStates.delete(pid); cardElements.delete(pid); }
|
|
|
|
|
const btnClearInterrupted = createActionBtn("pi pi-eraser", "Clear Cancelled & Failed", "#ffc107", async () => {
|
|
|
|
|
const toDelete = [];
|
|
|
|
|
for (const [pid, state] of promptStates.entries()) {
|
|
|
|
|
if (state.status === "cancelled" || state.status === "error") {
|
|
|
|
|
toDelete.push(pid);
|
|
|
|
|
promptStates.delete(pid);
|
|
|
|
|
}
|
|
|
|
|
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clear: true }) }); } catch (err) {}
|
|
|
|
|
}
|
|
|
|
|
if (toDelete.length > 0) {
|
|
|
|
|
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: toDelete }) }); } catch (err) {}
|
|
|
|
|
renderDOM();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
standardHeader.appendChild(clearHistoryBtn);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const btnClearAll = createActionBtn("pi pi-trash", "Clear All History", "#dc3545", async () => {
|
|
|
|
|
for (const [pid, state] of promptStates.entries()) {
|
|
|
|
|
if (state.status !== "pending" && state.status !== "active") {
|
|
|
|
|
promptStates.delete(pid);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
try { await api.fetchApi("/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clear: true }) }); } catch (err) {}
|
|
|
|
|
renderDOM();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
actionsGroup.appendChild(btnClearInterrupted);
|
|
|
|
|
actionsGroup.appendChild(btnClearAll);
|
|
|
|
|
standardHeader.appendChild(actionsGroup);
|
|
|
|
|
|
|
|
|
|
const searchContainer = document.createElement("div");
|
|
|
|
|
Object.assign(searchContainer.style, { display: "none", width: "100%", alignItems: "center", background: "var(--comfy-input-bg, #181818)", border: "1px solid var(--border-color, #555)", borderRadius: "4px", padding: "2px 8px", boxSizing: "border-box", height: "26px" });
|
|
|
|
@@ -262,7 +287,11 @@ export function renderDOM() {
|
|
|
|
|
const sBadge = document.createElement("div");
|
|
|
|
|
Object.assign(sBadge.style, { position: "absolute", top: "6px", right: "8px", fontSize: "9px", fontWeight: "bold", padding: "2px 6px", borderRadius: "2px", textTransform: "uppercase", display: "none", pointerEvents: "none" });
|
|
|
|
|
const grid = document.createElement("div"); grid.style.display = "flex"; grid.style.flexDirection = "column"; grid.style.gap = "6px";
|
|
|
|
|
const p = document.createElement("div"); Object.assign(p.style, { fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px", userSelect: "none" });
|
|
|
|
|
const p = document.createElement("div");
|
|
|
|
|
Object.assign(p.style, {
|
|
|
|
|
fontSize: "11px", opacity: "0.5", textAlign: "center", padding: "12px", marginTop: "12px", userSelect: "none",
|
|
|
|
|
display: "-webkit-box", WebkitLineClamp: "15", WebkitBoxOrient: "vertical", overflow: "hidden", wordBreak: "break-word"
|
|
|
|
|
});
|
|
|
|
|
const pt = document.createElement("div"); Object.assign(pt.style, { width: "100%", height: "4px", background: "#333", borderRadius: "2px", marginTop: "8px", overflow: "hidden", display: "none" });
|
|
|
|
|
const pb = document.createElement("div"); Object.assign(pb.style, { width: `0%`, height: "100%", background: "#3b82f6", transition: "width 0.1s linear" });
|
|
|
|
|
pt.appendChild(pb);
|
|
|
|
@@ -346,13 +375,39 @@ export function renderDOM() {
|
|
|
|
|
cardObj.grid.innerHTML = ""; cardObj.firstImgElement = null; cardObj.placeholder.style.display = "block";
|
|
|
|
|
} else {
|
|
|
|
|
cardObj.placeholder.style.display = "none";
|
|
|
|
|
if (cardObj.grid.children.length === state.images.length) {
|
|
|
|
|
// Check if we can reuse elements (count matches AND tag types match)
|
|
|
|
|
let canReuse = cardObj.grid.children.length === state.images.length;
|
|
|
|
|
if (canReuse) {
|
|
|
|
|
state.images.forEach((img, idx) => {
|
|
|
|
|
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`;
|
|
|
|
|
const thumb = cardObj.grid.children[idx];
|
|
|
|
|
if (thumb.src !== src) {
|
|
|
|
|
if (src.startsWith("blob:")) { const tempImg = new Image(); tempImg.onload = () => { const oldBlob = thumb._lastBlob; thumb.src = src; thumb._lastBlob = src; if (oldBlob && oldBlob !== src) try { URL.revokeObjectURL(oldBlob); } catch(e){} }; tempImg.src = src; }
|
|
|
|
|
else thumb.src = src;
|
|
|
|
|
const wrapper = cardObj.grid.children[idx];
|
|
|
|
|
// Fallback in case the DOM still has old unwrapped elements
|
|
|
|
|
const mediaEl = (wrapper.tagName === "IMG" || wrapper.tagName === "VIDEO") ? wrapper : wrapper.querySelector("img, video");
|
|
|
|
|
if (!mediaEl || isVideoFormat(src) !== (mediaEl.tagName.toLowerCase() === "video")) canReuse = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (canReuse) {
|
|
|
|
|
state.images.forEach((img, idx) => {
|
|
|
|
|
let src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`;
|
|
|
|
|
const isVideo = isVideoFormat(src);
|
|
|
|
|
if (isVideo) src = src + "#t=0.001"; // Keep the first-frame trick on update
|
|
|
|
|
|
|
|
|
|
const wrapper = cardObj.grid.children[idx];
|
|
|
|
|
const mediaEl = (wrapper.tagName === "IMG" || wrapper.tagName === "VIDEO") ? wrapper : wrapper.querySelector("img, video");
|
|
|
|
|
|
|
|
|
|
if (mediaEl.getAttribute("src") !== src && mediaEl.src !== src) {
|
|
|
|
|
if (src.startsWith("blob:") && !isVideo) {
|
|
|
|
|
const tempImg = new Image();
|
|
|
|
|
tempImg.onload = () => {
|
|
|
|
|
const oldBlob = mediaEl._lastBlob;
|
|
|
|
|
mediaEl.src = src;
|
|
|
|
|
mediaEl._lastBlob = src;
|
|
|
|
|
if (oldBlob && oldBlob !== src) try { URL.revokeObjectURL(oldBlob); } catch(e){}
|
|
|
|
|
};
|
|
|
|
|
tempImg.src = src;
|
|
|
|
|
}
|
|
|
|
|
else mediaEl.src = src;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
@@ -360,9 +415,42 @@ export function renderDOM() {
|
|
|
|
|
state.images.forEach(img => {
|
|
|
|
|
const src = img.url ? img.url : `/view?filename=${encodeURIComponent(img.filename)}&type=${img.type}&subfolder=${encodeURIComponent(img.subfolder)}`;
|
|
|
|
|
const isVideo = isVideoFormat(src);
|
|
|
|
|
|
|
|
|
|
// Container to hold both the media and the play icon
|
|
|
|
|
const wrapper = document.createElement("div");
|
|
|
|
|
Object.assign(wrapper.style, { position: "relative", width: "100%", display: "block" });
|
|
|
|
|
|
|
|
|
|
const thumb = isVideo ? document.createElement("video") : document.createElement("img");
|
|
|
|
|
Object.assign(thumb.style, { width: "100%", borderRadius: "2px", display: "block", cursor: "zoom-in" });
|
|
|
|
|
if (isVideo) { thumb.autoplay = true; thumb.loop = true; thumb.muted = true; thumb.playsInline = true; }
|
|
|
|
|
|
|
|
|
|
if (isVideo) {
|
|
|
|
|
thumb.muted = true;
|
|
|
|
|
thumb.playsInline = true;
|
|
|
|
|
thumb.preload = "metadata";
|
|
|
|
|
thumb.loop = true;
|
|
|
|
|
|
|
|
|
|
// Create the Play Icon overlay with perfectly centered scalable SVG
|
|
|
|
|
const playIcon = document.createElement("div");
|
|
|
|
|
Object.assign(playIcon.style, {
|
|
|
|
|
position: "absolute", top: "0", left: "0", width: "100%", height: "100%",
|
|
|
|
|
pointerEvents: "none", zIndex: "5", transition: "opacity 0.2s ease"
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The radius 16.65 ensures the circle is exactly 1/3 of the shorter edge.
|
|
|
|
|
// The polygon coordinates are an optically centered equilateral triangle.
|
|
|
|
|
playIcon.innerHTML = `
|
|
|
|
|
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.3));">
|
|
|
|
|
<circle cx="50" cy="50" r="16.65" fill="rgba(0, 0, 0, 0.55)" />
|
|
|
|
|
<polygon points="45.7,42.5 45.7,57.5 58.7,50" fill="rgba(255, 255, 255, 0.9)" />
|
|
|
|
|
</svg>
|
|
|
|
|
`;
|
|
|
|
|
wrapper.appendChild(playIcon);
|
|
|
|
|
|
|
|
|
|
// Play on Hover, Pause on Leave, and fade out the play button
|
|
|
|
|
thumb.onmouseenter = () => { playIcon.style.opacity = "0"; thumb.play().catch(()=>{}); };
|
|
|
|
|
thumb.onmouseleave = () => { playIcon.style.opacity = "1"; thumb.pause(); };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!keepAspect) { thumb.style.aspectRatio = "1 / 1"; thumb.style.objectFit = "cover"; }
|
|
|
|
|
thumb.setAttribute("draggable", "false");
|
|
|
|
|
|
|
|
|
@@ -370,11 +458,14 @@ export function renderDOM() {
|
|
|
|
|
thumb.onload = () => { if (keepAspect && thumb.naturalWidth && thumb.naturalHeight) thumb.style.aspectRatio = `${thumb.naturalWidth} / ${thumb.naturalHeight}`; };
|
|
|
|
|
if (src.startsWith("blob:")) { const tempImg = new Image(); tempImg.onload = () => { thumb.src = src; thumb._lastBlob = src; }; tempImg.src = src; } else thumb.src = src;
|
|
|
|
|
} else {
|
|
|
|
|
thumb.onloadedmetadata = () => { if (keepAspect && thumb.videoWidth && thumb.videoHeight) thumb.style.aspectRatio = `${thumb.videoWidth} / ${thumb.videoHeight}`; }; thumb.src = src;
|
|
|
|
|
thumb.onloadedmetadata = () => { if (keepAspect && thumb.videoWidth && thumb.videoHeight) thumb.style.aspectRatio = `${thumb.videoWidth} / ${thumb.videoHeight}`; };
|
|
|
|
|
thumb.src = src + "#t=0.001";
|
|
|
|
|
}
|
|
|
|
|
thumb.onclick = (ev) => { ev.stopPropagation(); showFullscreenPreview(state.images.map(i => i.url ? i.url : `/view?filename=${encodeURIComponent(i.filename)}&type=${i.type}&subfolder=${encodeURIComponent(i.subfolder)}`)); };
|
|
|
|
|
|
|
|
|
|
wrapper.insertBefore(thumb, wrapper.firstChild);
|
|
|
|
|
if (!cardObj.firstImgElement) cardObj.firstImgElement = thumb;
|
|
|
|
|
cardObj.grid.appendChild(thumb);
|
|
|
|
|
cardObj.grid.appendChild(wrapper);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -383,9 +474,9 @@ export function renderDOM() {
|
|
|
|
|
|
|
|
|
|
if (state.images.length === 0) {
|
|
|
|
|
if (state.texts && state.texts.length > 0) {
|
|
|
|
|
cardObj.placeholder.textContent = state.texts.join("\n"); Object.assign(cardObj.placeholder.style, { whiteSpace: "pre-wrap", textAlign: "left", fontSize: "10px", opacity: "0.8" });
|
|
|
|
|
cardObj.placeholder.textContent = state.texts.join("\n"); Object.assign(cardObj.placeholder.style, { whiteSpace: "pre-wrap", textAlign: "left" });
|
|
|
|
|
} else {
|
|
|
|
|
cardObj.placeholder.textContent = state.progressText || "No Outputs"; Object.assign(cardObj.placeholder.style, { whiteSpace: "normal", textAlign: "center", fontSize: "11px", opacity: "0.5" });
|
|
|
|
|
cardObj.placeholder.textContent = state.progressText || "No Outputs"; Object.assign(cardObj.placeholder.style, { whiteSpace: "normal", textAlign: "center" });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|