import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
app.registerExtension({
name: "Comfy.WebBrowser",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "WebViewerNode") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const widgetDiv = document.createElement("div");
widgetDiv.style.display = "flex";
widgetDiv.style.flexDirection = "column";
widgetDiv.style.width = "100%";
widgetDiv.dataset.captureWheel = "true";
widgetDiv.addEventListener("wheel", (e) => {
e.stopPropagation();
});
const imageUrlWidget = this.addWidget("hidden_text", "image_url", "", () => {}, { multiline: true });
imageUrlWidget.draw = function() {};
imageUrlWidget.computeSize = function() { return [0, 0]; };
this.addDOMWidget("browser", "div", widgetDiv);
widgetDiv.innerHTML = `
✓
✓
`;
const urlInput = widgetDiv.querySelector(".browser-url-input");
const goButton = widgetDiv.querySelector(".browser-go");
const bookmarkContainer = widgetDiv.querySelector(".browser-bookmark-container");
const bookmarkToggleBtn = widgetDiv.querySelector(".browser-bookmark-toggle");
const bookmarkMenu = widgetDiv.querySelector(".browser-bookmark-menu");
const iframe = widgetDiv.querySelector("iframe");
const imageUrlInput = widgetDiv.querySelector(".browser-image-url");
const loadImageBtn = widgetDiv.querySelector(".browser-load-image-btn");
const urlStatusIcon = widgetDiv.querySelector(".url-status");
const photopeaImportBtn = widgetDiv.querySelector(".browser-photopea-import-btn");
const photopeaStatusIcon = widgetDiv.querySelector(".photopea-status");
let bookmarkData = [];
let draggedItemInfo = null;
const homeButton = widgetDiv.querySelector(".browser-home-btn");
let homeUrl = localStorage.getItem("customHomepage") || "https://www.photopea.com";
homeButton.addEventListener("click", () => {
navigate(homeUrl);
});
homeButton.addEventListener("contextmenu", (e) => {
e.preventDefault();
const newHomeUrl = prompt("Enter your new homepage URL:", homeUrl);
if (newHomeUrl && newHomeUrl.trim()) {
homeUrl = newHomeUrl.trim();
localStorage.setItem("customHomepage", homeUrl);
alert("Homepage updated!");
}
});
const navigate = (url) => {
if (url && !url.startsWith("http")) url = "https://" + url;
if (url) { urlInput.value = url; iframe.src = url; }
};
navigate(homeUrl);
loadImageBtn.addEventListener("click", () => {
const imageUrl = imageUrlInput.value.trim();
if(imageUrl){
imageUrlWidget.value = imageUrl;
urlStatusIcon.style.display = "inline";
setTimeout(() => { urlStatusIcon.style.display = "none"; }, 2000);
}
});
imageUrlInput.addEventListener("paste", async (e) => {
if (!e.clipboardData || !e.clipboardData.files.length) {
return;
}
const file = e.clipboardData.files[0];
if (!file.type.startsWith("image/")) {
return;
}
e.preventDefault();
try {
imageUrlInput.value = "Pasting image...";
imageUrlInput.disabled = true;
const formData = new FormData();
const filename = `clipboard_paste_${Date.now()}.png`;
formData.append('image', file, filename);
const response = await api.fetchApi('/browser/upload_temp_image', { method: 'POST', body: formData });
const result = await response.json();
if (result.name) {
imageUrlWidget.value = result.name;
imageUrlInput.value = `Pasted: ${result.name}`;
imageUrlInput.disabled = false;
urlStatusIcon.style.display = "inline";
setTimeout(() => { urlStatusIcon.style.display = "none"; }, 2000);
} else {
throw new Error("Upload failed, no filename returned.");
}
} catch (err) {
console.error("Error pasting image:", err);
alert("Failed to paste image. See console for details.");
imageUrlInput.value = "";
imageUrlInput.disabled = false;
}
});
imageUrlInput.addEventListener("input", () => {
if(imageUrlInput.value.trim() === ""){
imageUrlWidget.value = "";
}
});
photopeaImportBtn.addEventListener("click", () => {
photopeaImportBtn.textContent = "Getting...";
photopeaImportBtn.disabled = true;
iframe.contentWindow.postMessage("app.activeDocument.saveToOE('png');", "*");
});
const handlePhotopeaMessage = async (event) => {
if (event.data instanceof ArrayBuffer) {
try {
const blob = new Blob([event.data], { type: "image/png" });
const formData = new FormData();
const filename = `photopea_export_${Date.now()}.png`;
formData.append('image', blob, filename);
const response = await api.fetchApi('/browser/upload_temp_image', { method: 'POST', body: formData });
const result = await response.json();
if (result.name) {
imageUrlWidget.value = result.name;
photopeaStatusIcon.style.display = "inline";
setTimeout(() => { photopeaStatusIcon.style.display = "none"; }, 2000);
} else {
throw new Error("Upload failed, no filename returned.");
}
} catch (e) {
console.error("Error processing image from Photopea:", e);
alert("Image import from photopea failed, please check the console for more information.");
} finally {
photopeaImportBtn.textContent = "From Photopea";
photopeaImportBtn.disabled = false;
}
}
};
window.addEventListener("message", handlePhotopeaMessage);
const onRemoved = this.onRemoved;
this.onRemoved = () => {
window.removeEventListener("message", handlePhotopeaMessage);
return onRemoved ? onRemoved.apply(this, arguments) : undefined;
};
const renderBookmarks = () => {
bookmarkMenu.innerHTML = "";
const addItem = document.createElement("a");
addItem.href = "#";
addItem.className = "browser-bookmark-item add-bookmark-btn";
addItem.innerHTML = `+Add the current URL`;
addItem.onclick = (e) => {
e.preventDefault();
const url = urlInput.value.trim();
if (!url) { alert("The address bar is empty."); return; }
const name = prompt("Enter a name for this bookmark:", url);
if (name && name.trim()) {
if (bookmarkData.length === 0) {
bookmarkData.push({ categoryName: "Uncategorized", bookmarks: [] });
}
bookmarkData[0].bookmarks.push({ name: name.trim(), url: url });
syncBookmarks();
}
bookmarkMenu.style.display = "none";
};
bookmarkMenu.appendChild(addItem);
bookmarkData.forEach((category, categoryIndex) => {
const categoryDetails = document.createElement("details");
categoryDetails.open = true;
categoryDetails.dataset.categoryIndex = categoryIndex;
const summary = document.createElement("summary");
summary.className = "bm-category-summary";
summary.innerHTML = `${category.categoryName}`;
summary.querySelector(".bm-add-here").addEventListener("click", (e) => {
e.preventDefault();
const url = urlInput.value.trim();
if (!url) { alert("The address bar is empty."); return; }
const name = prompt("Enter a name for the bookmark:", url);
if (name && name.trim()) {
bookmarkData[categoryIndex].bookmarks.push({ name: name.trim(), url: url });
syncBookmarks();
}
});
summary.querySelector(".bm-cat-edit").addEventListener("click", (e) => { e.preventDefault(); const newCatName = prompt("Enter a new category name:", category.categoryName); if (newCatName && newCatName.trim()) { bookmarkData[categoryIndex].categoryName = newCatName.trim(); syncBookmarks(); } });
summary.querySelector(".bm-cat-delete").addEventListener("click", (e) => { e.preventDefault(); if (confirm(`Confirm to delete the category "${category.categoryName}" And all of its bookmarks?`)) { bookmarkData.splice(categoryIndex, 1); syncBookmarks(); } });
const bookmarkList = document.createElement("div");
bookmarkList.className = "bm-category-bookmarks";
bookmarkList.dataset.categoryIndex = categoryIndex;
(category.bookmarks || []).forEach((bookmark, bookmarkIndex) => {
const item = document.createElement("a");
item.href = "#"; item.className = "browser-bookmark-item"; item.draggable = true;
item.innerHTML = `🌐${bookmark.name}`;
item.addEventListener("click", (e) => { e.preventDefault(); navigate(bookmark.url); bookmarkMenu.style.display = "none"; });
item.querySelector(".bm-edit").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const newName = prompt("Enter a new bookmark name:", bookmark.name); if (newName && newName.trim()) { bookmark.name = newName.trim(); syncBookmarks(); } });
item.querySelector(".bm-delete").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); if (confirm(`Confirm to delete the bookmark "${bookmark.name}"?`)) { category.bookmarks.splice(bookmarkIndex, 1); syncBookmarks(); } });
item.addEventListener("dragstart", (e) => { e.stopPropagation(); draggedItemInfo = { categoryIndex, bookmarkIndex }; setTimeout(() => item.classList.add("dragging"), 0); });
item.addEventListener("dragend", (e) => { e.stopPropagation(); item.classList.remove("dragging"); });
item.addEventListener("dragover", (e) => { e.preventDefault(); e.stopPropagation(); });
item.addEventListener("drop", (e) => {
e.preventDefault(); e.stopPropagation();
if (draggedItemInfo) {
const { categoryIndex: fromCatIndex, bookmarkIndex: fromBmIndex } = draggedItemInfo;
const toCatIndex = categoryIndex, toBmIndex = bookmarkIndex;
if(fromCatIndex === toCatIndex && fromBmIndex === toBmIndex) return;
const movedItem = bookmarkData[fromCatIndex].bookmarks.splice(fromBmIndex, 1)[0];
bookmarkData[toCatIndex].bookmarks.splice(toBmIndex, 0, movedItem);
syncBookmarks();
}
draggedItemInfo = null;
});
bookmarkList.appendChild(item);
});
const dropHandler = (e) => {
e.preventDefault(); e.stopPropagation();
if (draggedItemInfo && draggedItemInfo.categoryIndex !== categoryIndex) {
const { categoryIndex: fromCatIndex, bookmarkIndex: fromBmIndex } = draggedItemInfo;
const movedItem = bookmarkData[fromCatIndex].bookmarks.splice(fromBmIndex, 1)[0];
bookmarkData[categoryIndex].bookmarks.push(movedItem);
syncBookmarks();
}
draggedItemInfo = null;
};
summary.addEventListener("dragover", (e) => { e.preventDefault(); e.stopPropagation(); categoryDetails.open = true; });
summary.addEventListener("drop", dropHandler);
bookmarkList.addEventListener("drop", dropHandler);
categoryDetails.appendChild(summary);
categoryDetails.appendChild(bookmarkList);
bookmarkMenu.appendChild(categoryDetails);
});
const footer = document.createElement("div"); footer.className = "bm-menu-footer";
const addCategoryBtn = document.createElement("button"); addCategoryBtn.textContent = "+ new category";
addCategoryBtn.onclick = () => { const newCatName = prompt("Enter the name of the new category:"); if (newCatName && newCatName.trim()) { bookmarkData.push({ categoryName: newCatName.trim(), bookmarks: [] }); syncBookmarks(); } };
footer.appendChild(addCategoryBtn);
bookmarkMenu.appendChild(footer);
};
const syncBookmarks = async () => { try { await api.fetchApi("/browser/save_bookmarks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(bookmarkData) }); renderBookmarks(); } catch (e) { console.error("Failed to save bookmarks:", e); } };
bookmarkToggleBtn.addEventListener("click", () => { bookmarkMenu.style.display = bookmarkMenu.style.display === "block" ? "none" : "block"; });
document.addEventListener("click", (e) => { if (!bookmarkContainer.contains(e.target)) bookmarkMenu.style.display = "none"; });
goButton.addEventListener("click", () => navigate(urlInput.value.trim()));
urlInput.addEventListener("keydown", (e) => { if (e.key === "Enter") navigate(urlInput.value.trim()); });
this.onResize = function(size) {
const minHeight = 470;
const minWidth = 700;
if (size[1] < minHeight) size[1] = minHeight;
if (size[0] < minWidth) size[0] = minWidth;
let topOffset = widgetDiv.offsetTop;
const approximateHeaderHeight = 50;
if (topOffset < 20) {
topOffset += approximateHeaderHeight;
}
const bottomPadding = 20;
const targetHeight = size[1] - topOffset - bottomPadding;
widgetDiv.style.height = targetHeight + "px";
};
this.size = [900, 770];
(async () => {
try {
const res = await api.fetchApi("/browser/get_bookmarks");
bookmarkData = await res.json();
renderBookmarks();
} catch (e) { console.error("Failed to load bookmarks:", e); }
setTimeout(() => { this.onResize(this.size); }, 1);
})();
requestAnimationFrame(() => {
if (this.onResize) {
this.onResize(this.size);
}
});
return r;
};
}
},
});