diff --git a/README.md b/README.md index b3a7f83..56d216a 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,19 @@ Adds a custom color picker to nodes & groups Adds a favicon and title to the window, favicon changes color while generating and the window title includes the number of prompts in the queue ## Image Feed -![image](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/assets/125205205/caea0d48-85b9-4ca9-9771-5c795db35fbc) -Adds a panel showing images that have been generated in the current session, you can control the direction that images are added and the position of the panel via the ComfyUI settings screen and the size of the panel and the images via the sliders at the top of the panel. -![image](https://github.com/pythongosssss/ComfyUI-Custom-Scripts/assets/125205205/ca093d38-41a3-4647-9223-5bd0b9ee4f1e) +![image](https://github.com/birdddev/ComfyUI-Custom-Scripts/assets/47731506/b9f96e8b-c891-459d-b5c4-b43432caf880) +Adds a panel showing images that have been generated in the current session, you can sort the images that are added by newest or oldest and change the position of the panel via the panel's settings. + + +![image](https://github.com/birdddev/ComfyUI-Custom-Scripts/assets/47731506/1e4dbc2b-7484-43ee-9778-f4dcaddee913) + + +Holding Shift while adjusting the panel size also adjusts the size of the images. +While holding Ctrl, the panel will snap to the closest amount of "columns" the image size allows. + + +https://github.com/birdddev/ComfyUI-Custom-Scripts/assets/47731506/78707e01-2053-438c-905d-ce6a1578b53b + ## KSampler (Advanced) denoise helper Provides a simple method to set custom denoise on the advanced sampler diff --git a/web/js/common/popup.css b/web/js/common/popup.css new file mode 100644 index 0000000..8270c5d --- /dev/null +++ b/web/js/common/popup.css @@ -0,0 +1,38 @@ +.pysssss-popup { + /* Could be globals somewhere else? */ + --popup-radius: 5px; + + position: absolute; + left: 0px; + top: 0px; + font-family: sans-serif; + font-size: 12px; + z-index: 199; + border-radius: var(--popup-radius); + padding: 5px; + + width: 100%; + height: 100%; + + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.pysssss-popup-header { + border-bottom: 1px solid var(--border-color); + text-align: right; + padding: 4px; +} + +.pysssss-popup-container { + position: absolute; + left: 0px; + top: 0px; + clear: both; + background: #191919fa; + border-radius: var(--popup-radius); + width: auto; + height: auto; + padding: 4px; +} \ No newline at end of file diff --git a/web/js/common/popup.js b/web/js/common/popup.js new file mode 100644 index 0000000..c38a6bc --- /dev/null +++ b/web/js/common/popup.js @@ -0,0 +1,142 @@ +import { $el, ComfyDialog } from "../../../../scripts/ui.js"; +import { api } from "../../../../scripts/api.js"; +import { addStylesheet } from "./utils.js"; + +addStylesheet(import.meta.url); + +export class PopUp { + constructor(element, options) { + let name = options.name; + this.activeOnHover = options.activeOnHover; + this.maxMouseDistance = options.maxMouseDistance ? options.maxMouseDistance : 50; + + this.root = $el(`div.pysssss-popup`, { + parent: document.body, + }); + this.container = $el("div.pysssss-popup-container"); + + if (name) { + const header = $el("div.pysssss-popup-header"); + const headerLabel = $el("div.pysssss-popup-label") + header.append(headerLabel); + headerLabel.innerHTML = name; + + this.container.append(header); + } + + this.content = $el("div.pysssss-popup-content"); + + this.root.append(this.container); + this.container.append(this.content); + + + this.onRootMouseMoveBind = this.onRootMouseMove.bind(this); + this.onRootClickBind = this.onRootClick.bind(this); + this.onShowBind = this.show.bind(this); + + this.attach(element); + this.hide(); + } + + /* (content) => {} */ + setContent(contentCallback) { + contentCallback(this.content); + } + + hide() { + this.root.style.display = "none"; + + this.root.removeEventListener('mousemove', this.onRootMouseMoveBind); + this.root.removeEventListener('mousedown', this.onRootClickBind); + } + + show() { + this.root.style.display = "flex"; + + this.validatePosition(); + + this.root.addEventListener('mousemove', this.onRootMouseMoveBind); + this.root.addEventListener('mousedown', this.onRootClickBind); + } + + attach(element) { + this.attachedElement = element; + if (this.activeOnHover) { + element.addEventListener('mouseenter', this.onShowBind); + } else { + element.addEventListener('mousedown', this.onShowBind); + } + } + + detach() { + let element = this.attachedElement; + if (this.activeOnHover) { + element.removeEventListener('mouseenter', this.onShowBind); + } else { + element.removeEventListener('mousedown', this.onShowBind); + } + + this.attachedElement = null; + } + + inAttachedElement(event, x, y) { + let rect = this.attachedElement.getBoundingClientRect(); + return rect.left <= x && x <= rect.right && + rect.top <= y && y <= rect.bottom; + } + + onRootMouseMove(event) { + let mouseX = event.x; + let mouseY = event.y; + + if (this.inAttachedElement(event, mouseX, mouseY)) { + return; + } + + let rect = this.container.getBoundingClientRect(); + + /* If certain distance away from popup */ + const maxDistance = 50; + if (rect.top - mouseY > this.maxMouseDistance || mouseY - rect.bottom > this.maxMouseDistance || + rect.left - mouseX > this.maxMouseDistance || mouseX - rect.right > this.maxMouseDistance + ) { + this.hide(); + } + } + + onRootClick(event) { + if (this.container.matches(":hover")) { + return; + } + + this.hide(); + } + + validatePosition() { + /* Set to position of attached element*/ + let rect = this.attachedElement.getBoundingClientRect(); + let computedStyle = getComputedStyle(this.container); + + let buttonWidth = rect.right - rect.left; + let buttonHeight = rect.top - rect.bottom; + let windowWidth = window.innerWidth; + let windowHeight = window.innerHeight; + let containerWidth = parseFloat(computedStyle.width); + let containerHeight = parseFloat(computedStyle.height); + + let x = rect.left; + let y = rect.top; + + this.container.style.left = `${x}px`; + this.container.style.top = `${y - buttonHeight + 2}px`; + + /* Make sure popup is fully inside window */ + if (x + containerWidth > windowWidth) { + this.container.style.left = `${rect.right - containerWidth - 10.0}px`; + } + + if (y + containerHeight * 2.0 + (buttonHeight) > windowHeight) { + this.container.style.top = `${rect.top - containerHeight - 10.0}px`; + } + } +} \ No newline at end of file diff --git a/web/js/imageFeed.css b/web/js/imageFeed.css new file mode 100644 index 0000000..964bc69 --- /dev/null +++ b/web/js/imageFeed.css @@ -0,0 +1,197 @@ +.pysssss-image-feed-root { + --image-size: 128px; + --image-gap-size: 4px; + + --image-feed-radius: 5px; + --image-feed-theme-handle: #333; + + + align-items: stretch; + display: flex; + font-size: 12px; + position: absolute; + text-align: center; + vertical-align: top; + z-index: 99; +} +.pysssss-image-feed-root--left, .pysssss-image-feed-root--right { + top: 0; + height: 100vh; +} +.pysssss-image-feed-root--left { + left: 0; +} +.pysssss-image-feed-root--right { + right: 0; + flex-direction: row-reverse; +} +.pysssss-image-feed-root--top, .pysssss-image-feed-root--bottom { + left: 0; + width: 100vw; +} +.pysssss-image-feed-root--top { + top: 0; + flex-direction: column; +} +.pysssss-image-feed-root--bottom { + bottom: 0; + flex-direction: column-reverse; +} +.pysssss-image-feed { + flex: 1; + background: var(--comfy-menu-bg); + color: var(--fg-color); + font-family: sans-serif; + font-size: 12px; + display: flex; + flex-direction: column; +} +.pysssss-image-feed--left, .pysssss-image-feed--right { + top: 0; + height: 100vh; +} + +.pysssss-image-feed--top, .pysssss-image-feed--bottom { + left: 0; + width: 100vw; +} + +.pysssss-image-feed-header { + position: relative; + display: flex; + gap: 5px; + padding: 5px; + justify-content: space-between; +} +.pysssss-image-feed-btn-group { + align-items: stretch; + display: flex; + gap: .5rem; + flex: 0 1 fit-content; + justify-content: flex-end; +} +.pysssss-image-feed-btn { + background-color:var(--comfy-input-bg); + border-radius: var(--image-feed-radius); + border:2px solid var(--border-color); + color: var(--fg-color); + cursor:pointer; + display:inline-block; + flex: 0 1 fit-content; + text-decoration:none; +} +.pysssss-image-feed-btn.sizing-btn:checked { + filter: invert(); +} +.pysssss-image-feed-btn.clear-btn { + padding: 5px 20px; +} +.pysssss-image-feed-btn.hide-btn { + padding: 5px; + aspect-ratio: 1 / 1; +} +.pysssss-image-feed-btn:hover { + filter: brightness(1.2); +} +.pysssss-image-feed-btn:active { + position:relative; + top:1px; +} + +.pysssss-image-feed-header > * { + min-height: 24px; +} +.pysssss-image-feed-list { + flex: 1; + align-content: flex-start; + overflow-y: auto; + display: grid; + gap: var(--image-gap-size); + justify-items: center; + transition: 100ms linear; + scrollbar-gutter: stable both-edges; + padding: 5px; + background: var(--comfy-input-bg); + border-radius: var(--image-feed-radius); + margin: 5px; + margin-top: 0px; +} +.pysssss-image-feed-list div, .pysssss-image-feed-list a { + display: flex; + flex-wrap: wrap; + width: var(--image-size); + height: var(--image-size); + align-content: center; + justify-content: center; +} +.pysssss-image-feed-list div { + background: rgba(0,0,0,0.15); + box-sizing: border-box; +} +.pysssss-image-feed-list div:hover{ + filter: brightness(1.2); +} +.pysssss-image-feed-list img { + max-width: 100%; + max-height: 100%; +} +.pysssss-image-feed-list::-webkit-scrollbar { + background: var(--comfy-input-bg); + border-radius: var(--image-feed-radius); +} +.pysssss-image-feed-list::-webkit-scrollbar-thumb { + background:var(--comfy-menu-bg); + border: 5px solid transparent; + border-radius: var(--image-feed-radius); + background-clip: content-box; +} +.pysssss-image-feed-list::-webkit-scrollbar-thumb:hover { + background: var(--border-color); + background-clip: content-box; +} +.pysssss-image-feed-handle { + flex: 0; + background-color: #191919; + display: flex; + align-items: center; + justify-content: center; + + /* Disables dragging so it doesn't mess up the grab. */ + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.pysssss-image-feed-handle--left, .pysssss-image-feed-handle--right { + cursor: col-resize; + min-width: 8px; + height: 100vh; +} +.pysssss-image-feed-handle--top, .pysssss-image-feed-handle--bottom { + cursor: row-resize; + min-height: 8px; + width: 100vw; +} +.pysssss-image-feed-handle:hover, .pysssss-image-feed-handle:active { + background-color: var(--border-color); +} + +.pysssss-image-feed-table td { + text-align: right; +} +.pysssss-image-feed-table td.right { + text-align: left; +} +.pysssss-image-feed-table select { + color: var(--fg-color); + border: 1px solid var(--border-color); + border-radius: 5px; + width: 100%; + background-color: var(--comfy-input-bg); +} +.pysssss-image-feed-table select:hover { + filter: brightness(1.2); +} +.pysssss-image-feed-table select:focus { + outline: none !important; + box-shadow: 0 0 3px var(--border-color); +} \ No newline at end of file diff --git a/web/js/imageFeed.js b/web/js/imageFeed.js index b477912..b2218ac 100644 --- a/web/js/imageFeed.js +++ b/web/js/imageFeed.js @@ -2,207 +2,10 @@ import { api } from "../../../scripts/api.js"; import { app } from "../../../scripts/app.js"; import { $el } from "../../../scripts/ui.js"; import { lightbox } from "./common/lightbox.js"; +import { PopUp } from "./common/popup.js"; +import { addStylesheet } from "./common/utils.js"; -$el("style", { - textContent: ` - .pysssss-image-feed { - position: absolute; - background: var(--comfy-menu-bg); - color: var(--fg-color); - z-index: 99; - font-family: sans-serif; - font-size: 12px; - display: flex; - flex-direction: column; - } - .pysssss-image-feed--top, .pysssss-image-feed--bottom { - width: 100vw; - min-height: 30px; - max-height: calc(var(--max-size, 20) * 1vh); - } - .pysssss-image-feed--top { - top: 0; - } - .pysssss-image-feed--bottom { - bottom: 0; - flex-direction: column-reverse; - padding-top: 5px; - } - .pysssss-image-feed--left, .pysssss-image-feed--right { - top: 0; - height: 100vh; - min-width: 200px; - max-width: calc(var(--max-size, 10) * 1vw); - } - .pysssss-image-feed--left { - left: 0; - } - .pysssss-image-feed--right { - right: 0; - } - - .pysssss-image-feed--left .pysssss-image-feed-menu, .pysssss-image-feed--right .pysssss-image-feed-menu { - flex-direction: column; - } - - .pysssss-image-feed-menu { - position: relative; - flex: 0 1 min-content; - display: flex; - gap: 5px; - padding: 5px; - justify-content: space-between; - } - .pysssss-image-feed-btn-group { - align-items: stretch; - display: flex; - gap: .5rem; - flex: 0 1 fit-content; - justify-content: flex-end; - } - .pysssss-image-feed-btn { - background-color:var(--comfy-input-bg); - border-radius:5px; - border:2px solid var(--border-color); - color: var(--fg-color); - cursor:pointer; - display:inline-block; - flex: 0 1 fit-content; - text-decoration:none; - } - .pysssss-image-feed-btn.sizing-btn:checked { - filter: invert(); - } - .pysssss-image-feed-btn.clear-btn { - padding: 5px 20px; - } - .pysssss-image-feed-btn.hide-btn { - padding: 5px; - aspect-ratio: 1 / 1; - } - .pysssss-image-feed-btn:hover { - filter: brightness(1.2); - } - .pysssss-image-feed-btn:active { - position:relative; - top:1px; - } - - .pysssss-image-feed-menu section { - border-radius: 5px; - background: rgba(0,0,0,0.6); - padding: 0 5px; - display: flex; - gap: 5px; - align-items: center; - position: relative; - } - .pysssss-image-feed-menu section span { - white-space: nowrap; - } - .pysssss-image-feed-menu section input { - flex: 1 1 100%; - background: rgba(0,0,0,0.6); - border-radius: 5px; - overflow: hidden; - z-index: 100; - } - - .sizing-menu { - position: relative; - } - - .size-controls-flyout { - position: absolute; - transform: scaleX(0%); - transition: 200ms ease-out; - transition-delay: 500ms; - z-index: 101; - width: 300px; - } - - .sizing-menu:hover .size-controls-flyout { - transform: scale(1, 1); - transition: 200ms linear; - transition-delay: 0; - } - .pysssss-image-feed--bottom .size-controls-flyout { - transform: scale(1,0); - transform-origin: bottom; - bottom: 0; - left: 0; - } - .pysssss-image-feed--top .size-controls-flyout { - transform: scale(1,0); - transform-origin: top; - top: 0; - left: 0; - } - .pysssss-image-feed--left .size-controls-flyout { - transform: scale(0, 1); - transform-origin: left; - top: 0; - left: 0; - } - .pysssss-image-feed--right .size-controls-flyout { - transform: scale(0, 1); - transform-origin: right; - top: 0; - right: 0; - } - - .pysssss-image-feed-menu > * { - min-height: 24px; - } - .pysssss-image-feed-list { - flex: 1 1 auto; - overflow-y: auto; - display: grid; - align-items: center; - justify-content: center; - gap: 4px; - grid-auto-rows: min-content; - grid-template-columns: repeat(var(--img-sz, 3), 1fr); - transition: 100ms linear; - scrollbar-gutter: stable both-edges; - padding: 5px; - background: var(--comfy-input-bg); - border-radius: 5px; - margin: 5px; - margin-top: 0px; - } - .pysssss-image-feed-list:empty { - display: none; - } - .pysssss-image-feed-list div { - height: 100%; - text-align: center; - } - .pysssss-image-feed-list::-webkit-scrollbar { - background: var(--comfy-input-bg); - border-radius: 5px; - } - .pysssss-image-feed-list::-webkit-scrollbar-thumb { - background:var(--comfy-menu-bg); - border: 5px solid transparent; - border-radius: 8px; - background-clip: content-box; - } - .pysssss-image-feed-list::-webkit-scrollbar-thumb:hover { - background: var(--border-color); - background-clip: content-box; - } - .pysssss-image-feed-list img { - object-fit: var(--img-fit, contain); - max-width: 100%; - max-height: calc(var(--max-size) * 1vh); - border-radius: 4px; - } - .pysssss-image-feed-list img:hover { - filter: brightness(1.2); - }`, - parent: document.body, -}); +addStylesheet(import.meta.url); app.registerExtension({ name: "pysssss.ImageFeed", @@ -229,174 +32,158 @@ app.registerExtension({ localStorage.setItem("pysssss.ImageFeed." + n, v); }; - const imageFeed = $el("div.pysssss-image-feed", { + const getValString = (n, d) => { + const v = localStorage.getItem("pysssss.ImageFeed." + n); + if (v) { + return v; + } + return d; + } + + const imageFeedRoot = $el("div.pysssss-image-feed-root", { parent: document.body, }); + + const imageFeed = $el("div.pysssss-image-feed"); + const imageList = $el("div.pysssss-image-feed-list"); - const feedLocation = app.ui.settings.addSetting({ - id: "pysssss.ImageFeed.Location", - name: "🐍 Image Feed Location", - defaultValue: "bottom", - type: () => { - return $el("tr", [ + const resizeHandle = $el("div.pysssss-image-feed-handle"); + + imageFeedRoot.append(imageFeed, resizeHandle); + + function updateFeedLocation(value) { + imageFeed.className = `pysssss-image-feed pysssss-image-feed--${value}`; + imageFeedRoot.className = `pysssss-image-feed-root pysssss-image-feed-root--${value}`; + resizeHandle.className = `pysssss-image-feed-handle pysssss-image-feed-handle--${value}`; + if (["left", "right"].includes(value)) { + imageFeedRoot.style.height = "unset"; + } else { + imageFeedRoot.style.width = "unset"; + } + } + + updateFeedLocation(getValString("Location", "bottom")); + + const clearButton = $el("button.pysssss-image-feed-btn.clear-btn", { + textContent: "Clear", + onclick: () => { + imageList.replaceChildren(); + imageList.style.gridTemplateColumns = "unset"; + }, + }); + + const hideButton = $el("button.pysssss-image-feed-btn.hide-btn", { + textContent: "❌", + onclick: () => { + imageFeedRoot.style.display = "none"; + showButton.style.display = "unset"; + saveVal("Visible", 0); + visible = false; + }, + }); + + const settingsButton = $el("button.pysssss-image-feed-btn.settings-btn", {}, [ + $el("label.size-control-handle", { textContent: "⚙️" }) + ]); + + const settingsPopup = new PopUp(settingsButton, { + name: "Feed Settings", + activeOnHover: true, + }); + settingsPopup.setContent( (content) => { + const table = $el("table.pysssss-image-feed-table"); + + table.append( + $el("tr", [ $el("td", [ $el("label", { - textContent: "🐍 Image Feed Location:", + textContent: "Image Size" }), ]), + $el("input.image-feed-image-size", { + type: "range", + min: 32, + max: 512, + step: 12, + oninput: (e) => { + e.target.parentElement.title = `Controls the maximum size of the images in the feed panel (${e.target.value}px/512px)`; + imageFeedRoot.style.setProperty("--image-size", `${e.target.value}px` ); + saveVal("ImageSize", e.target.value); + e.target.textContent = `${e.target.value}px/512px`; + }, + $: (el) => { + requestAnimationFrame(() => { + el.value = getVal("ImageSize", 128); + el.oninput({ target: el }); + }); + }, + }), + ]), + $el("tr", [ $el("td", [ + $el("label", { + textContent: "Location", + }), + ]), + $el("td.right", [ $el( "select", { - style: { - fontSize: "14px", - }, oninput: (e) => { - feedLocation.value = e.target.value; - imageFeed.className = `pysssss-image-feed pysssss-image-feed--${feedLocation.value}`; + saveVal("Location", e.target.value); + updateFeedLocation(getValString("Location", "bottom")); }, }, ["left", "top", "right", "bottom"].map((m) => $el("option", { value: m, textContent: m, - selected: feedLocation.value === m, + selected: getValString("Location", "bottom") === m, }) ) ), ]), - ]); - }, - onChange(value) { - imageFeed.className = `pysssss-image-feed pysssss-image-feed--${value}`; - }, - }); - - const feedDirection = app.ui.settings.addSetting({ - id: "pysssss.ImageFeed.Direction", - name: "🐍 Image Feed Direction", - defaultValue: "newest first", - type: () => { - return $el("tr", [ + ]), + $el("tr", [ $el("td", [ $el("label", { - textContent: "🐍 Image Feed Direction:", + textContent: "Sort by", }), ]), - $el("td", [ + $el("td.right", [ $el( "select", { - style: { - fontSize: "14px", - }, oninput: (e) => { - feedDirection.value = e.target.value; + saveVal("Direction", e.target.value); imageList.replaceChildren(...[...imageList.childNodes].reverse()); }, }, - ["newest first", "oldest first"].map((m) => + ["newest", "oldest"].map((m) => $el("option", { value: m, textContent: m, - selected: feedDirection.value === m, + selected: getValString("Direction", "newest") === m, }) ) ), ]), - ]); - }, - }); - - const clearButton = $el("button.pysssss-image-feed-btn.clear-btn", { - textContent: "Clear", - onclick: () => imageList.replaceChildren(), - }); + ]), + ); - const hideButton = $el("button.pysssss-image-feed-btn.hide-btn", { - textContent: "❌", - onclick: () => { - imageFeed.style.display = "none"; - showButton.style.display = "unset"; - saveVal("Visible", 0); - visible = false; - }, + content.append(table); }); - let columnInput; - function updateColumnCount(v) { - columnInput.parentElement.title = `Controls the number of columns in the feed (${v} columns).\nClick label to set custom value.`; - imageFeed.style.setProperty("--img-sz", v); - saveVal("ImageSize", v); - columnInput.max = Math.max(10, v, columnInput.max); - columnInput.value = v; - } - imageFeed.append( - $el("div.pysssss-image-feed-menu", [ - $el("section.sizing-menu", {}, [ - $el("label.size-control-handle", { textContent: "↹ Resize Feed" }), - $el("div.size-controls-flyout", {}, [ - $el("section.size-control.feed-size-control", {}, [ - $el("span", { - textContent: "Feed Size...", - }), - $el("input", { - type: "range", - min: 10, - max: 80, - oninput: (e) => { - e.target.parentElement.title = `Controls the maximum size of the image feed panel (${e.target.value}vh)`; - imageFeed.style.setProperty("--max-size", e.target.value); - saveVal("FeedSize", e.target.value); - }, - $: (el) => { - requestAnimationFrame(() => { - el.value = getVal("FeedSize", 25); - el.oninput({ target: el }); - }); - }, - }), - ]), - $el("section.size-control.image-size-control", {}, [ - $el("a", { - textContent: "Column count...", - style: { - cursor: "pointer", - textDecoration: "underline", - }, - onclick: () => { - const v = +prompt("Enter custom column count", 20); - if (!isNaN(v)) { - updateColumnCount(v); - } - }, - }), - $el("input", { - type: "range", - min: 1, - max: 10, - step: 1, - oninput: (e) => { - updateColumnCount(e.target.value); - }, - $: (el) => { - columnInput = el; - requestAnimationFrame(() => { - updateColumnCount(getVal("ImageSize", 4)); - }); - }, - }), - ]), - ]), - ]), + $el("div.pysssss-image-feed-header", [ + settingsButton, $el("div.pysssss-image-feed-btn-group", {}, [clearButton, hideButton]), ]), imageList ); showButton.onclick = () => { - imageFeed.style.display = "block"; + imageFeedRoot.style.display = "flex"; showButton.style.display = "none"; saveVal("Visible", 1); visible = true; @@ -414,13 +201,14 @@ app.registerExtension({ src.type }&subfolder=${encodeURIComponent(src.subfolder)}&t=${+new Date()}`; - const method = feedDirection.value === "newest first" ? "prepend" : "append"; + const method = getValString("Direction", "newest") === "newest" ? "prepend" : "append"; imageList[method]( $el("div", [ $el( "a", { target: "_blank", + draggable: false, href, onclick: (e) => { const imgs = [...imageList.querySelectorAll("img")].map((img) => img.getAttribute("src")); @@ -433,7 +221,104 @@ app.registerExtension({ ]) ); } + if (imageList.childNodes.length > 0) { + imageList.style.gridTemplateColumns = "repeat( auto-fit, minmax(var(--image-size, 128), 1fr) )"; + } } }); + + let isResizing = false; + let initialSize = 0; + let imageFeedRootSize = 0; + let paddingSize = 0; + let isVertical = false; + let isOpposite = false; + + resizeHandle.addEventListener('mousedown', (e) => { + isResizing = true; + + const feedLocation = getValString("Location", "bottom"); + isVertical = ["left", "right"].includes(feedLocation); + isOpposite = ["right", "bottom"].includes(feedLocation); + + if (isVertical) { + initialSize = e.clientX; + imageFeedRootSize = parseFloat(getComputedStyle(imageFeedRoot).width); + paddingSize = (imageFeedRootSize - parseFloat(getComputedStyle(imageFeed).width)) * 8; + } else { + initialSize = e.clientY; + imageFeedRootSize = parseFloat(getComputedStyle(imageFeedRoot).height); + paddingSize = (imageFeedRootSize - parseFloat(getComputedStyle(imageFeed).height)) * 16; + } + + [...imageList.querySelectorAll("img")].map((img) => { + img.setAttribute("draggable", false); + }); + + document.documentElement.style.cursor = isVertical ? "col-resize" : "row-resize"; + + document.addEventListener('mousemove', resizeContainer); + document.addEventListener('mouseup', stopResize); + }); + + function resizeContainer(e) { + if (!isResizing) { + return; + } + + var newSize = 0; + const feedLocation = getValString("Location", "bottom"); + if (isVertical) { + const deltaX = e.clientX - initialSize; + newSize = feedLocation == "left" ? imageFeedRootSize + deltaX : imageFeedRootSize - deltaX; + } else { + const deltaY = e.clientY - initialSize; + newSize = feedLocation == "top" ? imageFeedRootSize + deltaY : imageFeedRootSize - deltaY; + } + + /* Change image size */ + if (e.shiftKey && !e.ctrlKey) { + const axis = isVertical ? "movementX" : "movementY"; + + let newImageSize = parseInt(getVal("ImageSize")); + newImageSize += isOpposite ? -e[axis] : e[axis]; + newImageSize = Math.min(Math.max(32, newImageSize), 512); + + saveVal("ImageSize", newImageSize); + imageFeedRoot.style.setProperty("--image-size", `${newImageSize}px`); + /* Update settings slider */ + document.querySelector("input.image-feed-image-size").value = newImageSize; + } + + /* Snap to closest image-size multiple for a cleaner list */ + if (e.ctrlKey) { + const imageSize = getVal("ImageSize", 128); + let steps = Math.round(newSize / imageSize) + let gapSize = parseFloat(getComputedStyle(imageFeedRoot).getPropertyValue("--image-gap-size")); + newSize = steps * imageSize; + newSize = newSize + paddingSize + (steps * (gapSize + 2)); + } + + /* Make sure panel size is always within window */ + if (newSize >= 0.0 && newSize <= isVertical ? window.innerWidth : window.innerHeight) { + if (isVertical) { + imageFeedRoot.style.minWidth = `${newSize}px`; + } else { + imageFeedRoot.style.minHeight = `${newSize}px`; + } + } + } + + function stopResize() { + isResizing = false; + document.removeEventListener('mousemove', resizeContainer); + document.removeEventListener('mouseup', stopResize); + + document.documentElement.style.cursor = "default"; + + [...imageList.querySelectorAll("img")].map((img) => { + img.setAttribute("draggable", true); + }); + } }, });