diff --git a/README.md b/README.md index f339612..65609a2 100644 --- a/README.md +++ b/README.md @@ -14,30 +14,29 @@ Navigate to the **_ComfyUI/custom_nodes_** directory with cmd, and run: `git clone https://github.com/TinyTerra/ComfyUI_tinyterraNodes.git` ### Special Features -**Fullscreen Image Viewer** +**ttN Image Viewer** *Enabled by default* -+
Adds 'Fullscreen 🌏' to the node right-click context menu Opens a Fullscreen image viewer - containing all images generated by the selected node during the current comfy session. -+
Adds 'Set Default Fullscreen Node 🌏' to the node right-click context menu Sets the currently selected node as the default Fullscreen node -+
Adds 'Clear Default Fullscreen Node 🌏' to the node right-click context menu Clears the assigned default Fullscreen node ++
Adds '🌏 Fullscreen Image Viewer' to the node right-click context menu Opens a Fullscreen image viewer - containing all images generated by the selected node during the current comfy session. ++
Adds '🌏 Popout Image Viewer' to the node right-click context menu Opens a Popout image viewer - containing all images generated by the selected node during the current comfy session. ++
Adds '🌏 Set Default Fullscreen Node' to the node right-click context menu Sets the currently selected node as the default Fullscreen node ++
Adds '🌏 Clear Default Fullscreen Node' to the node right-click context menu Clears the assigned default Fullscreen node + Slideshow Mode - + Toggled On - Automatically jumps to New images as they are generated (Black Background) - the UI will auto hide after a set time. + + Toggled On - Automatically jumps to New images as they are generated if the last image is selected (Black Background) - the UI will auto hide after a set time. + Toggled Off - Holds to the current user selected image (Light Background) -+ Fullscreen Overlay ++ UI Overlay + Toggles display of a navigable preview of all of the selected nodes images - + Toggles display of the default comfy menu + + Toggles display of the Image Viewer Settings button + *Shortcuts* - + 'shift + F11' => _Open ttN-Fullscreen using selected node OR default fullscreen node_ - + 'shift + down arrow' (Node Selected) => _Set selected node as default fullscreen node_ - + 'shift + down arrow' (No Node Selected) => _removes default fullscreen node to None_ + + 'shift + F11' => _Open ttN-Fullscreen Image Viewer using selected node OR default fullscreen node_ + + 'shift + F10' => _Open ttN-Popout Image Viewer using selected node OR default fullscreen node_ -+ *Shortcuts in Fullscreen* - + 'shift + F11' => _Close ttN-Fullscreen_ - + 'up arrow' => _Toggle Fullscreen Overlay_ ++ *Shortcuts in Image Viewer* + + 'up arrow' => _Toggle UI Overlay_ + 'down arrow' => _Toggle Slideshow Mode_ + 'left arrow' => _Select Image to the left_ + 'shift + left arrow' => _Select Image 5 to the left_ @@ -49,7 +48,8 @@ Navigate to the **_ComfyUI/custom_nodes_** directory with cmd, and run: + 'ctrl + mouse scroll' => _Select image to Left/Right_ + 'left click + drag' => _Update the current image's position_ + 'double click' => _Reset position of current image_ - + 'esc' => _Close Fullscreen Mode_ + + 'esc' => _Close Image Viewer_ + + 'F' => _Fit image to Viewer window_ + Show UI with mouse hover in Slideshow mode **Advanced XY(Z)Plot** diff --git a/__init__.py b/__init__.py index 73637ee..610e397 100644 --- a/__init__.py +++ b/__init__.py @@ -132,7 +132,7 @@ if os.path.exists(web_extension_path): js_files = { "interface": "enable_interface", - "fullscreen": "enable_fullscreen", + "imgViewer": "enable_fullscreen", "embedAC": "enable_embed_autocomplete", "dynamicWidgets": "enable_dynamic_widgets", } diff --git a/js/ttN.js b/js/ttN.js index 31e0999..ee061a5 100644 --- a/js/ttN.js +++ b/js/ttN.js @@ -1,6 +1,6 @@ import { app } from "../../scripts/app.js"; import { tinyterraReloadNode, wait, rebootAPI, getConfig, convertToInput, hideWidget } from "./utils.js"; -import { openFullscreenApp, _setDefaultFullscreenNode } from "./ttNfullscreen.js"; +import { openFullscreenApp, openPopoutViewer, _setDefaultFullscreenNode } from "./ttNimgViewer.js"; class TinyTerra extends EventTarget { constructor() { @@ -564,15 +564,19 @@ class TinyTerra extends EventTarget { getTinyTerraNodeMenuItems(node) { return [ { - content: "🌏 Fullscreen", + content: "🌏 Fullscreen Image Viewer", callback: () => { openFullscreenApp(node) } + }, + { + content: "🌏 Pop-Out Image Viewer", + callback: () => { openPopoutViewer(node) } }, { - content: "🌏 Set Default Fullscreen Node", + content: "🌏 Set Default Viewer Node", callback: _setDefaultFullscreenNode }, { - content: "🌏 Clear Default Fullscreen Node", + content: "🌏 Clear Default Viewer Node", callback: function () { sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node'); } diff --git a/js/ttNfullscreen.js b/js/ttNfullscreen.js deleted file mode 100644 index 48be28b..0000000 --- a/js/ttNfullscreen.js +++ /dev/null @@ -1,966 +0,0 @@ -import { app } from "../../scripts/app.js"; -import { api } from "../../scripts/api.js"; -import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttNdropdown.js"; - -//GLOBAL CONSTANTS -const FULLSCREEN_WRAPPER_ID = "ttN-FullscreenWrapper"; -const FULLSCREEN_IMAGE_ID = "ttN-FullscreenImage"; -const IMAGE_PREVIEWS_WRAPPER_ID = "ttN-imagePreviewsWrapper"; - -//GLOBAL OPTS -let TTN_AUTOHIDE = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.FullscreenAutohideToggle')) ?? true; -let TTN_INVERTCTRL = JSON.parse(localStorage.getItem('Comfy.Settings.ttN.FullscreenInvertCtrl')) ?? false; - -let TTN_SLIDESHOWMODE = true; - -//GLOBAL ELEMENTS -const TTN_LITEGRAPH = document.getElementsByClassName("litegraph")[0]; -const TTN_COMFYMENU = document.getElementsByClassName("comfy-menu")[0]; -const TTN_COMFYHAMBURGER = document.getElementsByClassName("comfy-menu-hamburger")[0]; -let TTN_FULLSCREEN_WRAPPER; -let TTN_FULLSCREEN_IMAGE = new Image(); -let TTN_PREVIEWS_WRAPPER; - -//GLOBAL VARS -let TTN_isFullscreen = false; -let TTN_isDraggingImage = false; -let TTN_offsetX, TTN_offsetY; - -let TTN_FS_ImageIndex = -1; -let TTN_FullscreenNode = null; -let TTN_ImageScale = 1; - -let TTN_COMPARE1_INDEX; -let TTN_COMPARE2_INDEX; -let TTN_CompareFromImage = null; -let TTN_CompareToImage = null; -let TTN_Comparing = false; - -let hideUI_Timeout; - -let ttN_srcDict = {}; -let ttN_imageElementsDict = {}; - -//COMFY SPECIFIC -loadSrcDict() - -function _stealComfyMenu() { - if (TTN_FULLSCREEN_WRAPPER && TTN_COMFYMENU && TTN_COMFYHAMBURGER) { - TTN_FULLSCREEN_WRAPPER.append(TTN_COMFYMENU) - TTN_FULLSCREEN_WRAPPER.append(TTN_COMFYHAMBURGER) - } -} - -function _replaceComfyMenu() { - TTN_LITEGRAPH.append(TTN_COMFYMENU) - TTN_LITEGRAPH.append(TTN_COMFYHAMBURGER) -} - -function _getSelectedNode() { - const graphcanvas = LGraphCanvas.active_canvas; - if (graphcanvas.selected_nodes && Object.keys(graphcanvas.selected_nodes).length === 1) { - return Object.values(graphcanvas.selected_nodes)[0]; - } - return null; -} - -export function _setDefaultFullscreenNode() { - let selectedNode = _getSelectedNode(); - if (selectedNode) { - sessionStorage.setItem('Comfy.Settings.ttN.default_fullscreen_node', JSON.stringify(selectedNode.id)); - } else { - sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node'); - } -} - -function clearSrcDict() { - ttN_srcDict = {}; - sessionStorage.removeItem('ttN_srcDict'); -} - -//get the src list -function _findFullImageSRC(node) { - if (node.imgs) { - let img = node.imgs.find(imgElement => imgElement.src.includes("filename")); - return img ? img.src : null; - } - return null; -} - -function _findLatentPreviewImageSRC(node) { - if (!node.imgs) return null; - - if (node.imageIndex !== null && node.imageIndex < node.imgs.length) { - return node.imgs[node.imageIndex].src; - } else if (node.overIndex !== null && node.overIndex < node.imgs.length) { - return node.imgs[node.overIndex].src; - } - return null; -} - -function _removeLatentPreviewImageSRC(node, imgSrc) { - let latentPreviewSrc = _findLatentPreviewImageSRC(node); - if (imgSrc && latentPreviewSrc) { - for (let i in node.imgs) { - if(!node.imgs[i].src.includes("filename")) { - node.imgs.splice(i, 1); - } - } - } -} - -function _handleLatentPreview(imgDivList) { - let latentPreview = _findLatentPreviewImageSRC(TTN_FullscreenNode); - if (latentPreview && !latentPreview.includes("filename") && TTN_FS_ImageIndex === imgDivList.length - 1) { - TTN_FULLSCREEN_IMAGE.src = latentPreview - } -} - -function clearCompareImages() { - if (!TTN_Comparing) return; - TTN_CompareToImage?.classList.remove('ttN-compare-to') - TTN_CompareFromImage?.classList.remove('ttN-compare-from') - TTN_CompareFromImage = null - TTN_CompareToImage = null - TTN_Comparing = false -} - -function addPreviewImageDropdown(imgElement) { - let suggestions = ['Load Prompt from Image', 'Select for Compare'] - - imgElement.addEventListener('contextmenu', (e) => { - e.preventDefault(); - - if (TTN_CompareFromImage && !suggestions.includes('Compare With Selected')) { - suggestions.push('Compare With Selected') - } else if (!TTN_CompareFromImage && suggestions.includes('Compare With Selected')) { - suggestions.splice(suggestions.indexOf('Compare With Selected'), 1) - } - - if (TTN_Comparing && !suggestions.includes('Clear Compare')) { - suggestions.push('Clear Compare') - } else if (!TTN_Comparing && suggestions.includes('Clear Compare')) { - suggestions.splice(suggestions.indexOf('Clear Compare'), 1) - } - - const manualOffset = ['80%', '70%']; - ttN_CreateDropdown(imgElement, suggestions, async (s) => { - if (s === 'Load Prompt from Image') { - imgElement.classList.add('ttN-loadToGraph') - app.handleFile(await (await fetch(imgElement.src)).blob()) - setTimeout(() => { - imgElement.classList.remove('ttN-loadToGraph') - }, 350) - } - if (s === 'Delete Image') { - console.log("Delete Image") - } - if (s === 'Select for Compare') { - if (TTN_CompareFromImage) { - TTN_CompareFromImage.classList.remove('ttN-compare-from') - } - imgElement.classList.add('ttN-compare-from') - TTN_CompareFromImage = imgElement - TTN_COMPARE1_INDEX = imgElement.index - } - if (s === 'Compare With Selected') { - if (!TTN_CompareFromImage) { - return; - } - - if (TTN_CompareToImage) { - TTN_CompareToImage.classList.remove('ttN-compare-to') - } - - imgElement.classList.add('ttN-compare-to') - TTN_CompareToImage = imgElement - TTN_COMPARE2_INDEX = imgElement.index - - TTN_Comparing = true - _setCurrentImageIndex(TTN_COMPARE2_INDEX) - } - if (s === 'Clear Compare') { - clearCompareImages() - } - - }, false, manualOffset, TTN_FULLSCREEN_WRAPPER); - - }); -} - -function _createImageDivFromSrc(imgSrc, index) { - // If image element doesn't exist, create it - if (!ttN_imageElementsDict[imgSrc]) { - const imgWrapper = document.createElement('div'); - imgWrapper.classList.add('ttN-imgWrapper'); - - const imgElement = document.createElement('img'); - imgElement.src = imgSrc; - imgElement.index = index; - imgElement.classList.add('ttN-img'); - imgWrapper.appendChild(imgElement); - - imgElement.addEventListener('click', () => { - _setCurrentImageIndex(index); - }); - - // right click - addPreviewImageDropdown(imgElement); - - ttN_imageElementsDict[imgSrc] = imgWrapper; - } - return ttN_imageElementsDict[imgSrc]; -} - -function updateImageElements(indexOverride = null) { - if (!TTN_isFullscreen) return; - - const srcList = ttN_srcDict[TTN_FullscreenNode.id] || null; - if (!srcList) return; - - if (!TTN_FULLSCREEN_WRAPPER) return - - const imgDivList = srcList.map((src, index) => _createImageDivFromSrc(src, index)); - - TTN_FS_ImageIndex = indexOverride || TTN_FS_ImageIndex - if ((TTN_FS_ImageIndex > imgDivList.length - 1) || (TTN_FS_ImageIndex === -1)) { - TTN_FS_ImageIndex = imgDivList.length - 1 - } - if (TTN_FS_ImageIndex < -1) TTN_FS_ImageIndex = 0 - - TTN_FULLSCREEN_IMAGE.src = imgDivList[TTN_FS_ImageIndex].children[0].src; - - if (!TTN_PREVIEWS_WRAPPER) return - - if (TTN_SLIDESHOWMODE) { - TTN_FULLSCREEN_WRAPPER.classList.add('ttN-slideshow') - _handleLatentPreview(imgDivList); - } else { - TTN_FULLSCREEN_WRAPPER.classList.remove('ttN-slideshow') - } - - if (TTN_FS_ImageIndex > imgDivList.length - 1) TTN_FS_ImageIndex = imgDivList.length - 1; - if (TTN_FS_ImageIndex < 0) TTN_FS_ImageIndex = 0; - - imgDivList.forEach((imgDiv, index) => { - if (TTN_PREVIEWS_WRAPPER.children[index] != imgDiv) TTN_PREVIEWS_WRAPPER.appendChild(imgDiv); - - const orderValue = index - TTN_FS_ImageIndex; - imgDiv.style.order = orderValue; - - if (index < TTN_FS_ImageIndex) { - // For images before the selected image - imgDiv.classList.remove('ttN-divSelected', 'ttN-divAfter'); - imgDiv.children[0].classList.remove('ttN-imgSelected', 'ttN-imgAfter'); - - imgDiv.classList.add('ttN-divBefore'); - //imgDiv.children[0].classList.add('ttN-imgBefore'); - } - else if (index === TTN_FS_ImageIndex) { - // For the selected image - imgDiv.classList.remove('ttN-divBefore', 'ttN-divAfter'); - imgDiv.children[0].classList.remove('ttN-imgBefore', 'ttN-imgAfter'); - - imgDiv.classList.add('ttN-divSelected'); - imgDiv.children[0].classList.add('ttN-imgSelected'); - } - else if (index > TTN_FS_ImageIndex) { - // For images after the selected image - imgDiv.classList.remove('ttN-divSelected', 'ttN-divBefore'); - imgDiv.children[0].classList.remove('ttN-imgSelected', 'ttN-imgBefore'); - - imgDiv.classList.add('ttN-divAfter'); - //imgDiv.children[0].classList.add('ttN-imgAfter'); - } - }); - - _applyTranslation(TTN_PREVIEWS_WRAPPER, TTN_FS_ImageIndex, imgDivList); -} - -function updateImageTLDE() { - for (let node of app.graph._nodes) { - if (!node.imgs) continue - - let imgSrc = _findFullImageSRC(node); - if (!imgSrc) continue; - - _removeLatentPreviewImageSRC(node, imgSrc) - - ttN_srcDict[node.id] = ttN_srcDict[node.id] || []; - - let index = ttN_srcDict[node.id].length; - - if (!ttN_srcDict[node.id].includes((index, imgSrc))) { - ttN_srcDict[node.id].push((index, imgSrc)); - if (TTN_SLIDESHOWMODE) { - updateImageElements(index); - } - } - - } - saveSrcDict(); - updateImageElements(); -}; - -function saveSrcDict() { - sessionStorage.setItem('ttN_srcDict', JSON.stringify(ttN_srcDict)); -} - -function loadSrcDict() { - const savedData = sessionStorage.getItem('ttN_srcDict'); - if (savedData) { - ttN_srcDict = JSON.parse(savedData); - } -} - -function removeDictEntry(node_id, index) { - delete ttN_srcDict[node_id][index]; - //TODO -} - -function _appendFullscreenMenuButtons() { - const ttN_hr = document.createElement('hr'); - ttN_hr.style.margin = '20px 0px'; - ttN_hr.style.width = '100%'; - ttN_hr.classList.add('ttN-hr'); - if (TTN_COMFYMENU) { - TTN_COMFYMENU.appendChild(ttN_hr); - } - - // Toggle hover auto-hide button - const toggleHoverButton = document.createElement('button'); - toggleHoverButton.innerHTML = 'Toggle Hover Auto-hide'; - toggleHoverButton.classList.add('ttN-toggle-hover-button'); - if (TTN_AUTOHIDE == true) { - toggleHoverButton.classList.add('ttN-true'); - } - - toggleHoverButton.addEventListener('click', function() { - if (TTN_AUTOHIDE == true) { - localStorage.setItem('Comfy.Settings.ttN.FullscreenAutohideToggle', 'false'); - TTN_AUTOHIDE = false; - - toggleHoverButton.classList.remove('ttN-true'); - __reset_hideUI_Timeout(); - } else { - localStorage.setItem('Comfy.Settings.ttN.FullscreenAutohideToggle', 'true'); - TTN_AUTOHIDE = true; - - toggleHoverButton.classList.add('ttN-true'); - __reset_hideUI_Timeout(); - } - }); - if (TTN_COMFYMENU) { - TTN_COMFYMENU.appendChild(toggleHoverButton); - } - - // Toggle slideshow mode button - const toggleSlideshowButton = document.createElement('button'); - toggleSlideshowButton.innerHTML = 'Toggle Slideshow Mode'; - toggleSlideshowButton.classList.add('ttN-slideshow-button'); - if (TTN_SLIDESHOWMODE) { - toggleSlideshowButton.classList.add('ttN-true'); - } - - toggleSlideshowButton.addEventListener('click', function() { - TTN_SLIDESHOWMODE = !TTN_SLIDESHOWMODE; - if (TTN_SLIDESHOWMODE) { - toggleSlideshowButton.classList.add('ttN-true'); - TTN_FULLSCREEN_WRAPPER.classList.add('ttN-slideshow') - } else { - toggleSlideshowButton.classList.remove('ttN-true'); - TTN_FULLSCREEN_WRAPPER.classList.remove('ttN-slideshow') - } - }); - if (TTN_COMFYMENU) { - TTN_COMFYMENU.appendChild(toggleSlideshowButton); - } - - // Invert Ctrl button - const invertCtrlButton = document.createElement('button'); - invertCtrlButton.classList.add('ttN-invert-ctrl-button'); - - if (TTN_INVERTCTRL == true) { - invertCtrlButton.classList.add('ttN-true'); - invertCtrlButton.innerHTML = 'Ctrl+Wheel: Scroll Images'; - } else { - invertCtrlButton.classList.remove('ttN-true'); - invertCtrlButton.innerHTML = 'Ctrl+Wheel: Zoom'; - } - - invertCtrlButton.addEventListener('click', function() { - if (TTN_INVERTCTRL == true) { - localStorage.setItem('Comfy.Settings.ttN.FullscreenInvertCtrl', 'false') - invertCtrlButton.classList.remove('ttN-true'); - TTN_INVERTCTRL = false - invertCtrlButton.innerHTML = 'Ctrl+Wheel: Zoom'; - } else { - localStorage.setItem('Comfy.Settings.ttN.FullscreenInvertCtrl', 'true') - invertCtrlButton.classList.add('ttN-true'); - TTN_INVERTCTRL = true - invertCtrlButton.innerHTML = 'Ctrl+Wheel: Scroll Images'; - } - }); - if (TTN_COMFYMENU) { - TTN_COMFYMENU.appendChild(invertCtrlButton); - } -} - -function _removeFullscreenMenuButtons() { - const ttN_hr = document.querySelector('.ttN-hr'); - if (ttN_hr) { - ttN_hr.remove(); - } - const toggleHoverButton = document.querySelector('.ttN-toggle-hover-button'); - if (toggleHoverButton) { - toggleHoverButton.remove(); - } - const toggleSlideshowButton = document.querySelector('.ttN-slideshow-button'); - if (toggleSlideshowButton) { - toggleSlideshowButton.remove(); - } - const invertCtrlButton = document.querySelector('.ttN-invert-ctrl-button'); - if (invertCtrlButton) { - invertCtrlButton.remove(); - } -} - -//EVENT LISTENERS -function enable_document_listeners(toggle=true) { - document.removeEventListener("keydown", _handleKeyPress_doc, true); - if (toggle==true) { - document.addEventListener("keydown", _handleKeyPress_doc, true); - } -} - -function enable_api_listeners(toggle=true) { - api.removeEventListener("status", _handleExecutedEvent); - api.removeEventListener("progress", _handleExecutedEvent); - api.removeEventListener("execution_cached", _handleExecutedEvent); - api.removeEventListener("reconnecting", _handleReconnectingEvent); - if (toggle==true) { - api.addEventListener("status", _handleExecutedEvent); - api.addEventListener("progress", _handleExecutedEvent); - api.addEventListener("execution_cached", _handleExecutedEvent); - api.addEventListener("reconnecting", _handleReconnectingEvent); - } -} - -function enable_wrapper_listeners(toggle=true) { - TTN_FULLSCREEN_WRAPPER.removeEventListener("wheel", _handleMouse); - TTN_FULLSCREEN_WRAPPER.removeEventListener('mousemove', _handleMouse); - TTN_FULLSCREEN_WRAPPER.removeEventListener('mousedown', _handleMouse); - TTN_FULLSCREEN_WRAPPER.removeEventListener('mouseup', _handleMouse); - TTN_FULLSCREEN_WRAPPER.removeEventListener('click', _handleMouse); - TTN_FULLSCREEN_WRAPPER.removeEventListener('dblclick', _handleMouse); - if (toggle==true) { - TTN_FULLSCREEN_WRAPPER.addEventListener("wheel", _handleMouse); - TTN_FULLSCREEN_WRAPPER.addEventListener('mousemove', _handleMouse); - TTN_FULLSCREEN_WRAPPER.addEventListener('mousedown', _handleMouse); - TTN_FULLSCREEN_WRAPPER.addEventListener('mouseup', _handleMouse); - TTN_FULLSCREEN_WRAPPER.addEventListener('click', _handleMouse); - TTN_FULLSCREEN_WRAPPER.addEventListener('dblclick', _handleMouse); - } -} - - -//EVENT HANDLERS -function _handleKeyPress_doc(e) { - if (TTN_isFullscreen) { - const imageList = ttN_srcDict[TTN_FullscreenNode.id] || []; //TODO: ADD IMAGE LIST - - switch (e.code) { - case 'ArrowLeft': - e.preventDefault(); - if (TTN_Comparing) { - _setCurrentImageIndex(Math.min(TTN_COMPARE1_INDEX, TTN_COMPARE2_INDEX)) - break; - } - - if (e.ctrlKey) { - _setCurrentImageIndex(0); - break; - } - - if (e.shiftKey) { - _setCurrentImageIndex(5, "-"); - if (TTN_FS_ImageIndex < 0) _setCurrentImageIndex(0); - break; - } - - if (TTN_FS_ImageIndex == 0) { - _setCurrentImageIndex(0); - } else { - _setCurrentImageIndex(1, "-"); - } - - __reset_hideUI_Timeout(); - break; - - case 'ArrowRight': - e.preventDefault(); - if (TTN_Comparing) { - _setCurrentImageIndex(Math.max(TTN_COMPARE1_INDEX, TTN_COMPARE2_INDEX)) - break; - } - - if (e.ctrlKey) { - _setCurrentImageIndex(-1); - break; - } - - if (e.shiftKey) { - _setCurrentImageIndex(5, "+"); - if (TTN_FS_ImageIndex > imageList.length - 1) _setCurrentImageIndex(-1); - break; - } - - _setCurrentImageIndex(1, "+"); - if (TTN_FS_ImageIndex > imageList.length - 1) _setCurrentImageIndex(-1); - __reset_hideUI_Timeout(); - break; - - case 'ArrowUp': - e.preventDefault(); - __toggleUI(); - break; - - case 'ArrowDown': - e.preventDefault(); - TTN_SLIDESHOWMODE = !TTN_SLIDESHOWMODE; - - if (TTN_SLIDESHOWMODE) { - __hideUI(); - _setCurrentImageIndex(-1); - } else { - __showUI(); - } - break; - - case 'Escape': - case 'F11': - e.preventDefault(); - closeFullscreenApp(); - return; - - } - updateImageElements(); - _applyTranslation(TTN_PREVIEWS_WRAPPER, TTN_FS_ImageIndex, imageList); - return; - }; - - if ((e.code === 'F11' && e.shiftKey) && !e.ctrlKey) { - let selected_node = _getSelectedNode(); - if (selected_node) { - openFullscreenApp(selected_node); - return; - } - - let defaultNodeID = JSON.parse(sessionStorage.getItem('Comfy.Settings.ttN.default_fullscreen_node')); - if (defaultNodeID) { - let defaultNode = app.graph._nodes_by_id[defaultNodeID]; - if (defaultNode) { - TTN_SLIDESHOWMODE = true - openFullscreenApp(defaultNode); - return - } - } - } - if ((e.code === 'ArrowDown' && e.shiftKey) && !e.ctrlKey) { - setDefaultNode(); - } - return -} - -function _handleExecutedEvent(e) { - setTimeout(updateImageTLDE, 500); - setTimeout(updateImageTLDE, 1000); -} - -function _handleReconnectingEvent(e) { - clearSrcDict(); - sessionStorage.removeItem('Comfy.Settings.ttN.default_fullscreen_node'); - ttN_imageElementsDict = {}; -} - -function _handleWheelEvent(e) { - const isMouseOverPreviewWrapper = _isMouseOverElement(TTN_PREVIEWS_WRAPPER, e.clientX, e.clientY); - const invertMenuScrolling = localStorage.getItem('Comfy.Settings.Comfy.InvertMenuScrolling') === "true"; - const deltaYSign = e.deltaY > 0 ? 1 : -1; - const zoomDirection = invertMenuScrolling ? deltaYSign === -1 ? "+" : "-" : deltaYSign === 1 ? "+" : "-"; - const scrollDirection = invertMenuScrolling ? deltaYSign === -1 ? "-" : "+" : deltaYSign === 1 ? "-" : "+"; - e.preventDefault(); - - if (((TTN_INVERTCTRL && !e.ctrlKey) || (e.ctrlKey && !TTN_INVERTCTRL)) && !isMouseOverPreviewWrapper) { - // SCALE IMAGE - _scrollScaleImage(e, TTN_FULLSCREEN_IMAGE, zoomDirection); - } - if ((!TTN_INVERTCTRL && !e.ctrlKey) || (e.ctrlKey && TTN_INVERTCTRL) || isMouseOverPreviewWrapper) { - //SCROLL PREVIEWS - if (scrollDirection === "-" && TTN_FS_ImageIndex <= 0) return; - - if (TTN_Comparing && scrollDirection === "-") { - _setCurrentImageIndex(Math.min(TTN_COMPARE1_INDEX, TTN_COMPARE2_INDEX)) - } else if (TTN_Comparing && scrollDirection === "+") { - _setCurrentImageIndex(Math.max(TTN_COMPARE1_INDEX, TTN_COMPARE2_INDEX)) - } else { - _setCurrentImageIndex(1, scrollDirection); - } - - if (TTN_FS_ImageIndex < 0) _setCurrentImageIndex(0); - } - const imageList = ttN_srcDict[TTN_FullscreenNode.id] || []; - _applyTranslation(TTN_PREVIEWS_WRAPPER, TTN_FS_ImageIndex, imageList); -} - -function _handleMouse(e) { - const isMouseOverPreviewWrapper = _isMouseOverElement(TTN_PREVIEWS_WRAPPER, e.clientX, e.clientY); - const isMouseOverMenu = _isMouseOverElement(TTN_COMFYMENU, e.clientX, e.clientY) || _isMouseOverElement(TTN_COMFYHAMBURGER, e.clientX, e.clientY); - const isMouseOverImage = _isMouseOverElement(TTN_FULLSCREEN_IMAGE, e.clientX, e.clientY); - - switch (e.type) { - case "wheel": - _handleWheelEvent(e); - break; - - case "mousedown": - if (!isMouseOverMenu && !isMouseOverPreviewWrapper) { - e.preventDefault(); - TTN_isDraggingImage = true - TTN_offsetX = TTN_FULLSCREEN_IMAGE.offsetLeft - e.clientX; - TTN_offsetY = TTN_FULLSCREEN_IMAGE.offsetTop - e.clientY; - } - break; - - case "mouseup": - TTN_isDraggingImage = false - break; - - case "click": - if (!isMouseOverMenu && !isMouseOverPreviewWrapper && TTN_SLIDESHOWMODE) { - __hideUI(); - } - break; - - case "dblclick": - if (!isMouseOverMenu && !isMouseOverPreviewWrapper) { - e.preventDefault(); - e.stopPropagation(); - - TTN_FULLSCREEN_IMAGE.style.left = ""; - TTN_FULLSCREEN_IMAGE.style.top = ""; - _setCurrentImageScale(TTN_FULLSCREEN_IMAGE, 1); - } - break; - - case "mousemove": - if (TTN_SLIDESHOWMODE) { - if (isMouseOverPreviewWrapper || isMouseOverMenu) { - __showUI(); - __reset_hideUI_Timeout(7000); - } - } - if (TTN_isDraggingImage) { - TTN_FULLSCREEN_IMAGE.style.left = (e.clientX + TTN_offsetX) + 'px'; - TTN_FULLSCREEN_IMAGE.style.top = (e.clientY + TTN_offsetY) + 'px'; - } - break; - } -} - -//UI FUNCTIONS -function createElement(tagName, attributes = {}, parentElement = document.body) { - if (attributes.id && document.getElementById(attributes.id)) { - return document.getElementById(attributes.id); - } - const element = document.createElement(tagName); - Object.entries(attributes).forEach(([key, value]) => { - element.setAttribute(key, value); - }); - parentElement.appendChild(element); - return element; -} - -function _createFullscreenBody() { - TTN_FULLSCREEN_WRAPPER = createElement("div", { id: FULLSCREEN_WRAPPER_ID, class: 'ttN-slideshow' }); - TTN_FULLSCREEN_WRAPPER.onfullscreenchange = function (event) { - if (!document.fullscreenElement) { - closeFullscreenApp(); - } - }; - TTN_PREVIEWS_WRAPPER = createElement("div", { id: IMAGE_PREVIEWS_WRAPPER_ID }, TTN_FULLSCREEN_WRAPPER); - - TTN_FULLSCREEN_IMAGE.src = _findFullImageSRC(TTN_FullscreenNode) || _findLatentPreviewImageSRC(TTN_FullscreenNode) || ''; - TTN_FULLSCREEN_IMAGE.id = FULLSCREEN_IMAGE_ID; - TTN_FULLSCREEN_WRAPPER.appendChild(TTN_FULLSCREEN_IMAGE); -} - -function _initiateFullscreen(Element) { - if (Element.requestFullscreen) { - return Element.requestFullscreen(); - } else if (Element.mozRequestFullScreen) { - return Element.mozRequestFullScreen(); - } else if (Element.webkitRequestFullscreen) { - return Element.webkitRequestFullscreen(); - } else if (Element.msRequestFullscreen) { - return Element.msRequestFullscreen(); - } -} - -export function openFullscreenApp(node) { - if (TTN_isFullscreen) return - - TTN_FullscreenNode = node; - - _createFullscreenBody(); - enable_wrapper_listeners(); - _stealComfyMenu(); - _appendFullscreenMenuButtons(); - __hideUI(); - - TTN_FULLSCREEN_IMAGE.style.left = ""; - TTN_FULLSCREEN_IMAGE.style.top = ""; - _setCurrentImageScale(TTN_FULLSCREEN_IMAGE, 1); - TTN_FS_ImageIndex = -1; - clearCompareImages(); - - - _initiateFullscreen(TTN_FULLSCREEN_WRAPPER); - TTN_isFullscreen = true; - updateImageTLDE(); -} - -function closeFullscreenApp() { - if (!TTN_isFullscreen) return - __showUI(); - clearTimeout(hideUI_Timeout); - _replaceComfyMenu(); - _removeFullscreenMenuButtons(); - - enable_wrapper_listeners(false); - - document.body.removeChild(TTN_FULLSCREEN_WRAPPER); - - TTN_isFullscreen = false; -} - -function _scrollScaleImage(e, image, zoomDirection, partner=null) { - let scaleFactor = 0.14; - if (TTN_ImageScale > 1) scaleFactor = scaleFactor * 2 - if (TTN_ImageScale > 3) scaleFactor = scaleFactor * 2 - if (TTN_ImageScale > 5) scaleFactor = scaleFactor * 2 - if (TTN_ImageScale > 7) scaleFactor = scaleFactor * 2 - if (TTN_ImageScale < 0.14) scaleFactor = scaleFactor / 2 - - const rect = image.getBoundingClientRect(); - - // Step 1: Get the Mouse's Position Relative to the Image - const offsetX = e.clientX - rect.left; - const offsetY = e.clientY - rect.top; - const relativeX = offsetX / rect.width; - const relativeY = offsetY / rect.height; - - // Step 2: Zoom - _setCurrentImageScale(image, scaleFactor, zoomDirection); - - const newRect = image.getBoundingClientRect(); - - // Step 3: Adjust the Image's Position - const newX = e.clientX - relativeX * newRect.width; - const newY = e.clientY - relativeY * newRect.height; - image.style.left = (image.offsetLeft + newX - newRect.left) + 'px'; - image.style.top = (image.offsetTop + newY - newRect.top) + 'px'; - -} - -function _setCurrentImageScale(image, s, symbol=null) { - switch (symbol) { - case "+": - s = TTN_ImageScale + s; - break; - case "-": - s = TTN_ImageScale - s; - break; - } - if (s < 0.01) s = 0.01 - if (s > 9) s = 9 - // round s to 2 decimal places - s = Math.round(s * 100) / 100 - TTN_ImageScale = s - image.style.transform = "scale(" + TTN_ImageScale + ")"; -} - -function _applyTranslation(Element, Index, List) { - let translationConst = (Index / (List.length - 1)) * 100; - - translationConst = translationConst - (0.5 / (List.length - 1)) * 100 - - if (Index === 0) { translationConst = 0; } - Element.style.transform = 'translateX(-' + translationConst + '%)'; -} - -function _isMouseOverElement(element, mouseX, mouseY) { - if (!element) return false; - const rect = element.getBoundingClientRect(); - return ( - mouseX >= rect.left && - mouseX <= rect.right && - mouseY >= rect.top && - mouseY <= rect.bottom - ); -} - -function __hideUI() { - if (TTN_COMFYMENU) TTN_COMFYMENU.classList.add('hidden') - if (TTN_COMFYHAMBURGER) TTN_COMFYHAMBURGER.classList.add('hidden'); - if (TTN_PREVIEWS_WRAPPER) TTN_PREVIEWS_WRAPPER.classList.add('hidden'); - ttN_RemoveDropdown(); -} - -function __showUI() { - if(TTN_COMFYMENU) TTN_COMFYMENU.classList.remove('hidden') - if (TTN_COMFYHAMBURGER) TTN_COMFYHAMBURGER.classList.remove('hidden'); - if (TTN_PREVIEWS_WRAPPER) TTN_PREVIEWS_WRAPPER.classList.remove('hidden'); - - __reset_hideUI_Timeout(); -} - -function __toggleUI() { - if (TTN_COMFYMENU) TTN_COMFYMENU.classList.toggle('hidden') - if (TTN_COMFYHAMBURGER) TTN_COMFYHAMBURGER.classList.toggle('hidden'); - if (TTN_PREVIEWS_WRAPPER) TTN_PREVIEWS_WRAPPER.classList.toggle('hidden'); - - __reset_hideUI_Timeout(); -} - -function __reset_hideUI_Timeout(timeout=3700) { - clearTimeout(hideUI_Timeout); - - hideUI_Timeout = setTimeout(() => { - if (TTN_SLIDESHOWMODE && TTN_AUTOHIDE) { - __hideUI(); - } - }, timeout); -} - -function _setCurrentImageIndex(i, symbol=null) { - switch (symbol) { - case null: - TTN_FS_ImageIndex = i; - break - case "+": - TTN_FS_ImageIndex += i; - break - case "-": - TTN_FS_ImageIndex -= i; - break - } - updateImageElements(); -} - -enable_document_listeners(); -enable_api_listeners(); - - -var styleElement = document.createElement("style"); -const cssCode = ` -.hidden { - transition: opacity 0.5s, visibility 0.5s, transform 0.2s ease!important; - opacity: 0!important; - visibility: hidden!important; -} - -#ttN-FullscreenWrapper { - display: flex; - justify-content: center; - align-items: end; - background-color: #1f1f1f; -} - -#ttN-FullscreenImage { - height: inherit; - position: absolute; -} - -#ttN-Compare1Image, #ttN-Compare2Image { - object-fit: contain; -} - -#ttN-imagePreviewsWrapper { - display: flex; - position: absolute; - width: max-content; - z-index: 10; - height: 14vh; - left: 50vw; - opacity: 1; - visibility: visible; - transition: transform 0.2s, visibility 0.2s ease; -} - -#ttN-CompareWrapper { - display: flex; - position: absolute; - width: 100vw; - height: 100vh; - z-index: 1; -} - -#ttN-Compare1, #ttN-Compare2 { - width: 50vw; - height: 100vh; - display: flex; - justify-content: center; -} - -.ttN-imgWrapper { - position: sticky; - transition: transform 0.2s ease; - align-self: end; -} - -.ttN-img { - height: 121px; - margin: 7px; - cursor: pointer; - display: block; - border: 3px solid rgba(255,255,255); - box-shadow: 0px 0px 0px 10px; - transition: all 0.4s ease; -} - -.ttN-img:hover { - transform: scale(1.1); - z-index: 10; -} - -.ttN-imgSelected { - height: 200px!important; - z-index: 10; - transition: 0.1s; -} -.ttN-slideshow { - background: black!important; -} - -.ttN-true { - color: green!important; -} - -.ttN-compare-from { - border: 7px solid cyan; -} - -.ttN-compare-to { - border: 7px solid red; -} - -.ttN-loadToGraph { - height: 0px!important; - transition: 0.2s ease-in-out; -} - -`; - -styleElement.innerHTML = cssCode -document.head.appendChild(styleElement); \ No newline at end of file diff --git a/js/ttNimgViewer.js b/js/ttNimgViewer.js new file mode 100644 index 0000000..72df696 --- /dev/null +++ b/js/ttNimgViewer.js @@ -0,0 +1,1125 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; +import { ttN_CreateDropdown, ttN_RemoveDropdown } from "./ttNdropdown.js"; + +/* ========================================================= + GLOBAL IMAGE STORE +========================================================= */ + +let ttN_srcDict = {}; +loadSrcDict(); +const TTN_POPOUTS = new Set(); +const MAX_HISTORY_PER_NODE = 100; +const STORAGE_KEYS = { + AUTOHIDE: 'Comfy.Settings.ttN.FullscreenAutohideToggle', + INVERT: 'Comfy.Settings.ttN.FullscreenInvertCtrl', + FITSCREEN: 'Comfy.Settings.ttN.FitScreenToggle', + DEFAULTNODE: 'Comfy.Settings.ttN.default_fullscreen_node' +}; + +function loadSrcDict() { + const saved = localStorage.getItem("ttN_srcDict"); + if (saved) ttN_srcDict = JSON.parse(saved); +} + +function saveSrcDict() { + localStorage.setItem("ttN_srcDict", JSON.stringify(ttN_srcDict)); +} + +function _findFullImageSRC(node) { + if (!node.imgs) return null; + const img = node.imgs.find(i => i.src.includes("filename")); + return img ? img.src : null; +} + +function _findLatentPreviewImageSRC(node) { + if (!node.imgs) return null; + + if (node.imageIndex != null && + node.imageIndex < node.imgs.length) { + return node.imgs[node.imageIndex].src; + } + + if (node.overIndex != null && + node.overIndex < node.imgs.length) { + return node.imgs[node.overIndex].src; + } + + return null; +} + +function updateImageTLDE() { + for (let node of app.graph._nodes) { + if (!node.imgs) continue; + + const finalSrc = _findFullImageSRC(node); + const latentSrc = _findLatentPreviewImageSRC(node); + + ttN_srcDict[node.id] = ttN_srcDict[node.id] || []; + + let previousLength = ttN_srcDict[node.id].length; + + if ( + finalSrc && + finalSrc.includes("filename") && + !ttN_srcDict[node.id].includes(finalSrc) + ) { + ttN_srcDict[node.id].push(finalSrc); + + // CAP HISTORY + if (ttN_srcDict[node.id].length > MAX_HISTORY_PER_NODE) { + ttN_srcDict[node.id].shift(); + } + } + + const viewers = + [...TTNViewer.instances] + .filter(v => v.node.id === node.id); + + for (const viewer of viewers) { + + const wasLast = viewer.imageIndex === previousLength - 1; + + if (finalSrc && wasLast && viewer.slideshow) { + viewer.setImage(-1); + continue; + } + + if ( + viewer.slideshow && + wasLast && + latentSrc && + !latentSrc.includes("filename") && + !finalSrc + ) { + viewer.image.src = latentSrc; + } + } + } + + const validNodeIds = new Set(app.graph._nodes.map(n => n.id)); + if (validNodeIds.size > 0) { + Object.keys(ttN_srcDict).forEach(id => { + if (!validNodeIds.has(Number(id))) { + delete ttN_srcDict[id]; + } + }); + } + + saveSrcDict(); + + TTNViewer.instances.forEach(v => v.refreshImages()); +} + +let _updateScheduled = null; + +function scheduleImageUpdate(delay = 300) { + if (_updateScheduled) return; + + _updateScheduled = setTimeout(() => { + updateImageTLDE(); + _updateScheduled = null; + }, delay); +} + +function _handleExecutedEvent(e) { + scheduleImageUpdate(500); +} + +function clearSrcDict() { + ttN_srcDict = {}; + saveSrcDict(); +} + +function _handleReconnectingEvent(e) { + clearSrcDict(); + localStorage.removeItem(STORAGE_KEYS.DEFAULTNODE); +} + + +api.addEventListener("status", _handleExecutedEvent); +api.addEventListener("progress", _handleExecutedEvent); +api.addEventListener("execution_cached", _handleExecutedEvent); +api.addEventListener("reconnecting", _handleReconnectingEvent); + +/* ========================================================= + VIEWER ENGINE +========================================================= */ + +class TTNViewer { + static instances = new Set(); + static fullscreenInstance = null; + + constructor(node, doc, mode = "fullscreen") { + this.node = node; + this.doc = doc; + this.mode = mode; + + this.imageIndex = -1; + + // Compare state + this.compareBase = null; + this.compareTarget = null; + this.comparing = false; + + // Transform + this.scale = 1; + this.offsetX = 0; + this.offsetY = 0; + this.dragging = false; + this.dragStartX = 0; + this.dragStartY = 0; + + this.autohide = JSON.parse(localStorage.getItem(STORAGE_KEYS.AUTOHIDE)) ?? true; + this.invertctrl = JSON.parse(localStorage.getItem(STORAGE_KEYS.INVERT)) ?? false; + this.fitscreentoggle = JSON.parse(localStorage.getItem(STORAGE_KEYS.FITSCREEN)) ?? true; + + this.slideshow = true; + this.hideTimeout = null; + + this._lastWheelTime = 0; + this._resizeObserver = null; + this._wheelOptions = { passive: false }; + this._lastSignature = null; + + TTNViewer.instances.add(this); + if (this.mode === "popout") { + window.addEventListener("storage", () => { + this.refreshImages(); + }); + } + this.init(); + } + + /* ================= INIT ================= */ + + init() { + this.injectCSS(); + this.createLayout(); + this.attachEvents(); + this.refreshImages(); + + if (this.mode === "fullscreen") this.wrapper.requestFullscreen(); + } + + injectCSS() { + if (this.doc.getElementById("ttn-viewer-style")) return; + + const style = this.doc.createElement("style"); + style.id = "ttn-viewer-style"; + style.innerHTML = ` + html, body { + margin:0; + padding:0; + width:100%; + height:100%; + background:black; + } + + .hidden { + transition: opacity 0.5s, visibility 0.5s, transform 0.2s ease!important; + opacity: 0!important; + visibility: hidden!important; + } + + .ttn-wrapper { + position: fixed; + inset: 0; + width:100%; + height:100%; + display:flex; + justify-content:center; + align-items:center; + transition: background 0.3s; + background-color: #1f1f1f; + } + + .ttn-wrapper.slideshow { + background:black; + } + + .ttn-main-img { + position:absolute; + transform-origin: 0 0; + max-width:none; + max-height:none; + user-select:none; + transform: translateZ(0); + } + + .ttn-previews { + position: absolute; + bottom: 0; + left: 0; + display: flex; + width: max-content; + height: 110px; + transition: transform 0.2s ease; + align-items: flex-end; + background: black; + } + + .ttn-img { + height: 90px; + border: 10px solid black; + cursor: pointer; + display: block; + transition: height 0.4s ease, transform 0.4s ease; + background: black; + box-sizing: content-box; + } + + + .ttn-img.active { + height: 140px; + z-index: 10; + transition: 0.1s; + } + + .ttn-img.before { + transform: scale(1.01); + } + + .ttn-img.before:hover { + height: 110px!important; + z-index: 10; + } + + .ttn-img.after { + transform: scale(1.01); + } + + .ttn-img.after:hover { + height: 110px!important; + z-index: 10; + } + .ttn-img.compare-base { + border:10px solid cyan; + } + + .ttn-img.compare-target { + border:10px solid red; + } + + .ttn-context { + position:absolute; + background:#222; + color:white; + padding:5px; + border:1px solid #555; + z-index:9999; + font-size:14px; + } + + .ttn-context div { + padding:4px 10px; + cursor:pointer; + } + + .ttn-context div:hover { + background:#444; + } + + .settingsBtn { + position: absolute; + top: 10px; + right: 10px; + z-index: 20; + background: gray; + color: white; + border-width: medium; + border-color: silver; + box-sizing: content-box; + } + + .settingsMenu { + position: absolute; + top: 35px; + right: 10px; + background: #222; + padding: 10px; + border: 1px solid #555; + z-Index: 20; + width: 140px; + box-sizing: content-box; + } + + .ttn-btn { + width:stretch; + background: #202020; + border-color: black; + color: gray; + margin: 5px; + padding: 5px; + } + + .ttN-dropdown, .ttN-nested-dropdown { + position: relative; + box-sizing: border-box; + background-color: #171717; + box-shadow: 0 4px 4px rgba(255, 255, 255, .25); + padding: 0; + margin: 0; + list-style: none; + z-index: 1000; + overflow: visible; + max-height: fit-content; + max-width: fit-content; + color: white; + } + + .ttN-dropdown { + position: absolute; + border-radius: 0; + } + + .ttN-dropdown.ttN-dropdown-scrollable { + max-height: min(48vh, 360px); + min-width: 220px; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + scrollbar-gutter: stable; + } + + .ttN-nested-dropdown.ttN-dropdown-scrollable { + max-height: min(48vh, 360px); + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + scrollbar-gutter: stable; + } + + .ttN-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar, + .ttN-nested-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar { + width: 10px; + } + + .ttN-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar-track, + .ttN-nested-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar-track { + background: #121212; + } + + .ttN-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar-thumb, + .ttN-nested-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar-thumb { + background: #4b4b4b; + border-radius: 8px; + } + + .ttN-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar-thumb:hover, + .ttN-nested-dropdown.ttN-dropdown-scrollable::-webkit-scrollbar-thumb:hover { + background: #646464; + } + + /* Style for final items */ + .ttN-dropdown li.item, .ttN-nested-dropdown li.item { + font-weight: normal; + min-width: max-content; + } + + /* Style for folders (parent items) */ + .ttN-dropdown li.folder, .ttN-nested-dropdown li.folder { + cursor: default; + position: relative; + border-right: 3px solid #005757; + } + + .ttN-dropdown li.folder::after, .ttN-nested-dropdown li.folder::after { + content: ">"; + position: absolute; + right: 2px; + font-weight: normal; + } + + .ttN-dropdown li, .ttN-nested-dropdown li { + padding: 4px 10px; + cursor: pointer; + font-family: system-ui; + font-size: 0.7rem; + position: relative; + } + + /* Style for nested dropdowns */ + .ttN-nested-dropdown { + position: absolute; + top: 0; + left: 100%; + margin: 0; + border: none; + display: none; + } + + .ttN-dropdown li.selected > .ttN-nested-dropdown, + .ttN-nested-dropdown li.selected > .ttN-nested-dropdown { + display: block; + border: none; + } + + .ttN-dropdown li.selected, + .ttN-nested-dropdown li.selected { + background-color: #222222; + border: none; + } + `; + this.doc.head.appendChild(style); + } + + createLayout() { + this.wrapper = this.doc.createElement("div"); + this.wrapper.className = "ttn-wrapper slideshow"; + this.doc.body.appendChild(this.wrapper); + + this.image = this.doc.createElement("img"); + this.image.className = "ttn-main-img"; + this.wrapper.appendChild(this.image); + + this.previewBar = this.doc.createElement("div"); + this.previewBar.className = "ttn-previews hidden"; + this.wrapper.appendChild(this.previewBar); + + this.settingsBtn = this.doc.createElement("button"); + this.settingsBtn.innerText = "⚙"; + this.settingsBtn.className = "settingsBtn hidden" + this.wrapper.appendChild(this.settingsBtn); + + this.settingsBtn.onclick = () => + this.toggleSettingsMenu(); + } + + /* ================= IMAGE ================= */ + + refreshImages() { + const list = ttN_srcDict[this.node.id] || []; + + const newSignature = list.join("|"); + if (this._lastSignature === newSignature) return; + this._lastSignature = newSignature; + + this.previewBar.innerHTML = ""; + + list.forEach((src, i) => { + const img = this.doc.createElement("img"); + img.src = src; + img.className = "ttn-img"; + + img.onclick = () => this.setImage(i); + + img.oncontextmenu = (e) => { + e.preventDefault(); + this.ttNcontextMenu(img, i); + }; + + this.previewBar.appendChild(img); + }); + + if (list.length && this.imageIndex === -1) { + this.setImage(list.length - 1); + } + + this.updatePreviewHighlight(); + } + + setImage(i) { + const list = ttN_srcDict[this.node.id] || []; + if (!list.length) return; + + if (i === -1) { + i = list.length - 1; + } else { + i = ((i % list.length) + list.length) % list.length; + } + + this.imageIndex = i; + this.image.src = list[i]; + + this.updatePreviewHighlight(); + + const activeThumb = this.previewBar.children[i]; + + if (activeThumb && !activeThumb.complete) { + activeThumb.onload = () => { + requestAnimationFrame(() => + this.applyPreviewTranslation() + ); + }; + } else { + requestAnimationFrame(() => + this.applyPreviewTranslation() + ); + } + } + + next(ctrl=false, shift=false, reverse=false) { + const num = shift === true ? 5 : 1 + if (this.compareBase !== null && this.compareTarget !== null) { + this.imageIndex = + this.imageIndex === this.compareBase + ? this.compareTarget + : this.compareBase; + this.setImage(this.imageIndex); + return; + } + if (reverse) { + if (ctrl) { + this.setImage(0) + } else { + this.setImage(this.imageIndex - num); + } + } else { + if (ctrl) { + this.setImage(-1) + } else { + this.setImage(this.imageIndex + num); + } + } + } + + prev(ctrl=false, shift=false) { this.next(ctrl, shift, true); } + + /* ================= COMPARE ================= */ + ttNcontextMenu(imgElement, index) { + const SOC = 'Select for Compare' + const CWS = 'Compare with Selected' + const CC = 'Clear Compare' + + let suggestions = {} + + if (this.compareBase !== index && this.compareTarget !== index) { + suggestions[SOC] = null + } + + if (this.compareBase !== null && this.compareBase !== index && this.compareTarget !== index) { + suggestions[CWS] = null + } + + if (this.comparing || this.compareBase !== null) { + suggestions[CC] = null + } + + const manualOffset = ['80%', '70%']; + ttN_CreateDropdown(imgElement, suggestions, async (s) => { + if (s === SOC) { + this.compareBase = index; + this.setImage(index); + this.updatePreviewHighlight(); + } + if (s === CWS) { + if (this.compareBase !== null && this.compareBase !== index) { + this.compareTarget = index; + this.imageIndex = this.compareBase; + this.comparing = true; + this.setImage(index); + } + this.updatePreviewHighlight(); + } + if (s === CC) { + this.compareBase = null; + this.compareTarget = null; + this.comparing = false; + this.updatePreviewHighlight(); + } + }, true, manualOffset, this.wrapper) + } + + updatePreviewHighlight() { + [...this.previewBar.children].forEach((el, i) => { + el.classList.toggle("active", i === this.imageIndex); + el.classList.toggle("compare-base", i === this.compareBase); + el.classList.toggle("compare-target", i === this.compareTarget); + + el.classList.toggle("before", i < this.imageIndex) + el.classList.toggle("after", i > this.imageIndex) + }); + } + + /* ================= TRANSFORM ================= */ + + resetTransform() { + this.scale = 1; + this.offsetX = 0; + this.offsetY = 0; + this.applyTransform(); + } + + applyTransform() { + const x = Math.round(this.offsetX * 1000) / 1000; + const y = Math.round(this.offsetY * 1000) / 1000; + const s = Math.round(this.scale * 1000) / 1000; + + this.image.style.transform = + `translate(${x}px, ${y}px) scale(${s})`; + } + + applyPreviewTranslation() { + if (!this.previewBar.children.length) return; + + const active = this.previewBar.children[this.imageIndex]; + if (!active) return; + + requestAnimationFrame(() => { + // Distance from preview bar left edge to active center + const activeCenter = + active.offsetLeft + + active.offsetWidth / 2 + + parseFloat(getComputedStyle(this.previewBar).paddingLeft); + + // Visible center of screen + const screenCenter = this.wrapper.clientWidth / 2; + + // Compute translation so activeCenter aligns with screenCenter + const translateX = screenCenter - activeCenter; + + this.previewBar.style.transform = + `translateX(${translateX}px)`; + }); + } + + zoomImage(e) { + const rect = this.image.getBoundingClientRect(); + + // Mouse position relative to image + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const prevScale = this.scale; + const zoomFactor = 1.2; + + let newScale = e.deltaY > 0 + ? prevScale / zoomFactor + : prevScale * zoomFactor; + newScale = Math.min(Math.max(newScale, 0.1), 8); + + const scaleRatio = newScale / prevScale; + + // Adjust offsets so the point under cursor stays fixed + this.offsetX -= mouseX * (scaleRatio - 1); + this.offsetY -= mouseY * (scaleRatio - 1); + + if (Math.abs(this.offsetX) < 0.0001) this.offsetX = 0; + if (Math.abs(this.offsetY) < 0.0001) this.offsetY = 0; + + this.scale = newScale; + this.applyTransform(); + } + + fitToScreen() { + if (!this.image.naturalWidth || !this.image.naturalHeight) return; + + const wrapperWidth = this.wrapper.clientWidth; + const wrapperHeight = this.wrapper.clientHeight; + + const imgWidth = this.image.naturalWidth; + const imgHeight = this.image.naturalHeight; + + const scaleX = wrapperWidth / imgWidth; + const scaleY = wrapperHeight / imgHeight; + + this.scale = Math.min(scaleX, scaleY); + + const scaledWidth = imgWidth * this.scale; + const scaledHeight = imgHeight * this.scale; + + this.offsetX = -(scaledWidth - imgWidth) / 2; + this.offsetY = -(scaledHeight - imgHeight) / 2; + this.applyTransform(); + } + /* ================ HELPERS ================= */ + + _isMouseOverElement(element, mouseX, mouseY) { + if (!element) return false; + const rect = element.getBoundingClientRect(); + return ( + mouseX >= rect.left && + mouseX <= rect.right && + mouseY >= rect.top && + mouseY <= rect.bottom + ); + } + + _isOverUI(mouseX, mouseY) { + if (this.previewBar && this._isMouseOverElement(this.previewBar, mouseX, mouseY)) { + return true + } + if (this.settingsBtn && this._isMouseOverElement(this.settingsBtn, mouseX, mouseY)) { + return true + } + if (this.settingsMenu && this._isMouseOverElement(this.settingsMenu, mouseX, mouseY)) { + return true + } + + return false + } + + _reset_hideUI_Timeout(timeout=3700) { + clearTimeout(this.hideTimeout); + + this.hideTimeout = setTimeout(() => { + if (this.slideshow && this.autohide) { + this.toggleUI(false, false); + } + }, timeout); + } + + toggleUI(show=null, reset=true) { + if (show==null) { + this.previewBar.classList.toggle('hidden') + this.settingsBtn.classList.toggle('hidden') + this.settingsMenu?.classList.toggle('hidden') + } else { + this.previewBar.classList.toggle('hidden', !show) + this.settingsBtn.classList.toggle('hidden', !show) + this.settingsMenu?.classList.toggle('hidden', !show) + } + if (reset) this._reset_hideUI_Timeout(); + } + + toggleSettingsMenu() { + if (this.settingsMenu) { + this.settingsMenu.remove(); + this.settingsMenu = null; + return; + } + + const menu = this.doc.createElement("div"); + menu.className = "settingsMenu" + + const autoBtn = this.doc.createElement("button"); + autoBtn.className = 'ttn-btn' + autoBtn.id = 'autoBtn' + autoBtn.innerText = `Autohide: ${this.autohide ? "ON" : "OFF"}`; + autoBtn.onclick = () => { + this.autohide = !this.autohide; + localStorage.setItem(STORAGE_KEYS.AUTOHIDE, + JSON.stringify(this.autohide) + ); + autoBtn.innerText = + `Autohide: ${this.autohide ? "ON" : "OFF"}`; + }; + + const invertBtn = this.doc.createElement("button"); + invertBtn.className = 'ttn-btn' + invertBtn.id = 'invertBtn' + invertBtn.innerText = `Wheel: ${this.invertctrl ? "ZOOM" : "SCROLL"}`; + invertBtn.onclick = () => { + this.invertctrl = !this.invertctrl; + localStorage.setItem(STORAGE_KEYS.INVERT, + JSON.stringify(this.invertctrl) + ); + invertBtn.innerText = + `Wheel: ${this.invertctrl ? "ZOOM" : "SCROLL"}`; + }; + + const slideBtn = this.doc.createElement("button"); + slideBtn.className = 'ttn-btn' + slideBtn.id = 'slideBtn' + slideBtn.innerText = `Slideshow: ${this.slideshow ? "ON" : "OFF"}`; + slideBtn.onclick = () => { + this.setSlideshow(!this.slideshow) + }; + + const fitScrnBtn = this.doc.createElement("button"); + fitScrnBtn.className = 'ttn-btn' + fitScrnBtn.id = 'fitScrnBtn' + fitScrnBtn.innerText = `Fit to Screen: ${this.fitscreentoggle ? "ON" : "OFF"}`; + fitScrnBtn.onclick = () => { + this.fitscreentoggle = !this.fitscreentoggle; + localStorage.setItem(STORAGE_KEYS.FITSCREEN, + JSON.stringify(this.fitscreentoggle)) + fitScrnBtn.innerText = `Fit to Screen: ${this.fitscreentoggle ? "ON" : "OFF"}`; + if (this.fitscreentoggle) this.fitToScreen() + } + + const infoEl = this.doc.createElement("p") + infoEl.textContent = "Up Arrow - Hide/Show UI\nDown Arrow - Toggle Slideshow\nLeft Arrow - Previous Image\nRight Arrow - Next Image\nF - Fit image to window" + + menu.appendChild(autoBtn); + menu.appendChild(this.doc.createElement("br")); + menu.appendChild(invertBtn); + menu.appendChild(this.doc.createElement("br")); + menu.appendChild(slideBtn); + if (this.mode != 'fullscreen') { + menu.appendChild(this.doc.createElement("br")); + menu.appendChild(fitScrnBtn); + } + + + this.wrapper.appendChild(menu); + this.settingsMenu = menu; + } + + setSlideshow(enabled) { + this.slideshow = enabled; + this.wrapper.classList.toggle("slideshow", enabled); + + if (this.settingsMenu) { + const slideBtn = this.settingsMenu.querySelector('#slideBtn'); + if (slideBtn) { + slideBtn.innerText = `Slideshow: ${this.slideshow ? "ON" : "OFF"}`; + } + } + + if (enabled) { + if (!this.comparing) this.setImage(-1); + if (this.autohide) this.toggleUI(false); + } else { + this.toggleUI(true); + } + } + + /* ================= EVENTS ================= */ + _onKeyDown = (e) => { + if (e.code === "ArrowLeft") { + e.preventDefault(); + this.prev(e.ctrlKey, e.shiftKey); + } + + if (e.code === "ArrowRight") { + e.preventDefault(); + this.next(e.ctrlKey, e.shiftKey); + } + + if (e.code === "ArrowDown") { + this.setSlideshow(!this.slideshow) + } + + if (e.code === "ArrowUp") { + this.toggleUI(); + } + + if (e.code === "Escape") { + if (this.mode === "fullscreen") { + if (this.doc.fullscreenElement) { + this.doc.exitFullscreen().catch(() => {}); + } + } else { + this.doc.defaultView.close(); + } + } + + if (e.code === "KeyF") { + this.fitToScreen() + } + } + + _onWheel = (e) => { + e.preventDefault(); + + const isZoom = (this.invertctrl && !e.ctrlKey) || + (!this.invertctrl && e.ctrlKey); + + if (isZoom) { + this.zoomImage(e); + return + } + + const now = performance.now(); + if (now - this._lastWheelTime < 40) return; + this._lastWheelTime = now; + + if (e.deltaY > 0) this.next(); + else this.prev(); + } + + _onMouseDown = (e) => { + if (!this._isOverUI(e.clientX, e.clientY)) { + e.preventDefault(); + this.dragging = true; + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + } + } + + _onMouseMove = (e) => { + if (this.dragging) { + const dx = e.clientX - this.dragStartX; + const dy = e.clientY - this.dragStartY; + + this.offsetX += dx; + this.offsetY += dy; + + this.dragStartX = e.clientX; + this.dragStartY = e.clientY; + + this.applyTransform(); + } + if (this.slideshow){ + if (this._isOverUI(e.clientX, e.clientY)) { + if (this.previewBar.classList.contains("hidden")) { + this.toggleUI(true); + } else { + this._reset_hideUI_Timeout(); + } + } + } + } + + _onMouseUp = (e) => { this.dragging = false; } + + _onClick = (e) => { + if (!this._isOverUI(e.clientX, e.clientY) && this.slideshow && this.autohide) { + this.toggleUI(false) + } + } + + _onDblClick = (e) => { + if (!this._isOverUI(e.clientX, e.clientY)) { + this.resetTransform(); + } + } + + _onFullscreenChange = () => { + if (this.doc.fullscreenElement) { + requestAnimationFrame(() => { + this.applyPreviewTranslation(); + }); + return; + } + + TTNViewer.fullscreenInstance = null; + this.destroy(); + }; + + attachEvents() { + this.doc.addEventListener("keydown", this._onKeyDown); + this.doc.addEventListener("wheel", this._onWheel, this._wheelOptions); + this.doc.addEventListener("mousedown", this._onMouseDown); + this.wrapper.addEventListener("mousemove", this._onMouseMove); + this.doc.addEventListener("mouseup", this._onMouseUp) + this.doc.addEventListener("click", this._onClick); + this.doc.addEventListener("dblclick", this._onDblClick); + this.doc.addEventListener("fullscreenchange", this._onFullscreenChange); + + this._lastWrapperSize = { w: 0, h: 0 }; + this._resizeObserver = new ResizeObserver(() => { + if (this._resizing) return; + const w = this.wrapper.clientWidth; + const h = this.wrapper.clientHeight; + + if (w === this._lastWrapperSize.w && + h === this._lastWrapperSize.h) { + return; + } + + this._lastWrapperSize = { w, h }; + + this._resizing = true; + + requestAnimationFrame(() => { + try { + if (this.fitscreentoggle) { + this.fitToScreen(); + } + this.applyPreviewTranslation(); + } finally { + this._resizing = false; + } + }); + }); + + this._resizeObserver.observe(this.wrapper); + } + + destroy() { + TTNViewer.instances.delete(this); + + this.doc.removeEventListener("keydown", this._onKeyDown); + this.doc.removeEventListener("wheel", this._onWheel, this._wheelOptions); + this.doc.removeEventListener("mousedown", this._onMouseDown); + this.wrapper.removeEventListener("mousemove", this._onMouseMove); + this.doc.removeEventListener("mouseup", this._onMouseUp) + this.doc.removeEventListener("click", this._onClick); + this.doc.removeEventListener("dblclick", this._onDblClick); + this.doc.removeEventListener("fullscreenchange", this._onFullscreenChange); + + if (this._resizeObserver) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } + + this.wrapper?.remove(); + + } +} + +/* ========================================================= + LAUNCHERS +========================================================= */ + +function _getSelectedNode() { + const graphcanvas = LGraphCanvas.active_canvas; + if (graphcanvas.selected_nodes && + Object.keys(graphcanvas.selected_nodes).length === 1) { + return Object.values(graphcanvas.selected_nodes)[0]; + } + return null; +} + +function _getViewerNode() { + const node = _getSelectedNode() + if (node) return node + + let defaultNodeID = JSON.parse(localStorage.getItem(STORAGE_KEYS.DEFAULTNODE)) + if (defaultNodeID) { + let defaultNode = app.graph._nodes_by_id[defaultNodeID] + if (defaultNode) return defaultNode + } + + return null; +} + +export function _setDefaultFullscreenNode() { + let selectedNode = _getSelectedNode(); + if (selectedNode) { + localStorage.setItem(STORAGE_KEYS.DEFAULTNODE, JSON.stringify(selectedNode.id)); + } else { + localStorage.removeItem(STORAGE_KEYS.DEFAULTNODE); + } +} + +export function openFullscreenApp(node) { + if (TTNViewer.fullscreenInstance) return; + TTNViewer.fullscreenInstance = + new TTNViewer(node, document, "fullscreen"); +} + +export function openPopoutViewer(node) { + const win = window.open("", "_blank","width=512,height=512,resizable=yes"); + if (!win) return; + + TTN_POPOUTS.add(win); + win.addEventListener("beforeunload", () => { + TTN_POPOUTS.delete(win); + }); + + win.document.write(` + + TTN Viewer - [${node.id}] ${node.title} + `); + win.document.close(); + + new TTNViewer(node, win.document, "popout"); +} + +window.addEventListener("beforeunload", () => { + for (const win of TTN_POPOUTS) { + try { + win.close(); + } catch {} + } +}); + +/* ========================================================= + HOTKEYS +========================================================= */ + +document.addEventListener("keydown", (e) => { + if (e.code === "F11" && e.shiftKey) { + const node = _getViewerNode(); + if (node) openFullscreenApp(node); + } + + if (e.code === "F10" && e.shiftKey) { + const node = _getViewerNode(); + if (node) openPopoutViewer(node); + } +}); \ No newline at end of file