| |
|
|
| import { $el } from "/scripts/ui.js"; |
| import { app } from "/scripts/app.js"; |
|
|
| import { info_VideoPlaybackOptions, options_VideoPlayback } from "../common/VideoOptions.js"; |
| import { utilitiesInstance } from "./Utilities.js"; |
|
|
| import { imageDrawerComponentManagerInstance } from "../ImageDrawer/Core/ImageDrawerModule.js"; |
|
|
| export const defaultKeyList = "prompt, workflow, parameters"; |
|
|
| var underButtonContent; |
|
|
| |
| |
| export class ConfigSetting { |
| constructor(settingName, defaultValue) { |
| this._settingName = settingName; |
| this._defaultValue = defaultValue; |
| this._onChange = () => { }; |
| } |
|
|
| |
| get value() { |
| return this._getValue(this._settingName, this._defaultValue); |
| } |
|
|
| |
| set value(newValue) { |
| this._setValue(this._settingName, newValue); |
| this._onChange(newValue); |
| } |
|
|
| |
| setOnChange(callback) { |
| this._onChange = callback; |
| } |
|
|
| |
| _getValue(name, defaultValue) { |
|
|
| const id = "JNodes.Settings." + name; |
|
|
| let val = null; |
|
|
| try { |
|
|
| val = app.ui.settings.getSettingValue(id, null); |
| |
|
|
| } catch { |
|
|
| } |
|
|
| if (val === null) { |
|
|
| val = localStorage.getItem(id); |
| |
|
|
|
|
| if (val === null || val === undefined || val === "undefined") { |
|
|
| |
| return defaultValue; |
| } |
| } |
|
|
| try { |
| const loadedValue = JSON.parse(val); |
| if (typeof loadedValue === "object") { |
| let fullValue = defaultValue; |
| Object.assign(fullValue, loadedValue); |
| return fullValue; |
| } else { return loadedValue; } |
| } catch (error) { |
| return val; |
| } |
| }; |
|
|
| _setValue(name, val) { |
|
|
| const id = "JNodes.Settings." + name; |
|
|
| localStorage.setItem(id, JSON.stringify(val)); |
| app.ui.settings.setSettingValue(id, val); |
| }; |
|
|
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| } |
|
|
| export class ImageDrawerConfigSetting extends ConfigSetting { |
| constructor(settingName, defaultValue) { |
| super("ImageDrawer." + settingName, defaultValue); |
| } |
| } |
|
|
| export let setting_bEnabled = new ImageDrawerConfigSetting("bEnabled", true); |
| export let setting_bMasterVisibility = new ImageDrawerConfigSetting("bMasterVisibility", true); |
| export let setting_DrawerAnchor = new ImageDrawerConfigSetting("DrawerAnchor", "top-left"); |
|
|
| export let setting_KeyList = new ImageDrawerConfigSetting("ImageVideo.KeyList", defaultKeyList); |
| export let setting_bKeyListAllowDenyToggle = new ImageDrawerConfigSetting("ImageVideo.bKeyListAllowDenyToggle", false); |
| export let setting_bMetadataTooltipToggle = new ImageDrawerConfigSetting("ImageVideo.bMetadataTooltipToggle", true); |
|
|
| export let setting_FavouritesDirectory = new ImageDrawerConfigSetting("Directories.Favourites", "output/Favourites"); |
|
|
| export let setting_ModelCardAspectRatio = new ImageDrawerConfigSetting("Models.AspectRatio", 0.67); |
|
|
| export let setting_VideoPlaybackOptions = new ImageDrawerConfigSetting("Video.VideoOptions", new options_VideoPlayback()); |
|
|
| |
|
|
| export const setupUiSettings = (onDrawerAnchorInput) => { |
| |
| { |
| const labelWidget = $el("label", { |
| textContent: "Image Drawer Enabled:", |
| }); |
|
|
| const settingWidget = $el( |
| "input", |
| { |
| type: "checkbox", |
| checked: setting_bEnabled.value, |
| oninput: (e) => { |
| setting_bEnabled.value = e.target.checked; |
| }, |
| }, |
| ); |
|
|
| const tooltip = "Whether or not the image drawer is initialized (requires page reload)"; |
| addJNodesSetting(labelWidget, settingWidget, tooltip); |
| } |
| |
| { |
| const labelWidget = $el("label", { |
| textContent: "Image Drawer Anchor:", |
| }); |
|
|
| const settingWidget = createDrawerSelectionWidget(onDrawerAnchorInput); |
|
|
| const tooltip = "To which part of the screen the drawer should be docked"; |
| addJNodesSetting(labelWidget, settingWidget, tooltip); |
| } |
|
|
| |
| { |
| const labelWidget = $el("label", { |
| textContent: "Image Drawer Image & Video Key List:", |
| }); |
|
|
| const settingWidget = $el( |
| "input", |
| { |
| defaultValue: setting_KeyList.value, |
| oninput: (e) => { |
| setting_KeyList.value = e.target.value; |
| }, |
| }, |
| ); |
|
|
| const tooltip = "A set of comma-separated names to include or exclude " + |
| "from the tooltips applied to images in the drawer"; |
| addJNodesSetting(labelWidget, settingWidget, tooltip); |
| } |
|
|
| |
| { |
| const labelWidget = $el("label", { |
| textContent: "Image Drawer Image & Video Key List Allow/Deny Toggle:", |
| }); |
|
|
| const settingWidget = $el( |
| "input", |
| { |
| type: "checkbox", |
| checked: setting_bKeyListAllowDenyToggle.value, |
| oninput: (e) => { |
| setting_bKeyListAllowDenyToggle.value = e.target.checked; |
| }, |
| }, |
| ); |
|
|
| const tooltip = `Whether the terms listed in the Key List should be |
| denied or allowed, excluding everything else. |
| True = Allow list, False = Deny list.`; |
| addJNodesSetting(labelWidget, settingWidget, tooltip); |
| } |
|
|
| |
| { |
| const labelWidget = $el("label", { |
| textContent: "Image Drawer Image & Video Metadata Tooltip Toggle:", |
| }); |
|
|
| const settingWidget = $el( |
| "input", |
| { |
| type: "checkbox", |
| checked: setting_bMetadataTooltipToggle.value, |
| oninput: (e) => { |
| setting_bMetadataTooltipToggle.value = e.target.checked; |
| }, |
| }, |
| ); |
|
|
| const tooltip = `Whether to show a tooltip with metadata when hovering images and videos in the drawer.`; |
| addJNodesSetting(labelWidget, settingWidget, tooltip); |
| } |
|
|
| |
| { |
| const labelWidget = $el("label", { |
| textContent: "Favourites directory:", |
| }); |
|
|
| const settingWidget = $el( |
| "input", |
| { |
| defaultValue: setting_FavouritesDirectory.value, |
| oninput: (e) => { |
| setting_FavouritesDirectory.value = e.target.value; |
| }, |
| }, |
| ); |
|
|
| const tooltip = "A directory to which all images marked as a favourite in image overflow menus will be copied. " + |
| "The directory can be relative to the comfy folder ('output/Favourites') or absolute (i.e. 'C:/Favourites' or '/home/Pictures/Favourites') " + |
| "assuming the user and comfy process have sufficient permissions to access these directories."; |
| addJNodesSetting(labelWidget, settingWidget, tooltip); |
| } |
|
|
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| }; |
|
|
| export function createDrawerSelectionWidget(onInput) { |
| return $el("select", { |
| oninput: onInput, |
| }, |
| ["top-left", "top-right", "bottom-left", "bottom-right"].map((m) => |
| $el("option", { |
| value: m, |
| textContent: m, |
| selected: setting_DrawerAnchor.value === m, |
| }) |
| ) |
| ); |
| } |
|
|
| function createExpandableSettingsArea() { |
|
|
| const nameWidget = $el("label", { |
| textContent: "JNodes Settings ", |
| }); |
|
|
| const toggleButton = document.createElement("button"); |
| toggleButton.textContent = "▶"; |
| toggleButton.classList.add("toggle-btn"); |
|
|
| underButtonContent = $el("div", { |
| style: { |
| id: "under-button-content", |
| textAlign: "center", |
| margin: "auto", |
| width: "100%", |
| display: "table", |
| visibility: "collapse" |
| } |
| }); |
|
|
| |
| toggleButton.addEventListener("click", function () { |
| const bIsCurrentlyCollapsed = underButtonContent.style.visibility === "collapse"; |
|
|
| |
| underButtonContent.style.visibility = |
| bIsCurrentlyCollapsed ? "visible" : "collapse"; |
|
|
| |
| toggleButton.textContent = bIsCurrentlyCollapsed ? "▼" : "▶"; |
| }); |
|
|
| app.ui.settings.addSetting({ |
| id: "JNodes.SettingsContainer", |
| name: "JNodes Settings Container", |
| type: () => { |
| return $el("tr", { |
| style: { |
| width: "100%", |
| } |
| }, [ |
| $el("td", { |
| colSpan: "2", |
| }, [ |
| $el("div", { |
| style: { |
| textAlign: "center", |
| margin: "auto", |
| width: "100%", |
| }, |
| }, [nameWidget, toggleButton]), underButtonContent |
| ]), |
| ]); |
| }, |
| }); |
| } |
|
|
| export function addJNodesSetting(nameWidget, settingWidget, tooltip) { |
| if (!underButtonContent) { |
| createExpandableSettingsArea(); |
| } |
|
|
| function sortTable() { |
| const rows = Array.from(underButtonContent.children); |
|
|
| |
| rows.sort((a, b) => { |
| const textA = a.children[0].textContent.trim().toLowerCase(); |
| const textB = b.children[0].textContent.trim().toLowerCase(); |
| return textA.localeCompare(textB); |
| }); |
|
|
| underButtonContent.innerHTML = ""; |
|
|
| |
| rows.forEach(row => underButtonContent.appendChild(row)); |
| } |
|
|
| let title = tooltip ? tooltip.toString() : ""; |
| nameWidget.title = nameWidget.title ? nameWidget.title : title; |
| settingWidget.title = settingWidget.title ? settingWidget.title : title; |
|
|
| underButtonContent.appendChild( |
| $el("tr", [ |
| $el("td", { |
| style: { |
| verticalAlign: "middle", |
| } |
| }, [ |
| nameWidget ? nameWidget : $el("div") |
| ]), |
| $el("td", { |
| style: { |
| verticalAlign: "middle", |
| textAlign: "left", |
| } |
| }, [ |
| settingWidget ? settingWidget : $el("div") |
| ]) |
| ]) |
| ); |
|
|
| sortTable(); |
| } |
|
|
| export function createFlyoutHandle(handleText, handleClassSuffix = "", menuClassSuffix = "", parentRect = window) { |
| let handle = $el(`section.flyout-handle${handleClassSuffix}`, [ |
| $el("label.flyout-handle-label", { textContent: handleText }) |
| ]); |
|
|
| let menu = $el(`div.flyout-menu${menuClassSuffix}`); |
|
|
| handle.appendChild(menu); |
|
|
| handle.determineTransformLayout = function () { |
|
|
| const handleRect = handle.getBoundingClientRect(); |
|
|
| let transformOriginX = "0%"; |
| let transformOriginY = "0%"; |
|
|
| const halfHeight = (parentRect.innerHeight || parentRect.height) / 2; |
|
|
| const bIsHandleInTopHalf = handleRect.top < halfHeight; |
| if (bIsHandleInTopHalf) { |
| |
| menu.style.top = "0"; |
| menu.style.bottom = "auto"; |
| menu.style.maxHeight = `${parentRect.bottom - handleRect.top - 50}px`; |
| } else { |
| |
| transformOriginY = "100%"; |
| menu.style.bottom = "0"; |
| menu.style.top = "auto"; |
| menu.style.maxHeight = `${handleRect.top - parentRect.top - 50}px`; |
| } |
|
|
| const halfWidth = (parentRect.innerWidth || parentRect.width) / 2; |
|
|
| const bIsHandleInLeftHalf = handleRect.left < halfWidth; |
| if (bIsHandleInLeftHalf) { |
| |
| menu.style.left = "0"; |
| menu.style.right = "auto"; |
| menu.style.maxWidth = `${parentRect.right - handleRect.left - 50}px`; |
| } else { |
| |
| transformOriginX = "100%"; |
| menu.style.right = "0"; |
| menu.style.left = "auto"; |
| menu.style.maxWidth = `${handleRect.left - parentRect.left - 50}px`; |
| } |
|
|
| menu.style.transformOrigin = `${transformOriginX} ${transformOriginY}`; |
| }; |
|
|
| handle.addEventListener("mouseover", handle.determineTransformLayout); |
|
|
| return { handle: handle, menu: menu }; |
| } |
|
|
| export function createVideoPlaybackOptionsMenuWidgets(menu) { |
|
|
| const infos = new info_VideoPlaybackOptions(); |
|
|
| async function callForEachCallbackOnEachElementInImageList(propertyName, propertyValue, info) { |
| const imageDrawerListInstance = imageDrawerComponentManagerInstance.getComponentByName("ImageDrawerList"); |
| for (let child of imageDrawerListInstance.getImageListChildren()) { |
| for (let element of utilitiesInstance.getVideoElements(child)) { |
| info.forEachElement(element, propertyName, propertyValue); |
| } |
| } |
| } |
|
|
| function oninput(propertyName, newValue) { |
| let videoOptionsCopy = { ...setting_VideoPlaybackOptions.value }; |
| videoOptionsCopy[propertyName] = newValue; |
| setting_VideoPlaybackOptions.value = videoOptionsCopy; |
|
|
| |
| const info = infos[propertyName]; |
| if (info.forEachElement) { |
| callForEachCallbackOnEachElementInImageList(propertyName, newValue, info); |
| } |
| } |
|
|
| |
| Object.entries(new options_VideoPlayback()).forEach(([propertyName, propertyValue]) => { |
| |
| const propertyValueToUse = propertyName in setting_VideoPlaybackOptions.value ? setting_VideoPlaybackOptions.value[propertyName] : propertyValue; |
| const info = infos[propertyName]; |
| let widget; |
| if (info.widgetType === "checkbox") { |
| let options = new options_LabeledCheckboxToggle(); |
| options.id = `VideoPlaybackOptions.${propertyName}` |
| options.checked = propertyValueToUse; |
| options.labelTextContent = propertyName; |
| options.oninput = (e) => { |
| |
| const newValue = e.target.checked; |
| oninput(propertyName, newValue); |
| } |
| widget = createLabeledCheckboxToggle(options); |
| } else if (info.widgetType === "range") { |
| let options = new options_LabeledSliderRange(); |
| options.id = `VideoPlaybackOptions.${propertyName}` |
| options.value = propertyValueToUse; |
| options.labelTextContent = propertyName; |
| options.bIncludeValueLabel = false; |
| options.min = info?.min ? info.min : options.min; |
| options.max = info?.max ? info.max : options.max; |
| options.step = info?.step ? info.step : options.step; |
| options.oninput = (e) => { |
| |
| const newValue = e.target.valueAsNumber; |
| oninput(propertyName, newValue); |
| } |
| widget = createLabeledSliderRange(options); |
| } else if (info.widgetType === "number") { |
| let options = new options_LabeledNumberInput(); |
| options.id = `VideoPlaybackOptions.${propertyName}` |
| options.value = propertyValueToUse; |
| options.labelTextContent = propertyName; |
| options.min = info?.min ? info.min : options.min; |
| options.max = info?.max ? info.max : options.max; |
| options.step = info?.step ? info.step : options.step; |
| options.onchange = (e) => { |
| |
| const newValue = e.target.valueAsNumber; |
| oninput(propertyName, newValue); |
| } |
| widget = createLabeledNumberInput(options); |
| } |
| widget.title = infos[propertyName]?.tooltip || ""; |
| menu.appendChild(widget); |
| }); |
| } |
|
|
| export function createVideoPlaybackOptionsFlyout() { |
| const handleClassSuffix = ".video-handle"; |
| const menuClassSuffix = ".video-menu"; |
| let flyout = createFlyoutHandle("📽️", handleClassSuffix, menuClassSuffix); |
|
|
| createVideoPlaybackOptionsMenuWidgets(flyout.menu); |
|
|
| return flyout; |
| } |
|
|
| class options_BaseLabeledWidget { |
| id = undefined; |
| labelTextContent = undefined; |
| oninput = undefined; |
| onchange = undefined; |
| bPlaceLabelAfterMainElement = false; |
|
|
| bindEvents(widget) { |
| if (this.oninput) { |
| widget.oninput = this.oninput; |
| } |
|
|
| if (this.onchange) { |
| widget.onchange = this.onchange; |
| } |
|
|
| return widget; |
| } |
| } |
|
|
| export class options_LabeledSliderRange extends options_BaseLabeledWidget { |
| bIncludeValueLabel = true; |
| bPrependValueLabel = false; |
| valueLabelFractionalDigits = 0; |
| value = 0; |
| min = 0; |
| max = 100 |
| step = 1; |
| } |
|
|
| export function createLabeledSliderRange(options = null) { |
|
|
| if (!options) { |
| options = new options_LabeledSliderRange(); |
| } |
|
|
| let valueLabelElement; |
|
|
| let OuterElement = $el("div", { |
| style: { |
| display: "flex", |
| alignItems: "center", |
| gap: "5%" |
| } |
| }); |
|
|
| if (options.bIncludeValueLabel) { |
| valueLabelElement = $el("label", { |
| textContent: options.value?.toFixed(options.valueLabelFractionalDigits) || 0 |
| }); |
|
|
| |
| const originalOnInput = options.oninput; |
|
|
| |
| options.oninput = (e) => { |
| |
| if (originalOnInput && typeof originalOnInput === "function") { |
| originalOnInput(e); |
| } |
|
|
| OuterElement.setLabelTextContent(e.target.value); |
| }; |
| } |
|
|
| let MainElement = $el("input", { |
| id: options.id, |
| type: "range", |
| value: options.value, |
| min: options.min, |
| max: options.max, |
| step: options.step |
| }); |
|
|
| options.bindEvents(MainElement); |
|
|
| const LabelWidget = $el("label", { textContent: options.labelTextContent }); |
|
|
| if (!options.bPlaceLabelAfterMainElement) { |
|
|
| OuterElement.appendChild(LabelWidget); |
| } |
|
|
| if (options.bPrependValueLabel && valueLabelElement) { |
| OuterElement.appendChild(valueLabelElement); |
| } |
| OuterElement.appendChild(MainElement); |
|
|
| if (!options.bPrependValueLabel && valueLabelElement) { |
| OuterElement.appendChild(valueLabelElement); |
| } |
|
|
| if (options.bPlaceLabelAfterMainElement) { |
|
|
| OuterElement.appendChild(LabelWidget); |
| } |
|
|
| OuterElement.getMainElement = function () { |
| return MainElement; |
| }; |
|
|
| OuterElement.setValueDirectly = function (value, bClamp = true) { |
| if (bClamp) { |
| value = utilitiesInstance.clamp(value, options.min, options.max); |
| } |
| MainElement.value = value; |
| OuterElement.setLabelTextContent(value); |
| } |
|
|
| OuterElement.setLabelTextContent = function (value) { |
|
|
| |
| const inputValue = parseFloat(value); |
| const roundedValue = isNaN(inputValue) ? 0.00 : inputValue.toFixed(options.valueLabelFractionalDigits); |
|
|
| |
| valueLabelElement.textContent = roundedValue; |
| } |
|
|
| return OuterElement; |
| } |
|
|
| export class options_LabeledNumberInput extends options_BaseLabeledWidget { |
| value = 0; |
| min = 0; |
| max = 100 |
| step = 1; |
| } |
|
|
| export function createLabeledNumberInput(options = null) { |
|
|
| if (!options) { |
| options = new options_LabeledNumberInput(); |
| } |
|
|
| let MainElement = $el("input", { |
| id: options.id, |
| type: "number", |
| value: options.value, |
| min: options.min, |
| max: options.max, |
| step: options.step |
| }); |
|
|
|
|
| |
| const originalOnInput = options.oninput; |
|
|
| |
| options.oninput = (e) => { |
|
|
| |
| const inputValue = parseFloat(e.target.value); |
| if (isNaN(inputValue)) { |
| e.target.value = MainElement.lastValue ? MainElement.lastValue : 1.00; |
| e.target.select(); |
| } |
|
|
| |
|
|
| MainElement.lastValue = e.target.value; |
|
|
| |
| if (originalOnInput && typeof originalOnInput === "function") { |
| originalOnInput(e); |
| } |
| }; |
|
|
| options.bindEvents(MainElement); |
|
|
| let OuterElement = $el("div", { |
| style: { |
| display: "flex", |
| alignItems: "center", |
| gap: "5%" |
| } |
| }); |
|
|
| const LabelWidget = $el("label", { textContent: options.labelTextContent }); |
|
|
| if (!options.bPlaceLabelAfterMainElement) { |
|
|
| OuterElement.appendChild(LabelWidget); |
| } |
|
|
| OuterElement.appendChild(MainElement); |
|
|
| if (options.bPlaceLabelAfterMainElement) { |
|
|
| OuterElement.appendChild(LabelWidget); |
| } |
|
|
| OuterElement.getMainElement = function () { |
| return MainElement; |
| }; |
|
|
| return OuterElement; |
| } |
|
|
| export class options_LabeledCheckboxToggle extends options_BaseLabeledWidget { |
| checked = false; |
| } |
|
|
| export function createLabeledCheckboxToggle(options = null) { |
|
|
| if (!options) { |
| options = new options_LabeledCheckboxToggle(); |
| } |
|
|
| let MainElement = $el("input", { |
| id: options.id, |
| type: "checkbox", |
| checked: options.checked |
| }); |
|
|
| options.bindEvents(MainElement); |
|
|
| let OuterElement = $el("div", { |
| style: { |
| display: "flex", |
| alignItems: "center" |
| } |
| }); |
|
|
| const LabelWidget = $el("label", { textContent: options.labelTextContent }); |
|
|
| if (!options.bPlaceLabelAfterMainElement) { |
|
|
| OuterElement.appendChild(LabelWidget); |
| } |
|
|
| OuterElement.appendChild(MainElement); |
|
|
| if (options.bPlaceLabelAfterMainElement) { |
|
|
| OuterElement.appendChild(LabelWidget); |
| } |
|
|
| OuterElement.getMainElement = function () { |
| return MainElement; |
| }; |
|
|
| return OuterElement; |
| } |