From eb6d15e217df429dc7e4040be7813aeab9448f3e Mon Sep 17 00:00:00 2001 From: Dan Date: Tue, 3 Sep 2024 07:04:22 -0500 Subject: [PATCH] Initial Code --- __init__.py | 10 + promptImages/SKIP.jpeg | Bin 0 -> 50743 bytes promptImages/custom/custom_img_storage.txt | 1 + .../extract_navimixu_archives_here.txt | 3 + web/prompt_gallery.js | 1030 +++++++++++++++++ 5 files changed, 1044 insertions(+) create mode 100644 __init__.py create mode 100644 promptImages/SKIP.jpeg create mode 100644 promptImages/custom/custom_img_storage.txt create mode 100644 promptImages/thumbnails/extract_navimixu_archives_here.txt create mode 100644 web/prompt_gallery.js diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..cb9348b --- /dev/null +++ b/__init__.py @@ -0,0 +1,10 @@ +NODE_CLASS_MAPPINGS = { + +} + +NODE_DISPLAY_NAME_MAPPINGS = { + +} + +WEB_DIRECTORY = "./web" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/promptImages/SKIP.jpeg b/promptImages/SKIP.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..07811d0116867a7cb7f22e11b3a1abbe482ef0c5 GIT binary patch literal 50743 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 HbTsjQK$WjE literal 0 HcmV?d00001 diff --git a/promptImages/custom/custom_img_storage.txt b/promptImages/custom/custom_img_storage.txt new file mode 100644 index 0000000..5461e4e --- /dev/null +++ b/promptImages/custom/custom_img_storage.txt @@ -0,0 +1 @@ +DO NOT place images in this folder manually, they will NOT be picked up by the plugin. You must Add them via the UI. \ No newline at end of file diff --git a/promptImages/thumbnails/extract_navimixu_archives_here.txt b/promptImages/thumbnails/extract_navimixu_archives_here.txt new file mode 100644 index 0000000..398dbae --- /dev/null +++ b/promptImages/thumbnails/extract_navimixu_archives_here.txt @@ -0,0 +1,3 @@ +You can download any of the following wildcard archives from: +https://civitai.com/models/615967?modelVersionId=789576 +and extract them into this directory, if you do it right this folder will have "ponyxl" as the only subfolder here. diff --git a/web/prompt_gallery.js b/web/prompt_gallery.js new file mode 100644 index 0000000..aa278f6 --- /dev/null +++ b/web/prompt_gallery.js @@ -0,0 +1,1030 @@ +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; +import { $el } from "../../../scripts/ui.js"; +import * as pngMetadata from "../../../scripts/metadata/png.js"; + + +class PromptGallery { + constructor(app) { + this.app = app; + this.allImages = []; + this.filteredImages = []; + this.sortAscending = true; + this.searchInput = this.createSearchInput(); + this.sortToggle = this.createSortToggle(); + this.accordion = $el("div.prompt-accordion"); + this.element = $el("div.prompt-gallery-popup", [ + $el("h3", "Prompt Image Gallery"), + $el("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "10px" } }, [ + this.searchInput, + this.sortToggle + ]), + this.accordion + ]); + this.yamlFiles = [ + { name: "PonyXl-artstyles.yaml", type: "Art Styles", skipLevels: 0, sections: null, order: 1 }, + { name: "PonyXl-game_persona.yaml", type: "Game Characters", skipLevels: 0, sections: null, order: 2 }, + { name: "PonyXl-show_persona.yaml", type: "Show Characters", skipLevels: 0, sections: null, order: 3 }, + { name: "PonyXl-f-body.yaml", type: "Female Body", skipLevels: 0, sections: { body_race: "Race", body_form: "Build" }, order: 4 }, + { name: "PonyXl-poses.yaml", type: "Poses", skipLevels: 0, sections: null, order: 5 }, + { name: "PonyXl-expressions.yaml", type: "Expressions", skipLevels: 0, sections: null, order: 6, ignoreKey: "chara_expression" }, + { name: "PonyXl-scenes.yaml", type: "Scenes", skipLevels: 0, sections: null, order: 7 } + // Add custom yaml files here if you want and know how to do it :D + ]; + + this.baseUrl = `${window.location.protocol}//${window.location.host}`; + this.sectionStates = this.loadSectionStates(); + this.placeholderImageUrl = `${this.baseUrl}/view?filename=SKIP.jpeg&type=input&subfolder=promptImages/thumbnails`; + this.customImages = this.loadCustomImages(); + this.isSearchActive = false; + + this.resetButton = this.createResetCustomImagesButton(); + this.element = $el("div.prompt-gallery-popup", [ + $el("h3", "Prompt Image Gallery"), + this.resetButton, // Add the reset button here + $el("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "10px" } }, [ + this.searchInput, + this.sortToggle + ]), + this.accordion + ]); + + } + + createResetCustomImagesButton() { + const button = $el("button", { + textContent: "Reset Custom Images", + onclick: () => this.resetCustomImages(), + style: { + marginBottom: "10px", + padding: "5px 10px", + backgroundColor: "#f44336", + color: "white", + border: "none", + borderRadius: "4px", + cursor: "pointer", + width: "100%", // Make the button full width + } + }); + return button; + } + + resetCustomImages() { + if (confirm("Are you sure you want to reset all custom images? This action cannot be undone! (This does not delete the images in the directory)")) { + localStorage.removeItem('customGalleryImages'); + this.customImages = []; + this.update(); + console.log("Custom images have been reset."); + app.extensionManager.toast.add({ + severity: "info", + summary: "Custom Images Reset", + detail: "All custom images have been cleared.", + life: 3000 + }); + } + } + + loadCustomImages() { + const savedImages = localStorage.getItem('customGalleryImages'); + return savedImages ? JSON.parse(savedImages) : []; + } + + saveCustomImages() { + localStorage.setItem('customGalleryImages', JSON.stringify(this.customImages)); + } + + loadSectionStates() { + const savedStates = localStorage.getItem('wildcardGallerySectionStates'); + if (savedStates) { + return JSON.parse(savedStates); + } else { + // If no saved states, return an empty object + // Sections will default to open + return {}; + } + } + + saveSectionStates() { + localStorage.setItem('wildcardGallerySectionStates', JSON.stringify(this.sectionStates)); + } + + createAddCustomImageButton() { + const button = $el("div", { + style: { + width: "100px", + height: "100px", + display: "flex", + flexDirection: "column", + justifyContent: "center", + alignItems: "center", + cursor: "pointer", + border: "2px dashed #ccc", + borderRadius: "5px", + backgroundColor: "#2a2a2a" + }, + onclick: () => this.showAddCustomImageDialog() + }); + + const plusSign = $el("div", { + textContent: "+", + style: { + fontSize: "40px", + color: "#ccc" + } + }); + + const addText = $el("div", { + textContent: "Add", + style: { + marginTop: "5px", + fontSize: "12px", + color: "#ccc" + } + }); + + button.appendChild(plusSign); + button.appendChild(addText); + + return button; + } + + setupDragAndDrop() { + console.log("Setting up drag and drop"); + + const customSection = this.accordion.querySelector('.custom-section'); + console.log("Custom section found:", customSection); + if (!customSection) { + console.error("Custom section not found in the DOM"); + return; + } + + const dropZone = customSection.querySelector('.accordion-content'); + console.log("Drop zone found:", dropZone); + if (!dropZone) { + console.error("Drop zone not found in the custom section"); + return; + } + + dropZone.style.transition = 'all 0.3s ease'; + + const addHighlight = () => { + dropZone.style.border = '2px dashed #4CAF50'; + dropZone.style.backgroundColor = 'rgba(76, 175, 80, 0.1)'; + }; + + const removeHighlight = () => { + dropZone.style.border = ''; + dropZone.style.backgroundColor = ''; + }; + + ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { + dropZone.addEventListener(eventName, (e) => { + e.preventDefault(); + e.stopPropagation(); + }, false); + }); + + ['dragenter', 'dragover'].forEach(eventName => { + dropZone.addEventListener(eventName, () => { + console.log("Drag enter/over"); + addHighlight(); + }, false); + }); + + ['dragleave', 'drop'].forEach(eventName => { + dropZone.addEventListener(eventName, () => { + console.log("Drag leave/drop"); + removeHighlight(); + }, false); + }); + + dropZone.addEventListener('drop', (e) => { + console.log("File dropped"); + let dt = e.dataTransfer; + let files = dt.files; + this.handleFiles(files); + }, false); + } + + handleFiles(files) { + console.log("Handling files:", files); + //[...files].forEach(file => this.uploadAndProcessFile(file)); + [...new Set(files)].forEach(file => this.uploadAndProcessFile(file)); + } + + showAddCustomImageDialog() { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.style.display = 'none'; + document.body.appendChild(fileInput); + + fileInput.addEventListener('change', async (event) => { + const file = event.target.files[0]; + if (file) { + await this.uploadAndProcessFile(file); + } + document.body.removeChild(fileInput); + }); + + fileInput.click(); + } + + async uploadAndProcessFile(file) { + try { + // Prepare the form data with the correct path + const formData = new FormData(); + formData.append('image', file); + formData.append('subfolder', 'promptImages/thumbnails/custom'); // Specify the subfolder + + // Upload file directly to the correct directory + const uploadResponse = await api.fetchApi('/upload/image', { + method: 'POST', + body: formData + }); + + if (!uploadResponse.ok) { + throw new Error("Failed to upload file"); + } + + const uploadResult = await uploadResponse.json(); + const imagePath = uploadResult.name; // This should now be the full path including the subfolder + + // Check if the image already exists in customImages + const existingImageIndex = this.customImages.findIndex(img => img.name === imagePath.split('/').pop()); + + if (existingImageIndex === -1) { + // Add to custom images only if it doesn't already exist + await this.addCustomImage(imagePath, ""); + } else { + console.log(`Image ${imagePath} already exists in custom images. Skipping addition.`); + } + + // Attempt to fetch metadata (this might not be available for custom images) + try { + const metadata = await this.fetchImageMetadata(imagePath); + const tags = this.extractTagsFromMetadata(metadata); + // Update the custom image with tags if available + this.updateCustomImageTags(imagePath, tags); + } catch (metadataError) { + console.warn("Metadata not available for custom image:", metadataError); + } + + api.toast.add({ + severity: "success", + summary: "Upload Successful", + detail: `Added custom image: ${imagePath.split('/').pop()}`, + life: 3000 + }); + } catch (error) { + console.error("Error handling file upload:", error); + api.toast.add({ + severity: "error", + summary: "Upload Failed", + detail: "Failed to add custom image. Please try again.", + life: 3000 + }); + } + } + + async fetchImageMetadata(imagePath) { + const fullImagePath = imagePath.startsWith('http') ? imagePath : `${this.baseUrl}${imagePath}`; + + const response = await fetch(fullImagePath); + if (!response.ok) { + throw new Error("Failed to fetch image"); + } + const arrayBuffer = await response.arrayBuffer(); + return await pngMetadata.getFromPngBuffer(new Uint8Array(arrayBuffer)); + } + + extractPromptFromMetadata(metadata) { + if (!metadata || !metadata.prompt) { + return ""; + } + + const promptData = JSON.parse(metadata.prompt); + + // Look for clip_l first + for (const key in promptData) { + if (promptData[key].inputs && promptData[key].inputs.clip_l) { + return promptData[key].inputs.clip_l; + } + } + + // Then look for text_g or text_l in any top-level object + for (const key in promptData) { + if (promptData[key].inputs && (promptData[key].inputs.text_g || promptData[key].inputs.text_l)) { + return promptData[key].inputs.text_g || promptData[key].inputs.text_l; + } + } + + // Look for 'positive' reference and follow it + let positiveRef = null; + for (const key in promptData) { + if (promptData[key].inputs && promptData[key].inputs.positive) { + positiveRef = promptData[key].inputs.positive[0]; + break; + } + } + + if (positiveRef && promptData[positiveRef]) { + const positiveNode = promptData[positiveRef]; + if (positiveNode.inputs && positiveNode.inputs.text) { + return positiveNode.inputs.text; + } + } + + // If all else fails, look for any 'text' input in a CLIPTextEncode node + for (const key in promptData) { + if (promptData[key].class_type === "CLIPTextEncode" && promptData[key].inputs && promptData[key].inputs.text) { + return promptData[key].inputs.text; + } + } + + return ""; // Return empty string if no prompt found + } + + async addCustomImage(imagePath, tags) { + const newImage = { + name: imagePath.split('/').pop(), + path: `/view?filename=${imagePath}&type=input&subfolder=promptImages/thumbnails/custom`, + tags: tags, + type: "Custom" + }; + + // Check if the image already exists + const existingImageIndex = this.customImages.findIndex(img => img.name === newImage.name); + + let imageAdded = false; + if (existingImageIndex === -1) { + this.customImages.push(newImage); + imageAdded = true; + } else { + console.log(`Image ${newImage.name} already exists in custom images. Updating metadata.`); + newImage = this.customImages[existingImageIndex]; // Use the existing image object + } + + // Attempt to extract metadata + try { + const metadata = await this.fetchImageMetadata(newImage.path); + const extractedTags = this.extractPromptFromMetadata(metadata); + if (extractedTags) { + newImage.tags = extractedTags; + app.extensionManager.toast.add({ + severity: "success", + summary: "Metadata Extracted", + detail: "Prompt tags were successfully extracted from the image.", + life: 3000 + }); + } else { + app.extensionManager.toast.add({ + severity: "info", + summary: "No Metadata Found", + detail: "No prompt tags were found in the image metadata.", + life: 3000 + }); + } + } catch (error) { + console.error("Error extracting metadata:", error); + app.extensionManager.toast.add({ + severity: "error", + summary: "Metadata Extraction Failed", + detail: "An error occurred while trying to extract metadata.", + life: 3000 + }); + } + + this.saveCustomImages(); + this.update(); + return imageAdded; + } + + updateCustomImageTags(imagePath, tags) { + const image = this.customImages.find(img => img.path.includes(imagePath)); + if (image) { + image.tags = tags; + this.saveCustomImages(); + } + } + + createSearchInput() { + const input = $el("input", { + type: "text", + placeholder: "Search prompt images...", + style: { + width: "70%", + padding: "8px", + borderRadius: "4px", + border: "1px solid #ccc" + } + }); + input.addEventListener("input", this.debounce(() => this.handleSearch(input.value), 300)); + return input; + } + + createSortToggle() { + const button = $el("button", { + textContent: "Sort: A-Z", + onclick: () => this.toggleSort(), + style: { + padding: "8px 12px", + borderRadius: "4px", + border: "none", + background: "#2a2a2a", + color: "white", + cursor: "pointer", + fontSize: "14px", + transition: "background-color 0.3s" + } + }); + + button.addEventListener("mouseenter", () => { + button.style.backgroundColor = "#3a3a3a"; + }); + button.addEventListener("mouseleave", () => { + button.style.backgroundColor = "#2a2a2a"; + }); + + return button; + } + + toggleSort() { + this.sortAscending = !this.sortAscending; + this.sortToggle.textContent = this.sortAscending ? "Sort: A-Z" : "Sort: Z-A"; + this.sortAndDisplayImages(); + } + + sortAndDisplayImages() { + this.accordion.innerHTML = ""; + + const imagesToDisplay = this.isSearchActive ? this.filteredImages : this.allImages; + + if (imagesToDisplay.length === 0) { + const noImagesFoundMessage = $el("div", { + style: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + height: "200px", + width: "100%", + backgroundColor: "#1a1a1a", // Dark background + borderRadius: "8px", + border: "1px solid #333" // Subtle border + } + }); + + const sadEmoji = $el("div", { + textContent: "😔", + style: { + fontSize: "64px", + color: "#666666", // Darker gray for the emoji + marginBottom: "20px" + } + }); + + const messageText = $el("div", { + textContent: this.isSearchActive ? "No matching images found" : "No images available", + style: { + fontSize: "18px", + color: "#aaaaaa" // Light gray for better readability on dark background + } + }); + + noImagesFoundMessage.appendChild(sadEmoji); + noImagesFoundMessage.appendChild(messageText); + this.accordion.appendChild(noImagesFoundMessage); + return; + } + + const groupedImages = {}; + + for (const image of imagesToDisplay) { + const category = image.section || image.type; + if (!groupedImages[category]) { + groupedImages[category] = []; + } + if (!groupedImages[category].some(img => img.name === image.name && img.path === image.path)) { + groupedImages[category].push(image); + } + } + + // Handle custom images separately + groupedImages["Custom"] = this.customImages; + + // Ensure Custom category always exists + if (!groupedImages["Custom"]) { + groupedImages["Custom"] = []; + } + + const categoryOrder = new Map(this.yamlFiles.flatMap(file => { + const orders = [[file.type, file.order]]; + if (file.sections) { + Object.values(file.sections).forEach((sectionName, index) => { + orders.push([sectionName, file.order + index / 100]); + }); + } + return orders; + })); + + const categories = Object.keys(groupedImages).sort((a, b) => { + if (a === "Custom") return 1; + if (b === "Custom") return -1; + const orderA = categoryOrder.get(a) || Infinity; + const orderB = categoryOrder.get(b) || Infinity; + return orderA - orderB; + }); + + for (const category of categories) { + const images = groupedImages[category]; + const sortedImages = [...images].sort((a, b) => { + return this.sortAscending + ? a.name.localeCompare(b.name) + : b.name.localeCompare(a.name); + }); + + // Always create section for Custom, even if empty + if (sortedImages.length > 0 || category === "Custom") { + const accordionSection = this.createAccordionSection(category, sortedImages); + this.accordion.appendChild(accordionSection); + } + } + + this.setupDragAndDrop(); + } + + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } + + handleSearch(searchTerm) { + searchTerm = searchTerm.toLowerCase(); + this.isSearchActive = searchTerm.length > 0; + this.filteredImages = this.allImages.filter(image => + image && image.name && image.name.toLowerCase().includes(searchTerm) + ); + this.sortAndDisplayImages(); + } + + flattenAllImages() { + return this.allImages.flatMap(category => { + if (Array.isArray(category.images)) { + return category.images; + } else if (typeof category === 'object') { + return Object.entries(category) + .filter(([key, value]) => key !== 'type' && Array.isArray(value)) + .flatMap(([subType, images]) => images.map(img => ({...img, type: subType}))); + } + return []; + }); + } + + groupImagesByType(images) { + return images.reduce((acc, image) => { + if (image && typeof image === 'object' && image.type) { + if (!acc[image.type]) { + acc[image.type] = []; + } + acc[image.type].push(image); + } + return acc; + }, {}); + } + + async update() { + this.accordion.innerHTML = "Loading..."; + + try { + this.allImages = []; + let filesFound = false; + + for (const file of this.yamlFiles) { + try { + const yamlContent = await this.fetchYamlContent(file.name); + const parsedContent = this.parseYamlForImages( + yamlContent, + file.type, + file.skipLevels, + file.sections, + file.pathAdjustment, + file.ignoreKey + ); + + console.log(`Parsed content for ${file.name}:`, parsedContent); + + this.allImages.push(...parsedContent); + filesFound = true; + } catch (error) { + console.warn(`File ${file.name} not found or couldn't be processed. Skipping.`, error); + } + } + + console.log("All Images after parsing:", this.allImages); + + this.accordion.innerHTML = ""; // Clear the "Loading..." message + + // Display warning if conditions are met + if (!filesFound && this.customImages.length === 0 && localStorage.getItem('noFilesWarningDismissed') !== 'true') { + this.displayNoFilesMessage(); + } + + // Always display the accordion + this.filteredImages = this.allImages; + console.log("Parsed Prompt Images:", this.allImages); + this.sortAndDisplayImages(); + + // Use setTimeout to ensure DOM is updated before setting up drag and drop + setTimeout(() => { + this.setupDragAndDrop(); + }, 0); + + } catch (error) { + console.error("Error loading prompt images:", error); + this.accordion.innerHTML = "Error loading prompt images: " + error.message; + } + } + + displayNoFilesMessage() { + const messageContainer = $el("div", { + style: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "flex-start", + minHeight: "200px", + textAlign: "center", + backgroundColor: "#FFF3CD", + border: "1px solid #FFEEBA", + borderRadius: "5px", + padding: "20px", + margin: "20px 0" + } + }); + + const iconElement = $el("div", { + innerHTML: "⚠", // Warning icon + style: { + fontSize: "48px", + color: "#856404", + marginBottom: "15px" + } + }); + + const textElement = $el("p", { + innerHTML: "No prompt images found.", + style: { + fontSize: "18px", + color: "#856404", + marginBottom: "10px" + } + }); + + const linkElement = $el("a", { + href: "https://civitai.com/models/615967?modelVersionId=789576", + target: "_blank", + innerHTML: "Download officially supported file packages here", + style: { + color: "#856404", + textDecoration: "underline" + } + }); + + const text2Element = $el("p", { + innerHTML: "After downloading a package simply extract it to your promptImages folder in your Input directory. (If it doesn't exist just create it, obviously.)", + style: { + fontSize: "18px", + color: "#856404", + marginBottom: "10px" + } + }); + + const dismissButton = $el("button", { + textContent: "Dismiss", + onclick: () => { + localStorage.setItem('noFilesWarningDismissed', 'true'); + messageContainer.remove(); + this.update(); // Re-run update to display categories + }, + style: { + marginTop: "10px", + padding: "5px 10px", + backgroundColor: "#856404", + color: "white", + border: "none", + borderRadius: "4px", + cursor: "pointer" + } + }); + + messageContainer.appendChild(iconElement); + messageContainer.appendChild(textElement); + messageContainer.appendChild(linkElement); + messageContainer.appendChild(text2Element); + messageContainer.appendChild(dismissButton); + + this.accordion.appendChild(messageContainer); + } + + async fetchYamlContent(filename) { + const response = await fetch(`${this.baseUrl}/view?filename=${filename}&type=input&subfolder=promptImages`); + if (!response.ok) { + throw new Error(`Failed to fetch YAML file ${filename}: ${response.statusText}`); + } + return await response.text(); + } + + parseYamlForImages(yamlContent, type, skipLevels, sections = null, pathAdjustment = null, ignoreKey = null) { + const lines = yamlContent.split('\n'); + const stack = []; + const images = []; + + lines.forEach((line, index) => { + const trimmedLine = line.trim(); + if (trimmedLine === '' || trimmedLine.startsWith('#')) return; + + const indent = line.search(/\S|$/); + while (stack.length > 0 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = trimmedLine.split(':')[0].trim(); + stack.push({ key, indent }); + + const nextLine = lines[index + 1]; + if (nextLine && nextLine.trim().startsWith('-')) { + let path = stack.slice(skipLevels, -1).map(item => item.key).join('/'); + + // Remove the ignored key from the path + // Only apply the ignoreKey logic if ignoreKey is not null + if (ignoreKey) { + path = path.split('/').filter(segment => segment !== ignoreKey).join('/'); + } + + if (pathAdjustment) { + if (pathAdjustment.remove) { + path = path.split('/').filter(segment => !pathAdjustment.remove.includes(segment)).join('/'); + } + if (pathAdjustment.add) { + path = `${pathAdjustment.add}/${path}`; + } + } + + const imageFilename = `${key}.jpeg`; + const imageUrl = `${this.baseUrl}/view?filename=${imageFilename}&type=input&subfolder=promptImages/thumbnails/${path}`; + const tags = nextLine.trim().substring(1).trim(); + + const image = { name: key, path: imageUrl, tags: tags, type: type }; + + if (sections) { + for (const [sectionKey, sectionName] of Object.entries(sections)) { + if (path.includes(sectionKey)) { + image.section = sectionName; + break; + } + } + } + + images.push(image); + } + }); + + return images; + } + + displaypromptImages(images) { + this.accordion.innerHTML = ""; + if (images.length === 0) { + this.accordion.innerHTML = "No matching prompts found."; + return; + } + + const groupedImages = this.groupImagesByType(images); + + for (const [type, typeImages] of Object.entries(groupedImages)) { + const section = this.createAccordionSection(type, typeImages); + this.accordion.appendChild(section); + } + } + + updateGallery() { + // Add custom images to allImages + const customImageSection = this.allImages.find(section => section.type === "Custom"); + if (customImageSection) { + customImageSection.images = this.customImages; + } else { + this.allImages.push({ type: "Custom", images: this.customImages }); + } + + this.filteredImages = this.allImages.flatMap(section => section.images); + this.sortAndDisplayImages(); + } + + groupImagesByType(images) { + return images.reduce((acc, image) => { + if (image && typeof image === 'object') { + const type = image.type || 'Unknown'; + if (!acc[type]) { + acc[type] = []; + } + acc[type].push(image); + } else { + console.warn('Invalid image object:', image); + } + return acc; + }, {}); + } + + createAccordionSection(type, images) { + const section = $el("div", { + className: `accordion-section ${type === "Custom" ? "custom-section" : ""}`, + style: { marginBottom: "10px" }, + "data-type": type + }); + + + const header = $el("div.accordion-header", { + style: { + cursor: "pointer", + padding: "10px", + backgroundColor: "#2a2a2a", + borderRadius: "4px", + marginBottom: "5px", + display: "flex", + justifyContent: "space-between", + alignItems: "center" + } + }); + + // Always add 1 to the count for the Custom category to include the "Add" button + const itemCount = type === "Custom" ? images.length + 1 : images.length; + const headerText = $el("span", { textContent: `${type} (${itemCount})` }); + const isOpen = this.sectionStates[type] !== false; + const indicator = $el("span", { + textContent: this.sectionStates[type] ? "-" : "+", + style: { + fontSize: "18px", + fontWeight: "bold" + } + }); + + header.appendChild(headerText); + header.appendChild(indicator); + + const content = $el("div.accordion-content", { + style: { + display: isOpen ? "grid" : "none", + gridTemplateColumns: "repeat(auto-fill, minmax(100px, 1fr))", + gap: "10px", + padding: "10px", + backgroundColor: "#1a1a1a", + borderRadius: "4px" + } + }); + + header.onclick = () => { + if (content.style.display === "none") { + content.style.display = "grid"; + indicator.textContent = "-"; + this.sectionStates[type] = true; + } else { + content.style.display = "none"; + indicator.textContent = "+"; + this.sectionStates[type] = false; + } + this.saveSectionStates(); + }; + + if (type === "Custom") { + const addButton = this.createAddCustomImageButton(); + content.appendChild(addButton); + + // Add custom images after the buttons + images.forEach(image => { + const imgElement = this.createImageElement(image); + content.appendChild(imgElement); + }); + } else { + images.forEach(image => { + const imgElement = this.createImageElement(image); + content.appendChild(imgElement); + }); + } + + section.appendChild(header); + section.appendChild(content); + + return section; + } + + createImageElement(image) { + const imgContainer = $el("div.prompt-image-container", { + style: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "flex-start", + cursor: "pointer", + width: "100px", // Set a fixed width + height: "140px", // Set a fixed height to accommodate image and text + overflow: "hidden" // Hide overflow + }, + onclick: () => this.copyToClipboard(image.name, image.tags) + }); + + const img = $el("img", { + src: image.path, + alt: image.name, + style: { + width: "100px", + height: "100px", + objectFit: "cover", + borderRadius: "5px" + }, + onerror: () => { + img.src = this.placeholderImageUrl; + img.onerror = () => { + console.error("Failed to load both original and placeholder images for:", image.name); + img.style.display = 'none'; + }; + } + }); + + const label = $el("span", { + textContent: image.name, + style: { + marginTop: "5px", + fontSize: "12px", + textAlign: "center", + width: "100%", // Ensure the label takes full width + overflow: "hidden", + textOverflow: "ellipsis", + display: "-webkit-box", + "-webkit-line-clamp": "2", // Limit to 2 lines + "-webkit-box-orient": "vertical", + wordBreak: "break-word" + } + }); + + imgContainer.appendChild(img); + imgContainer.appendChild(label); + + return imgContainer; + } + + copyToClipboard(imageName, tags) { + let textToCopy = tags; + + // If tags is an object or array, convert it to a string + if (typeof tags === 'object') { + textToCopy = JSON.stringify(tags); + } + + // Ensure textToCopy is a string and trim any whitespace + textToCopy = String(textToCopy).trim(); + + navigator.clipboard.writeText(textToCopy).then(() => { + console.log('Tags copied to clipboard'); + this.showToast('success', 'Tags Copied!', `Tags for "${imageName}" copied to clipboard`); + }).catch(err => { + console.error('Failed to copy tags: ', err); + this.showToast('error', 'Copy Failed', `Failed to copy tags for "${imageName}"`); + }); + } + + showToast(severity, summary, detail) { + app.extensionManager.toast.add({ + severity: severity, + summary: summary, + detail: detail, + life: 5000 + }); + } +} + +app.registerExtension({ + name: "comfy.prompt.gallery", + async setup() { + const gallery = new PromptGallery(app); + + app.extensionManager.registerSidebarTab({ + id: "prompt.gallery", + icon: "pi pi-id-card", + title: "Prompt Gallery", + tooltip: "Prompt Gallery", + type: "custom", + render: (el) => { + el.appendChild(gallery.element); + gallery.update(); // Load wildcard images when the tab is rendered + }, + }); + }, +}); \ No newline at end of file