Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
005efdd7c5 | ||
|
|
78682dd0cb | ||
|
|
29198a5fd7 | ||
|
|
5c09936c3a | ||
|
|
ba35d58e81 | ||
|
|
ef95266cf4 | ||
|
|
b4228dc340 | ||
|
|
88f1485cc9 | ||
|
|
16ef5bf5db | ||
|
|
983862eec3 | ||
|
|
acc1570629 | ||
|
|
30c0547009 | ||
|
|
13e24c3a57 | ||
|
|
cd00cce1f8 | ||
|
|
a26d060d5f |
@@ -30,6 +30,6 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install Pillow torch numpy aiohttp piexif
|
||||
|
||||
- name: Execute LeafFlow Unit Test Suite
|
||||
- name: Execute SaturnNodes Unit Test Suite
|
||||
run: |
|
||||
python tests/run_tests.py
|
||||
|
||||
+17
-1
@@ -1,10 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `ComfyUI-LeafFlow` will be documented in this file.
|
||||
All notable changes to `ComfyUI-SaturnNodes` will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.3.2] - 2026-09-22
|
||||
|
||||
### Changed & Rebranded
|
||||
- **Full Suite Rebrand to SaturnNodes (`🪐`)**:
|
||||
- Rebranded the entire suite from `ComfyUI-LeafFlow` to **`ComfyUI-SaturnNodes`** with signature emoji `🪐`.
|
||||
- Node display names updated with the `🪐` prefix across all 16 custom nodes.
|
||||
- Node category updated to `🪐 SaturnNodes/*` (`Loaders`, `Utils`, `Previews`, `Queue`, `Automation`).
|
||||
- Full backward compatibility: all legacy class names (`LeafFlowDecision`, `LeafFlowTextSplit`) preserved and aliased alongside new class names in `NODE_CLASS_MAPPINGS`.
|
||||
- Dual HTTP route registration: both `/saturnnodes/*` and `/leafflow/*` routes are active, guaranteeing compatibility with existing scripts, extensions, and bookmarks.
|
||||
- Dual WebSocket event emission: both `saturnnodes_*` and `leafflow_*` events are broadcast to the frontend.
|
||||
- Seamless settings migration: frontend settings and local storage automatically migrate from `LeafFlow.*` to `SaturnNodes.*` with instant fallback.
|
||||
- Automatic user directory migration: `.env` and `prompt_iterator_state.json` migrate smoothly from `user/default/LeafFlow/` to `user/default/SaturnNodes/`.
|
||||
- Removed third-party PromptBookmarks coupling: cleaned UI bookmarks button from copy prompt node for modularity.
|
||||
|
||||
---
|
||||
|
||||
## [2.3.1] - 2026-09-22
|
||||
|
||||
### Security & Hardening
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
# 🤝 Contributing to ComfyUI-LeafFlow
|
||||
# 🤝 Contributing to ComfyUI-SaturnNodes
|
||||
|
||||
Thank you for your interest in contributing to **`ComfyUI-LeafFlow`**! We welcome bug reports, feature suggestions, documentation updates, and pull requests from the community.
|
||||
Thank you for your interest in contributing to **`ComfyUI-SaturnNodes`**! We welcome bug reports, feature suggestions, documentation updates, and pull requests from the community.
|
||||
|
||||
Please take a moment to review these guidelines before submitting an issue or pull request.
|
||||
|
||||
@@ -9,7 +9,7 @@ Please take a moment to review these guidelines before submitting an issue or pu
|
||||
## 🐛 Reporting Issues & Bugs
|
||||
|
||||
Before opening a new issue:
|
||||
1. **Check Existing Issues**: Search the [Issue Tracker](https://github.com/KOFiblto/ComfyUI-LeafFlow/issues) to see if your bug or feature request has already been reported.
|
||||
1. **Check Existing Issues**: Search the [Issue Tracker](https://github.com/KOFiblto/ComfyUI-SaturnNodes/issues) to see if your bug or feature request has already been reported.
|
||||
2. **Include Reproduction Steps**: Provide a clear step-by-step description of how to reproduce the bug.
|
||||
3. **Include Logs & Environment Details**:
|
||||
- **ComfyUI Version** (V2 Vue UI or Classic V1 LiteGraph)
|
||||
@@ -33,8 +33,8 @@ All development follows a **Feature Branch $\rightarrow$ Pull Request** workflow
|
||||
### Step 1: Fork and Clone
|
||||
Fork the repository on GitHub, then clone your fork locally:
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/ComfyUI-LeafFlow.git
|
||||
cd ComfyUI-LeafFlow
|
||||
git clone https://github.com/YOUR_USERNAME/ComfyUI-SaturnNodes.git
|
||||
cd ComfyUI-SaturnNodes
|
||||
```
|
||||
|
||||
### Step 2: Create a Feature Branch
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
# 🍃 ComfyUI-LeafFlow
|
||||
# 🪐 ComfyUI-SaturnNodes
|
||||
|
||||
A unified custom node suite for **ComfyUI** featuring real-time queue controls, mid-generation pausing, queue crash recovery, visual model pickers, live latent canvas previews, folder automation, prompt utilities, and resolution tools.
|
||||
A unified workflow control and productivity suite for **ComfyUI**. Built for **ComfyUI Frontend V2 (Nodes 2.0 / Vue UI)** (fully supported and actively tested) with backwards compatibility for the **Classic ComfyUI Frontend (Nodes 1.0 / LiteGraph)** (supported as-is, but untested—no guarantees).
|
||||
|
||||
> ⚠️ **Notice**: Fully compatible with both **ComfyUI Frontend V2 (Nodes 2.0 / Vue UI)** and **Classic ComfyUI Frontend (Nodes 1.0 / LiteGraph)**.
|
||||
---
|
||||
|
||||
## 🚀 Key Capabilities & Overview
|
||||
|
||||
- **Live Latent Preview**: Watch generations materialize step-by-step directly inside your graph canvas nodes in real time.
|
||||
- **Batch Prompting & Iteration**: Queue multiple prompts automatically with delimiter splits, regex blocks, and persistent queue state tracking.
|
||||
- **Visual Thumbnail Browsing**: Browse LoRAs and image folders with auto-scraped Civitai & TMDB previews, ranking badges, and multi-selection.
|
||||
- **Real-Time Queue & Pause Controls**: Pause, resume, or cleanly cancel execution mid-generation, with automatic crash recovery for interrupted queues.
|
||||
- **Aspect Ratio & Resolution Utilities**: Find exact matching aspect ratios, crop dimensions, and visual sizing helpers.
|
||||
- **Safe Local Script Execution**: Run local scripts and executables strictly confined to `ComfyUI/scripts/` with interactive operator authorization.
|
||||
|
||||
---
|
||||
|
||||
@@ -11,7 +20,7 @@ A unified custom node suite for **ComfyUI** featuring real-time queue controls,
|
||||
Expand any node below to view its description, inputs, outputs, and usage documentation.
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 📁 LoRA Loader (Folder)</b> (<code>FolderLoraLoader</code>)</summary>
|
||||
<summary><b>🪐 📁 LoRA Loader (Folder)</b> (<code>FolderLoraLoader</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Loads a LoRA by folder path using raw filename matching or formatted names with customizable output formatting options.
|
||||
@@ -33,7 +42,7 @@ Loads a LoRA by folder path using raw filename matching or formatted names with
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 ✨ LoRA Loader (Pretty)</b> (<code>FolderLoraLoaderPretty</code>)</summary>
|
||||
<summary><b>🪐 ✨ LoRA Loader (Pretty)</b> (<code>FolderLoraLoaderPretty</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Loads a LoRA using formatted pretty names (e.g. `Ana De Armas V1` instead of `krea2_Ana-De-Armas_v1.safetensors`).
|
||||
@@ -53,7 +62,7 @@ Loads a LoRA using formatted pretty names (e.g. `Ana De Armas V1` instead of `kr
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 🖼️ Visual LoRA Loader</b> (<code>VisualLoraLoader</code> / alias <code>FolderLoraLoaderVisualPrettyV2</code>)</summary>
|
||||
<summary><b>🪐 🖼️ Visual LoRA Loader</b> (<code>VisualLoraLoader</code> / alias <code>FolderLoraLoaderVisualPrettyV2</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Visual thumbnail browser for LoRAs with Civitai SHA256 search & TMDB auto-scraping, popularity rank badges (🔥), multi-selection support, and independent per-node thumbnail zoom sizing.
|
||||
@@ -75,7 +84,7 @@ Visual thumbnail browser for LoRAs with Civitai SHA256 search & TMDB auto-scrapi
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 📷 Visual Image Loader</b> (<code>VisualImageLoader</code> / alias <code>ImageLoaderVisualPrettyV2</code>)</summary>
|
||||
<summary><b>🪐 📷 Visual Image Loader</b> (<code>VisualImageLoader</code> / alias <code>ImageLoaderVisualPrettyV2</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Visual thumbnail browser for image folders with instant preview selection, independent per-node thumbnail zoom sizing, and EXIF positive prompt metadata extraction. Path access is strictly confined to ComfyUI `input`, `output`, and `temp` directories for safe operation.
|
||||
@@ -94,7 +103,7 @@ Visual thumbnail browser for image folders with instant preview selection, indep
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 📂 Load Image From Folder</b> (<code>LoadImageFromFolder</code>)</summary>
|
||||
<summary><b>🪐 📂 Load Image From Folder</b> (<code>LoadImageFromFolder</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Monitors a folder for incoming images, loads the target image into a PyTorch tensor, with an optional toggle to delete the image after loading.
|
||||
@@ -113,7 +122,7 @@ Monitors a folder for incoming images, loads the target image into a PyTorch ten
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 ⏱️ Recent Outputs</b> (<code>LoadRecentOutputs</code>)</summary>
|
||||
<summary><b>🪐 ⏱️ Recent Outputs</b> (<code>LoadRecentOutputs</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Loads the N newest images from an output directory with step-through index selection.
|
||||
@@ -128,21 +137,21 @@ Loads the N newest images from an output directory with step-through index selec
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 👁️ Live Latent Preview</b> (<code>PreviewLatentLiveNode</code>)</summary>
|
||||
<summary><b>🪐 👁️ Live Latent Preview</b> (<code>PreviewLatentLiveNode</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Canvas rendering node that listens to sampler WebSocket latent binary streams and displays real-time live previews on the canvas during generation.
|
||||
|
||||
#### Inputs & Outputs
|
||||
- **Category**: `LeafFlow/Previews`
|
||||
- **Category**: `🪐 SaturnNodes/Previews`
|
||||
- **Output Node**: True
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 ⏸️ LeafFlow Decision</b> (<code>LeafFlowDecision</code>)</summary>
|
||||
<summary><b>🪐 ⏸️ Saturn Decision</b> (<code>SaturnDecision</code> / alias <code>LeafFlowDecision</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Pauses workflow execution at a specific step and displays an inline UI popup with Continue, Cancel, or Stop Workflow actions, plus optional native OS desktop notifications.
|
||||
Pauses workflow execution at a specific step and displays an inline UI popup with Continue, Cancel, or Stop Workflow actions, plus optional native OS desktop notifications. Fully backward compatible with workflows containing `LeafFlowDecision`.
|
||||
|
||||
#### Inputs & Widgets
|
||||
- **`disable`** (`BOOLEAN`): Bypass decision gate.
|
||||
@@ -154,7 +163,7 @@ Pauses workflow execution at a specific step and displays an inline UI popup wit
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 📐 Text Aspect Ratio Finder</b> (<code>TextAspectRatioFinder</code> / alias <code>AspectRatioFinder</code>)</summary>
|
||||
<summary><b>🪐 📐 Text Aspect Ratio Finder</b> (<code>TextAspectRatioFinder</code> / alias <code>AspectRatioFinder</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Parses input text for aspect ratios (e.g. `16:9`, `2.35:1`), syntax-checks them, and calculates pixel resolution for a target megapixel target.
|
||||
@@ -177,7 +186,7 @@ Parses input text for aspect ratios (e.g. `16:9`, `2.35:1`), syntax-checks them,
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 📐 Preview Image Size & Aspect Ratio</b> (<code>PreviewImageSizeAspectRatio</code>)</summary>
|
||||
<summary><b>🪐 📐 Preview Image Size & Aspect Ratio</b> (<code>PreviewImageSizeAspectRatio</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Computes image dimensions and aspect ratio from an input image tensor and formats the result for previewing.
|
||||
@@ -192,7 +201,7 @@ Computes image dimensions and aspect ratio from an input image tensor and format
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 🔎 Text LoRA Finder & Loader</b> (<code>TextLoraFinder</code> / alias <code>LoraTextFinder</code>)</summary>
|
||||
<summary><b>🪐 🔎 Text LoRA Finder & Loader</b> (<code>TextLoraFinder</code> / alias <code>LoraTextFinder</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Scans input prompt text for `<lora:name:strength>` tags or names matching a folder on disk, dynamically applies them, and returns patched models along with sanitized prompt text.
|
||||
@@ -214,7 +223,7 @@ Scans input prompt text for `<lora:name:strength>` tags or names matching a fold
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 🔄 Prompt Queue Iterator</b> (<code>PromptQueueIterator</code>)</summary>
|
||||
<summary><b>🪐 🔄 Prompt Queue Iterator</b> (<code>PromptQueueIterator</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Deterministically iterates over multiline prompt text blocks per queue run with live progress display, counter reset controls, and index tracking. Supports splitting prompts by empty lines, newlines, or a custom regular expression delimiter.
|
||||
@@ -233,10 +242,10 @@ Deterministically iterates over multiline prompt text blocks per queue run with
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 📝 Prompt Counter</b> (<code>PromptCounter</code>)</summary>
|
||||
<summary><b>🪐 📝 Prompt Counter</b> (<code>PromptCounter</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Multiline text prompt input node that functions identically to a standard multiline string node, with real-time prompt counting according to your chosen delimiter (`>1 Empty Line`, `Newline`, `>2 Empty Lines`, or `Custom Regex`). Displays the live prompt count directly on the node (e.g. `🍃 24 Prompts`) on every keystroke, allowing you to easily determine and set the exact ComfyUI batch size before running your queue.
|
||||
Multiline text prompt input node that functions identically to a standard multiline string node, with real-time prompt counting according to your chosen delimiter (`>1 Empty Line`, `Newline`, `>2 Empty Lines`, or `Custom Regex`). Displays the live prompt count directly on the node on every keystroke, allowing you to easily determine and set the exact ComfyUI batch size before running your queue.
|
||||
|
||||
#### Inputs & Widgets
|
||||
- **`text`** (`STRING`, Multiline): Input prompt text block.
|
||||
@@ -249,7 +258,7 @@ Multiline text prompt input node that functions identically to a standard multil
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 🔤 Multi Text Replacer</b> (<code>MultiTextReplacer</code>)</summary>
|
||||
<summary><b>🪐 🔤 Multi Text Replacer</b> (<code>MultiTextReplacer</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Performs multiple text replacements in a single step using comma-separated or newline-separated find/replace lists, with support for exact phrases and regex matching.
|
||||
@@ -266,26 +275,25 @@ Performs multiple text replacements in a single step using comma-separated or ne
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 ✂️ Text Split</b> (<code>LeafFlowTextSplit</code>)</summary>
|
||||
<summary><b>🪐 ✂️ Text Split</b> (<code>SaturnTextSplit</code> / alias <code>LeafFlowTextSplit</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Splits text into two parts at a specified delimiter. Supports forward (from start) and backward (from end) search, as well as regular expressions.
|
||||
|
||||
#### Inputs & Widgets
|
||||
- **`text`** (`STRING`, Multiline): Input text to split.
|
||||
- **`delimiter`** (`STRING`): Character or string to split on (e.g. `,`, `---`, `\n`).
|
||||
- **`search_from`** (`COMBO`): `Forward (First Match)` vs `Backward (Last Match)`.
|
||||
- **`use_regex`** (`BOOLEAN`): Treat delimiter as a regular expression pattern.
|
||||
- **`strip_whitespace`** (`BOOLEAN`): Trims surrounding whitespace from output strings.
|
||||
- **`split_by`** (`STRING`): Character sequence or regex pattern to split on (e.g. `--`, `,`, `---`, `\n`).
|
||||
- **`split_direction`** (`COMBO`, *Advanced*): `forward (first occurrence from start)` vs `backward (last occurrence from end)`.
|
||||
- **`use_regex`** (`BOOLEAN`, *Advanced*): Treat delimiter as a regular expression pattern.
|
||||
- **`strip_whitespace`** (`BOOLEAN`, *Advanced*): Trims surrounding whitespace from output strings.
|
||||
|
||||
#### Outputs
|
||||
- **`left_text`** (`STRING`): Text before the split delimiter.
|
||||
- **`right_text`** (`STRING`): Text after the split delimiter.
|
||||
- **`delimiter_found`** (`BOOLEAN`): True if delimiter was found, False otherwise.
|
||||
- **`text1`** (`STRING`): Text before the split delimiter.
|
||||
- **`text2`** (`STRING`): Text after the split delimiter.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🍃 ⚡ Run Local File</b> (<code>RunLocalFileNode</code>)</summary>
|
||||
<summary><b>🪐 ⚡ Run Local File</b> (<code>RunLocalFileNode</code>)</summary>
|
||||
|
||||
#### Overview
|
||||
Safely executes a local script or executable (`.bat`, `.cmd`, `.ps1`, `.exe` on Windows; `.sh` or binaries on Linux/macOS) with command-line parameters and working directory control. Built with multi-layered security guards to ensure malicious workflows or dropped images cannot execute code without explicit authorization.
|
||||
@@ -300,7 +308,7 @@ Safely executes a local script or executable (`.bat`, `.cmd`, `.ps1`, `.exe` on
|
||||
- **`run_mode`** (`COMBO`): `Synchronous (Wait for Output)` vs `Asynchronous (Background)`.
|
||||
- **`timeout`** (`INT`): Maximum execution time in seconds for synchronous mode (`0` = no timeout). If exceeded, the process tree is cleanly terminated via OS process management.
|
||||
- **`trigger`** (`*`, Optional Wildcard): Input wire allowing you to connect any workflow data to order execution.
|
||||
- **⚡ In-Memory Authorization**: Does NOT use an input widget (preventing malicious authorization smuggling inside shared workflow JSON or image metadata). Instead, the operator must click the interactive `[⚡ Authorize Run]` button directly on the canvas node to grant a 5-minute single-use execution token.
|
||||
- **⚡ In-Memory Authorization**: Does NOT use an input widget (preventing malicious authorization smuggling inside shared workflow JSON or image metadata). Instead, the operator must click the interactive `⚡ Authorize Run (5 min)` button directly on the canvas node to grant a 5-minute single-use execution token.
|
||||
|
||||
#### Outputs
|
||||
- **`stdout`** (`STRING`): Captured standard output stream.
|
||||
@@ -312,14 +320,17 @@ Safely executes a local script or executable (`.bat`, `.cmd`, `.ps1`, `.exe` on
|
||||
|
||||
---
|
||||
|
||||
<a id="comfyui-settings-menu-reference"></a>
|
||||
## ⚙️ ComfyUI Settings Menu Reference
|
||||
|
||||
Configure options directly under ComfyUI Settings (⚙ gear icon):
|
||||
|
||||
<details open>
|
||||
<summary><b>1. 🖼️ Visual Loaders (Civitai & TMDB Duo)</b></summary>
|
||||
<details>
|
||||
<summary><b>🖼️ Visual Loaders (Civitai & TMDB Duo)</b></summary>
|
||||
|
||||
- **`Enable Custom LeafFlow Node Colors`** (`boolean`, *Default: true*): Applies a vibrant Leaf Green color theme to LeafFlow nodes on the canvas. When disabled, nodes use default ComfyUI colors.
|
||||
- **`Enable Custom SaturnNodes Colors`** (`boolean`, *Default: false*): Applies custom amber colors to SaturnNodes on the canvas. When disabled (default), nodes use default ComfyUI colors.
|
||||
- **`Custom Node Title Color`** (`color`, *Default: `#6a3f20`*): Header / title bar color picker for SaturnNodes when custom colors are enabled.
|
||||
- **`Custom Node Body Color`** (`color`, *Default: `#472a15`*): Body background color picker for SaturnNodes when custom colors are enabled.
|
||||
- **`Civitai API Key`** (`text`): Optional key for Civitai SHA256 model preview search (`Authorization: Bearer <key>`).
|
||||
- **`Enable Civitai Auto-Scraping`** (`boolean`, *Default: true*): Automatically download model preview images from Civitai via SHA256 file hashes. (Note: Local SHA256 hash searching always works).
|
||||
- **`TMDB Access Token`** (`text`): Optional key or v4 Read Access Token (`eyJ...`) for celebrity preview search.
|
||||
@@ -328,66 +339,66 @@ Configure options directly under ComfyUI Settings (⚙ gear icon):
|
||||
- **`Reset Failed Scrapes Cache`** (*Button: `🗑️ Clear Scrapes Cache`*): Clears failed scrape history so Civitai/TMDB can retry downloading missing preview images.
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>2. 🔄 Prompt Iterator</b></summary>
|
||||
<details>
|
||||
<summary><b>🔄 Prompt Iterator</b></summary>
|
||||
|
||||
- **`Clear State on Launch`** (`boolean`, *Default: false*): Privacy toggle to empty `prompt_iterator_state.json` on ComfyUI startup.
|
||||
- **`Reset All Queues`** (*Button: `🔄 Reset All Queues`*): Immediately empties all active prompt queues and resets iterator state.
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>3. 📋 Prompt Actions</b></summary>
|
||||
<details>
|
||||
<summary><b>📋 Prompt Actions</b></summary>
|
||||
|
||||
- **`Show "Copy Prompt" Button on Images`** (`boolean`, *Default: true*): Shows the 📋 "Copy Prompt" overlay action button when hovering over generated images in the Assets / History pane and preview nodes.
|
||||
- **`Show Right-Click "Copy Prompt" Menu Action`** (`boolean`, *Default: true*): Adds "📋 Copy Prompt" to node right-click context menus.
|
||||
- **`Show "Save to Prompt Saver" Action`** (`boolean`, *Default: true*): Adds 🔖 "Save to Prompt Bookmarks" to image hover overlay bars and node context menus.
|
||||
- **`Show "Inspect Asset" (Zoom) Button on Images`** (`boolean`, *Default: false*): Restores the 🔍 "Inspect asset" (zoom in) button directly onto image cards in the Assets pane next to Download, Copy Prompt, and Bookmarks (restoring one-click access moved behind the 3-dots menu in newer ComfyUI versions).
|
||||
- **`Show "Inspect Asset" (Zoom) Button on Images`** (`boolean`, *Default: false*): Restores the 🔍 "Inspect asset" (zoom in) button directly onto image cards in the Assets pane next to Download and Copy Prompt (restoring one-click access moved behind the 3-dots menu in newer ComfyUI versions).
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>4. ⏸️ Pause Controls</b></summary>
|
||||
<details>
|
||||
<summary><b>⏸️ Pause Controls</b></summary>
|
||||
|
||||
- **`Default State on Launch`** (`combo`, *Default: `Paused`*): Sets whether the queue starts `Paused` or `Running` on boot.
|
||||
- **`Default State on Launch`** (`combo`, *Default: `Running`*): Sets whether the queue starts `Paused` or `Running` on boot.
|
||||
- **`Default Pause Action`** (`combo`, *Default: `Finish Active Prompt`*): Sets default pause behavior (`Finish Active Prompt` vs `Instant Resume Node`).
|
||||
- **`Enable Top Toolbar Button`** (`boolean`, *Default: true*): Toggle top action bar Pause & Continue button group ON/OFF.
|
||||
- **`Toolbar Button Unpaused Color`** (`text`, *Default: `#16a34a`*): Hex color for the toolbar unpaused/running state.
|
||||
- **`Toolbar Button Paused Color`** (`text`, *Default: `#ea580c`*): Hex color for the toolbar paused state.
|
||||
- **`Enable System Tray Icon`** (`boolean`, *Default: false*): Displays an OS system tray icon with real-time queue status colors and outside-browser controls.
|
||||
- **`Allow Process Management (Restart / Shutdown)`** (`boolean`, *Default: false*): Opt-in authorization allowing server restart and shutdown actions from the LeafFlow sidebar and API. Disabled by default for maximum security.
|
||||
- **`Allow Process Management (Restart / Shutdown)`** (`boolean`, *Default: false*): Opt-in authorization allowing server restart and shutdown actions from the SaturnNodes power controls. Disabled by default for maximum security.
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>5. 💾 Persistent Queue</b></summary>
|
||||
<details>
|
||||
<summary><b>💾 Persistent Queue</b></summary>
|
||||
|
||||
- **`Persistent Queue (Auto-Recovery)`** (`boolean`, *Default: true*): Automatically saves unfinished queue items and restores them after restart/crash.
|
||||
- **`Recovery Launch State`** (`combo`, *Default: `Match Default`*): Override launch state when restored queue items are recovered on startup (`Match Default`, `Force Paused`, `Force Running`).
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>6. 🖼️ Assets & History Restore</b></summary>
|
||||
<details>
|
||||
<summary><b>🖼️ Assets & History Restore</b></summary>
|
||||
|
||||
- **`Restore Assets on Launch`** (`boolean`, *Default: true*): Automatically restores your latest generated images into the Assets / History pane on startup.
|
||||
- **`Restored Assets Count`** (`number`, *Default: 64*): The number of newest images from the output folder to populate into the Assets pane.
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>7. 🩺 Diagnostics & Debug</b></summary>
|
||||
<details>
|
||||
<summary><b>🩺 Diagnostics & Debug</b></summary>
|
||||
|
||||
- **`Export Debug Profile`** (*Button: `📥 Export Debug Profile`*): Exports non-sensitive system environment details (OS, Python, PyTorch, LeafFlow settings, local cache counts) to a JSON file to share when reporting bugs or requesting assistance. Sensitive API keys and tokens are never exported.
|
||||
- **`Export Debug Profile`** (*Button: `📥 Export Debug Profile`*): Exports non-sensitive system environment details (OS, Python, PyTorch, SaturnNodes settings, local cache counts) to a JSON file to share when reporting bugs or requesting assistance. Sensitive API keys and tokens are never exported.
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>8. 🎨 Batch Queue Visuals</b></summary>
|
||||
<details>
|
||||
<summary><b>🎨 Batch Queue Visuals</b></summary>
|
||||
|
||||
- **`Show Batch Queue 1D Lines`** (`boolean`, *Default: true*): Renders a 1D git-graph style colored line on the left side of queued items in the queue list.
|
||||
- **36 Cycling Colors:** Every queued batch is assigned a vibrant, high-contrast color from a 36-color sequence.
|
||||
- **Clean 1D Line:** The first item starts with a top curve (`┌`), middle items are straight lines (`│`), and the last item ends with a bottom curve (`└`). Standalone 1-item batches are rounded at both ends (`(`).
|
||||
- **In-between Infiltration Detection:** If a different prompt is queued in-between items of a batch, the interrupted batch maintains its open line ends without false curves, while the inserted item is highlighted with its own bracket.
|
||||
- **Works Standalone or with PersistentQueue:** Operates client-side via `localStorage` when standalone, and syncs with `PersistentQueue` to retain batch relationships after server restarts.
|
||||
- **`Batch Graph Snapshot Guard`** (`boolean`, *Default: true*): Background prompt and node input snapshotting ensuring canvas edits don't mutate active batches.
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary><b>9. 🛡️ Security & Script Execution</b></summary>
|
||||
<details>
|
||||
<summary><b>🛡️ Security & Script Execution</b></summary>
|
||||
|
||||
- **`Allow Local File Execution`** (`boolean`, *Default: false*): Master toggle controlling whether the *Run Local File* node is permitted to execute local scripts or executables. Disabled by default for operator safety.
|
||||
- **Strict Confinement**: Execution is strictly limited to the `ComfyUI/scripts/` directory. Absolute paths and directory traversal (`..`) are permanently blocked.
|
||||
@@ -397,15 +408,15 @@ Configure options directly under ComfyUI Settings (⚙ gear icon):
|
||||
|
||||
## 🔒 Security & Privacy
|
||||
|
||||
- **Local Runner Sandbox & Operator Authorization Gate:** The `Run Local File` node enforces single-use interactive operator authorization (`[⚡ Authorize Run]`), strictly confines execution to `ComfyUI/scripts/`, never uses `shell=True`, terminates whole process trees on timeout or cancellation, and **never persists authorization inside workflow JSON or image metadata**.
|
||||
- **CSRF Defense & Fetch Metadata Validation:** All administrative endpoints (`/leafflow/settings`, `/leafflow/power/*`, `/leafflow/local_runner/*`, `/leafflow/scrapes/clear`) enforce loopback checks, `Sec-Fetch-Site` header checks (blocking cross-site requests from visited web pages), and cryptographic per-session `X-LeafFlow-CSRF-Token` headers.
|
||||
- **Local Runner Sandbox & Operator Authorization Gate:** The `Run Local File` node enforces single-use interactive operator authorization (`⚡ Authorize Run (5 min)`), strictly confines execution to `ComfyUI/scripts/`, never uses `shell=True`, terminates whole process trees on timeout or cancellation, and **never persists authorization inside workflow JSON or image metadata**.
|
||||
- **CSRF Defense & Fetch Metadata Validation:** All administrative endpoints (`/saturnnodes/settings`, `/saturnnodes/power/*`, `/saturnnodes/local_runner/*`, `/saturnnodes/scrapes/clear` and backward-compatible `/leafflow/*` aliases) enforce loopback checks, `Sec-Fetch-Site` header checks (blocking cross-site requests from visited web pages), and cryptographic per-session `X-SaturnNodes-CSRF-Token` headers.
|
||||
- **Two-Step Ephemeral Ticket Handshake for Power Actions:** Process restart and shutdown controls are disabled by default (`ALLOW_PROCESS_MANAGEMENT=false`), require CSRF authentication, and execute only after a single-use 30-second ephemeral ticket confirmation handshake.
|
||||
- **Universal Loopback Protection:** All administrative and queue endpoints strictly verify loopback origin (`127.0.0.1` / `::1`), rejecting external network requests with `403 Forbidden`.
|
||||
- **Directory Traversal Protection:** Image loading and thumbnail routes are strictly confined to ComfyUI `input`, `output`, and `temp` directories with traversal guards.
|
||||
- **Optional Headless Dependencies:** System tray desktop integration (`pystray`) is an optional dependency, ensuring cloud containers (Docker, Colab, RunPod) run with zero dependency errors.
|
||||
- **Opt-In Scraping:** Civitai and TMDB network scraping are opt-in and disabled by default.
|
||||
- **Zero Runtime Package Installs:** No `install.py` or dynamic `pip` execution; all standard dependencies are cleanly declared in `requirements.txt`.
|
||||
- **Local Environment:** All API keys and settings are stored locally in `.env` (excluded from git commits via `.gitignore`).
|
||||
- **Local Environment:** All API keys and settings are stored locally in `.env` inside `user/default/SaturnNodes/` (auto-migrated from legacy `LeafFlow/` folder if present).
|
||||
- **Atomic File Writes:** Safe atomic writes (`.tmp` + `os.replace`) prevent JSON corruption during sudden crashes.
|
||||
|
||||
---
|
||||
@@ -415,7 +426,7 @@ Configure options directly under ComfyUI Settings (⚙ gear icon):
|
||||
1. Open your terminal and navigate to your ComfyUI `custom_nodes` directory:
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/KOFiblto/ComfyUI-LeafFlow.git
|
||||
git clone https://github.com/KOFiblto/ComfyUI-SaturnNodes.git
|
||||
```
|
||||
2. Restart ComfyUI.
|
||||
3. Open ComfyUI Settings (⚙ gear menu) to configure API keys or feature toggles if desired.
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
We take the security of ComfyUI-LeafFlow seriously. If you discover a security vulnerability or sensitive key exposure, please **DO NOT** open a public issue.
|
||||
We take the security of ComfyUI-SaturnNodes seriously. If you discover a security vulnerability or sensitive key exposure, please **DO NOT** open a public issue.
|
||||
|
||||
Instead, please report security vulnerabilities directly to the maintainer via GitHub private vulnerability reporting.
|
||||
|
||||
@@ -15,15 +15,15 @@ Instead, please report security vulnerabilities directly to the maintainer via G
|
||||
|
||||
## Security Architecture & Best Practices
|
||||
|
||||
- **CSRF Defense & Fetch Metadata Validation**: All administrative endpoints (`/leafflow/settings`, `/leafflow/power/*`, `/leafflow/local_runner/*`, `/leafflow/scrapes/clear`) enforce multi-layer CSRF protection via `is_authenticated_local_request(request)`.
|
||||
- **CSRF Defense & Fetch Metadata Validation**: All administrative endpoints (`/saturnnodes/*`, `/leafflow/*`) enforce multi-layer CSRF protection via `is_authenticated_local_request(request)`.
|
||||
- Enforces loopback origin checks (`127.0.0.1` / `::1`), rejecting remote calls with `403 Forbidden`.
|
||||
- Rejects `cross-site` requests via `Sec-Fetch-Site` header checks, preventing external malicious web pages visited by the operator from triggering actions.
|
||||
- Verifies `Origin` and `Referer` headers against loopback hostnames.
|
||||
- Requires cryptographically random session tokens in the `X-LeafFlow-CSRF-Token` header (`secrets.compare_digest`), prompting CORS preflight rejection if an external origin attempts unauthorized requests.
|
||||
- Requires cryptographically random session tokens in the `X-SaturnNodes-CSRF-Token` / `X-LeafFlow-CSRF-Token` header (`secrets.compare_digest`), prompting CORS preflight rejection if an external origin attempts unauthorized requests.
|
||||
- **Two-Step Ephemeral Ticket Handshake for Power Actions**:
|
||||
- Server restart and shutdown endpoints require `ALLOW_PROCESS_MANAGEMENT=true` in settings.
|
||||
- Execution requires a two-step handshake: the client requests a single-use ephemeral ticket (`POST /leafflow/power/request_token`) valid for 30 seconds.
|
||||
- The confirmation endpoint (`POST /leafflow/power/confirm_action`) consumes the single-use ticket in server memory before executing restart or shutdown, preventing arbitrary or cross-site triggers.
|
||||
- Execution requires a two-step handshake: the client requests a single-use ephemeral ticket (`POST /saturnnodes/power/request_token`) valid for 30 seconds.
|
||||
- The confirmation endpoint (`POST /saturnnodes/power/confirm_action`) consumes the single-use ticket in server memory before executing restart or shutdown, preventing arbitrary or cross-site triggers.
|
||||
- **Strict Local Script Confinement**:
|
||||
- `RunLocalFileNode` strictly confines executable scripts and binaries to `ComfyUI/scripts/`.
|
||||
- Absolute paths (e.g. `C:\`, `/etc/`, `\\server`) and directory traversal sequences (`..`) are strictly rejected.
|
||||
|
||||
+43
-44
@@ -4,7 +4,7 @@ import subprocess
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
|
||||
__version__ = "2.3.1"
|
||||
__version__ = "2.3.2"
|
||||
|
||||
from .nodes.queue_control import setup_queue_control_routes, tray_manager
|
||||
from .nodes.lora_loader import (
|
||||
@@ -17,15 +17,16 @@ from .nodes.image_loader import VisualImageLoader, ImageLoaderVisualPrettyV2
|
||||
from .nodes.auto_watcher import LoadImageFromFolder
|
||||
from .nodes.load_recent import LoadRecentOutputs
|
||||
from .nodes.preview_latent import PreviewLatentLiveNode
|
||||
from .nodes.decision_node import LeafFlowDecision
|
||||
from .nodes.decision_node import SaturnDecision
|
||||
from .nodes.aspect_ratio import TextAspectRatioFinder, AspectRatioFinder, PreviewImageSizeAspectRatio
|
||||
from .nodes.lora_finder import TextLoraFinder, LoraTextFinder
|
||||
from .nodes.prompt_iterator import PromptQueueIterator
|
||||
from .nodes.prompt_counter import PromptCounter
|
||||
from .nodes.text_replacer import MultiTextReplacer
|
||||
from .nodes.text_split import LeafFlowTextSplit
|
||||
from .nodes.text_split import SaturnTextSplit
|
||||
from .nodes.local_runner import RunLocalFileNode, setup_local_runner_routes
|
||||
from .nodes.utils import (
|
||||
get_saturnnodes_user_dir,
|
||||
get_leafflow_user_dir,
|
||||
get_env_setting,
|
||||
is_local_request,
|
||||
@@ -42,34 +43,34 @@ NODE_CLASS_MAPPINGS = {
|
||||
"LoadImageFromFolder": LoadImageFromFolder,
|
||||
"LoadRecentOutputs": LoadRecentOutputs,
|
||||
"PreviewLatentLive": PreviewLatentLiveNode,
|
||||
"LeafFlowDecision": LeafFlowDecision,
|
||||
"SaturnDecision": SaturnDecision,
|
||||
"TextAspectRatioFinder": TextAspectRatioFinder,
|
||||
"PreviewImageSizeAspectRatio": PreviewImageSizeAspectRatio,
|
||||
"TextLoraFinder": TextLoraFinder,
|
||||
"PromptQueueIterator": PromptQueueIterator,
|
||||
"PromptCounter": PromptCounter,
|
||||
"MultiTextReplacer": MultiTextReplacer,
|
||||
"LeafFlowTextSplit": LeafFlowTextSplit,
|
||||
"SaturnTextSplit": SaturnTextSplit,
|
||||
"RunLocalFileNode": RunLocalFileNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FolderLoraLoader": "🍃 📁 LoRA Loader (Folder)",
|
||||
"FolderLoraLoaderPretty": "🍃 ✨ LoRA Loader (Pretty)",
|
||||
"VisualLoraLoader": "🍃 🖼️ Visual LoRA Loader",
|
||||
"VisualImageLoader": "🍃 📷 Visual Image Loader",
|
||||
"LoadImageFromFolder": "🍃 📂 Load Image From Folder",
|
||||
"LoadRecentOutputs": "🍃 ⏱️ Recent Outputs",
|
||||
"PreviewLatentLive": "🍃 👁️ Live Latent Preview",
|
||||
"LeafFlowDecision": "🍃 ⏸️ LeafFlow Decision",
|
||||
"TextAspectRatioFinder": "🍃 📐 Text Aspect Ratio Finder",
|
||||
"PreviewImageSizeAspectRatio": "🍃 📐 Preview Image Size & Aspect Ratio",
|
||||
"TextLoraFinder": "🍃 🔎 Text LoRA Finder & Loader",
|
||||
"PromptQueueIterator": "🍃 🔄 Prompt Queue Iterator",
|
||||
"PromptCounter": "🍃 📝 Prompt Counter",
|
||||
"MultiTextReplacer": "🍃 🔤 Multi Text Replacer",
|
||||
"LeafFlowTextSplit": "🍃 ✂️ Text Split",
|
||||
"RunLocalFileNode": "🍃 ⚡ Run Local File"
|
||||
"FolderLoraLoader": "🪐 📁 LoRA Loader (Folder)",
|
||||
"FolderLoraLoaderPretty": "🪐 ✨ LoRA Loader (Pretty)",
|
||||
"VisualLoraLoader": "🪐 🖼️ Visual LoRA Loader",
|
||||
"VisualImageLoader": "🪐 📷 Visual Image Loader",
|
||||
"LoadImageFromFolder": "🪐 📂 Load Image From Folder",
|
||||
"LoadRecentOutputs": "🪐 ⏱️ Recent Outputs",
|
||||
"PreviewLatentLive": "🪐 👁️ Live Latent Preview",
|
||||
"SaturnDecision": "🪐 ⏸️ Saturn Decision",
|
||||
"TextAspectRatioFinder": "🪐 📐 Text Aspect Ratio Finder",
|
||||
"PreviewImageSizeAspectRatio": "🪐 📐 Preview Image Size & Aspect Ratio",
|
||||
"TextLoraFinder": "🪐 🔎 Text LoRA Finder & Loader",
|
||||
"PromptQueueIterator": "🪐 🔄 Prompt Queue Iterator",
|
||||
"PromptCounter": "🪐 📝 Prompt Counter",
|
||||
"MultiTextReplacer": "🪐 🔤 Multi Text Replacer",
|
||||
"SaturnTextSplit": "🪐 ✂️ Text Split",
|
||||
"RunLocalFileNode": "🪐 ⚡ Run Local File"
|
||||
}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
@@ -79,31 +80,31 @@ server = PromptServer.instance
|
||||
setup_queue_control_routes(server)
|
||||
setup_local_runner_routes(server)
|
||||
|
||||
try:
|
||||
from .prompt_bookmarks.api import register_routes as register_prompt_bookmarks_routes
|
||||
register_prompt_bookmarks_routes()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
USER_DIR = get_leafflow_user_dir()
|
||||
USER_DIR = get_saturnnodes_user_dir()
|
||||
ENV_FILE = os.path.join(USER_DIR, ".env")
|
||||
|
||||
routes = server.routes
|
||||
|
||||
try:
|
||||
print(f"[ComfyUI-LeafFlow] 🍃 Loaded {len(NODE_CLASS_MAPPINGS)} nodes & visual endpoints successfully (v{__version__}).")
|
||||
except UnicodeEncodeError:
|
||||
print(f"[ComfyUI-LeafFlow] Loaded {len(NODE_CLASS_MAPPINGS)} nodes & visual endpoints successfully (v{__version__}).")
|
||||
can_encode_saturn = True
|
||||
try:
|
||||
"🪐".encode(sys.stdout.encoding or "ascii")
|
||||
except Exception:
|
||||
can_encode_saturn = False
|
||||
saturn_symbol = "🪐 " if can_encode_saturn else ""
|
||||
print(f"[ComfyUI-SaturnNodes] {saturn_symbol}Loaded {len(NODE_CLASS_MAPPINGS)} nodes & visual endpoints successfully (v{__version__}).")
|
||||
except Exception:
|
||||
print(f"[ComfyUI-SaturnNodes] Loaded {len(NODE_CLASS_MAPPINGS)} nodes & visual endpoints successfully (v{__version__}).")
|
||||
|
||||
@routes.get("/leafflow/auth/token")
|
||||
@routes.get("/saturnnodes/auth/token")
|
||||
async def get_csrf_token_endpoint(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
|
||||
return web.json_response({"csrf_token": get_csrf_token()})
|
||||
|
||||
@routes.get("/leafflow/get_image_prompt")
|
||||
@routes.get("/leafflow/view_image_prompt")
|
||||
@routes.get("/saturnnodes/get_image_prompt")
|
||||
@routes.get("/saturnnodes/view_image_prompt")
|
||||
async def get_image_prompt_endpoint(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"success": False, "error": "Forbidden: Local access only"}, status=403)
|
||||
@@ -145,8 +146,7 @@ async def get_image_prompt_endpoint(request):
|
||||
except Exception as e:
|
||||
return web.json_response({"success": False, "error": str(e)}, status=500)
|
||||
|
||||
@routes.get("/leafflow/settings")
|
||||
@routes.get("/flow_control/settings")
|
||||
@routes.get("/saturnnodes/settings")
|
||||
async def get_settings(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
|
||||
@@ -196,8 +196,7 @@ def _clean_env_val(v):
|
||||
return None
|
||||
return str(v).replace("\n", "").replace("\r", "").strip()
|
||||
|
||||
@routes.post("/leafflow/settings")
|
||||
@routes.post("/flow_control/settings")
|
||||
@routes.post("/saturnnodes/settings")
|
||||
async def save_settings(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
@@ -352,11 +351,11 @@ async def save_settings(request):
|
||||
os.environ["CLEAR_PROMPT_ITERATOR_ON_LAUNCH"] = str(clear_prompt_iterator_on_launch)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error saving settings to .env: {e}")
|
||||
print(f"[SaturnNodes] 🪐 Error saving settings to .env: {e}")
|
||||
|
||||
return web.json_response({"status": "ok"})
|
||||
|
||||
@routes.post("/leafflow/scrapes/clear")
|
||||
@routes.post("/saturnnodes/scrapes/clear")
|
||||
async def clear_scrapes_endpoint(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
@@ -366,10 +365,10 @@ async def clear_scrapes_endpoint(request):
|
||||
f.write("{}")
|
||||
return web.json_response({"status": "ok"})
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error clearing scrapes cache: {e}")
|
||||
print(f"[SaturnNodes] 🪐 Error clearing scrapes cache: {e}")
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@routes.get("/leafflow/debug/export")
|
||||
@routes.get("/saturnnodes/debug/export")
|
||||
async def export_debug_profile(request):
|
||||
try:
|
||||
import platform
|
||||
@@ -409,7 +408,7 @@ async def export_debug_profile(request):
|
||||
|
||||
debug_profile = {
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"package": "ComfyUI-LeafFlow",
|
||||
"package": "ComfyUI-SaturnNodes",
|
||||
"version": __version__,
|
||||
"system": {
|
||||
"os": platform.system(),
|
||||
@@ -424,7 +423,7 @@ async def export_debug_profile(request):
|
||||
"registered_nodes_count": len(NODE_CLASS_MAPPINGS),
|
||||
"settings": {
|
||||
"enable_persistent_queue": get_env_setting("ENABLE_PERSISTENT_QUEUE", "true"),
|
||||
"default_pause_state": get_env_setting("DEFAULT_PAUSE_STATE", "Paused"),
|
||||
"default_pause_state": get_env_setting("DEFAULT_PAUSE_STATE", "Running"),
|
||||
"default_pause_mode": get_env_setting("DEFAULT_PAUSE_MODE", "after_finish"),
|
||||
"enable_civitai_scraping": get_env_setting("ENABLE_CIVITAI_SCRAPING", "true"),
|
||||
"enable_tmdb_scraping": get_env_setting("ENABLE_TMDB_SCRAPING", "true"),
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
# Package marker for LeafFlow nodes
|
||||
# Package marker for SaturnNodes nodes
|
||||
|
||||
@@ -59,7 +59,7 @@ class TextAspectRatioFinder:
|
||||
RETURN_TYPES = ("INT", "INT", "STRING")
|
||||
RETURN_NAMES = ("width", "height", "aspect_ratio")
|
||||
FUNCTION = "find_aspect_ratio"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
DESCRIPTION = "Searches text for aspect ratios (e.g. 16:9), syntax checks them, and calculates width & height for target megapixels."
|
||||
|
||||
def find_aspect_ratio(
|
||||
@@ -147,11 +147,11 @@ class TextAspectRatioFinder:
|
||||
raise ValueError(f"Cannot parse aspect ratio: {found_ratio}")
|
||||
except Exception as e:
|
||||
err_msg = f"Aspect ratio '{found_ratio}' is invalid. Falling back to '1:1'."
|
||||
print(f"[LeafFlow] Warning: {err_msg} ({e})")
|
||||
print(f"[SaturnNodes] Warning: {err_msg} ({e})")
|
||||
try:
|
||||
from server import PromptServer
|
||||
if hasattr(PromptServer, "instance") and PromptServer.instance:
|
||||
PromptServer.instance.send_sync("leafflow_node_error_state", {
|
||||
PromptServer.instance.send_sync("saturnnodes_node_error_state", {
|
||||
"node_id": str(unique_id or ""),
|
||||
"title": "Aspect Ratio Warning",
|
||||
"message": err_msg,
|
||||
@@ -196,7 +196,7 @@ class PreviewImageSizeAspectRatio:
|
||||
RETURN_TYPES = ()
|
||||
RETURN_NAMES = ()
|
||||
FUNCTION = "process_preview"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
OUTPUT_NODE = True
|
||||
DESCRIPTION = "Visual preview node that displays image aspect ratio and dimension summary."
|
||||
|
||||
@@ -241,7 +241,7 @@ class PreviewImageSizeAspectRatio:
|
||||
display_text = f"1 x {h_disp}"
|
||||
|
||||
try:
|
||||
PromptServer.instance.send_sync("leafflow_update_preview_aspect_ratio", {
|
||||
PromptServer.instance.send_sync("saturnnodes_update_preview_aspect_ratio", {
|
||||
"node_id": str(unique_id),
|
||||
"ratio": calc_ratio,
|
||||
"display_text": display_text
|
||||
|
||||
@@ -231,7 +231,7 @@ class AssetsRestoreManager:
|
||||
}
|
||||
|
||||
if restored_count > 0:
|
||||
print(f"[LeafFlow] Restored {restored_count} recent image(s) into Assets / History pane.")
|
||||
print(f"[SaturnNodes] Restored {restored_count} recent image(s) into Assets / History pane.")
|
||||
if server:
|
||||
try:
|
||||
server.queue_updated()
|
||||
|
||||
@@ -51,7 +51,7 @@ class LoadImageFromFolder:
|
||||
RETURN_TYPES = ("IMAGE", "BOOLEAN")
|
||||
RETURN_NAMES = ("image", "has_image")
|
||||
FUNCTION = "watch"
|
||||
CATEGORY = "🍃 LeafFlow/Automation"
|
||||
CATEGORY = "🪐 SaturnNodes/Automation"
|
||||
DESCRIPTION = "Loads an image from a folder, optionally waiting if the folder is empty, with automatic sequential cycling when delete is False."
|
||||
|
||||
@classmethod
|
||||
@@ -73,7 +73,7 @@ class LoadImageFromFolder:
|
||||
try:
|
||||
os.remove(filepath)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Warning: Failed to remove processed file '{filepath}': {e}")
|
||||
print(f"[SaturnNodes] Warning: Failed to remove processed file '{filepath}': {e}")
|
||||
|
||||
return img_tensor
|
||||
|
||||
@@ -82,7 +82,7 @@ class LoadImageFromFolder:
|
||||
try:
|
||||
regex = re.compile(regex_filter)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Invalid regex: {e}. Falling back to .*")
|
||||
print(f"[SaturnNodes] Invalid regex: {e}. Falling back to .*")
|
||||
regex = re.compile(".*")
|
||||
|
||||
files = []
|
||||
@@ -164,9 +164,9 @@ class LoadImageFromFolder:
|
||||
img_tensor = self.load_and_remove_image(filepath, delete_image=delete_image)
|
||||
return (img_tensor, True)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error processing {filepath}: {e}")
|
||||
print(f"[SaturnNodes] Error processing {filepath}: {e}")
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error accessing directory {folder}: {e}")
|
||||
print(f"[SaturnNodes] Error accessing directory {folder}: {e}")
|
||||
|
||||
time.sleep(rescan_interval)
|
||||
else:
|
||||
@@ -180,9 +180,9 @@ class LoadImageFromFolder:
|
||||
img_tensor = self.load_and_remove_image(filepath, delete_image=delete_image)
|
||||
return (img_tensor, True)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error processing {filepath}: {e}")
|
||||
print(f"[SaturnNodes] Error processing {filepath}: {e}")
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error accessing directory {folder}: {e}")
|
||||
print(f"[SaturnNodes] Error accessing directory {folder}: {e}")
|
||||
|
||||
# No image present or folder missing -> return standard dummy tensor & False immediately
|
||||
return (self.create_dummy_image(), False)
|
||||
|
||||
+18
-15
@@ -38,7 +38,7 @@ class DecisionManager:
|
||||
return False
|
||||
|
||||
|
||||
class LeafFlowDecision:
|
||||
class SaturnDecision:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -55,7 +55,7 @@ class LeafFlowDecision:
|
||||
RETURN_TYPES = ("BOOLEAN",)
|
||||
RETURN_NAMES = ("cancel",)
|
||||
FUNCTION = "decide"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
DESCRIPTION = "Pauses execution and waits for your input via the UI buttons.\n\n- 'Continue' outputs False (0).\n- 'Cancel' outputs True (1) so you can route it into a Switch node to bypass later nodes.\n- 'Stop Workflow' instantly aborts the entire ComfyUI generation queue.\n- 'OS Notification': Sends a native desktop toast (Windows/macOS/Linux) when waiting."
|
||||
|
||||
def send_notification(self, title, message):
|
||||
@@ -97,7 +97,7 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
# Linux dbus notify-send
|
||||
subprocess.Popen(["notify-send", title, message])
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Failed to send OS notification: {e}")
|
||||
print(f"[SaturnNodes] Failed to send OS notification: {e}")
|
||||
|
||||
def decide(self, disable, send_os_notification, timeout, unique_id=None):
|
||||
if disable:
|
||||
@@ -107,10 +107,10 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
return (False,)
|
||||
|
||||
# Notify frontend
|
||||
PromptServer.instance.send_sync("leafflow_decision_waiting", {"node_id": unique_id})
|
||||
PromptServer.instance.send_sync("saturnnodes_decision_waiting", {"node_id": unique_id})
|
||||
|
||||
if send_os_notification:
|
||||
self.send_notification("ComfyUI LeafFlow", "Workflow paused! Waiting for your decision.")
|
||||
self.send_notification("ComfyUI SaturnNodes", "Workflow paused! Waiting for your decision.")
|
||||
|
||||
# Wait for user input
|
||||
event = threading.Event()
|
||||
@@ -124,10 +124,10 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
event.wait(0.5)
|
||||
if not event.is_set():
|
||||
print(f"[LeafFlow] Node {unique_id} timed out. Auto-continuing...")
|
||||
print(f"[SaturnNodes] Node {unique_id} timed out. Auto-continuing...")
|
||||
DecisionManager.unregister_wait(unique_id)
|
||||
# Notify frontend to update UI back to normal
|
||||
PromptServer.instance.send_sync("leafflow_decision_resolved", {"node_id": unique_id})
|
||||
PromptServer.instance.send_sync("saturnnodes_decision_resolved", {"node_id": unique_id})
|
||||
return (False,)
|
||||
else:
|
||||
while not event.is_set():
|
||||
@@ -135,20 +135,20 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
event.wait(0.5)
|
||||
except Exception:
|
||||
DecisionManager.unregister_wait(unique_id)
|
||||
PromptServer.instance.send_sync("leafflow_decision_resolved", {"node_id": unique_id})
|
||||
PromptServer.instance.send_sync("saturnnodes_decision_resolved", {"node_id": unique_id})
|
||||
raise
|
||||
|
||||
# Retrieve action
|
||||
action = DecisionManager.get_action(unique_id)
|
||||
|
||||
# Notify frontend that the decision has been resolved
|
||||
PromptServer.instance.send_sync("leafflow_decision_resolved", {"node_id": unique_id})
|
||||
PromptServer.instance.send_sync("saturnnodes_decision_resolved", {"node_id": unique_id})
|
||||
|
||||
if action == "cancel":
|
||||
print(f"[LeafFlow] Node {unique_id} cancelled.")
|
||||
print(f"[SaturnNodes] Node {unique_id} cancelled.")
|
||||
return (True,)
|
||||
elif action == "stop":
|
||||
print(f"[LeafFlow] Node {unique_id} stopped workflow.")
|
||||
print(f"[SaturnNodes] Node {unique_id} stopped workflow.")
|
||||
try:
|
||||
# Attempt to cancel current execution queue
|
||||
PromptServer.instance.prompt_queue.cancel_current_execution()
|
||||
@@ -160,16 +160,16 @@ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||
pass
|
||||
# Raise exception to halt this thread instantly
|
||||
try:
|
||||
raise comfy.model_management.InterruptProcessingException("Workflow stopped by LeafFlow Decision Node.")
|
||||
raise comfy.model_management.InterruptProcessingException("Workflow stopped by SaturnNodes Decision Node.")
|
||||
except AttributeError:
|
||||
raise Exception("Workflow stopped by LeafFlow Decision Node.")
|
||||
raise Exception("Workflow stopped by SaturnNodes Decision Node.")
|
||||
|
||||
print(f"[LeafFlow] Node {unique_id} continuing.")
|
||||
print(f"[SaturnNodes] Node {unique_id} continuing.")
|
||||
return (False,)
|
||||
|
||||
|
||||
# Register API Route
|
||||
@PromptServer.instance.routes.post("/leafflow/decision")
|
||||
@PromptServer.instance.routes.post("/saturnnodes/decision")
|
||||
async def handle_decision(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
@@ -184,3 +184,6 @@ async def handle_decision(request):
|
||||
return web.json_response({"status": "error", "message": "Node is not waiting."}, status=400)
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
LeafFlowDecision = SaturnDecision
|
||||
SaturnDecision = SaturnDecision
|
||||
|
||||
@@ -14,12 +14,13 @@ from .utils import (
|
||||
get_leafflow_user_dir,
|
||||
is_local_request,
|
||||
is_safe_path,
|
||||
sanitize_image_loader_folder
|
||||
sanitize_image_loader_folder,
|
||||
get_saturnnodes_user_dir
|
||||
)
|
||||
|
||||
IMAGE_CATEGORY = "🍃 LeafFlow/Loaders"
|
||||
IMAGE_CATEGORY = "🪐 SaturnNodes/Loaders"
|
||||
CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
USER_DIR = get_leafflow_user_dir()
|
||||
USER_DIR = get_saturnnodes_user_dir()
|
||||
CACHE_FILE = os.path.join(USER_DIR, "image_prompts_cache.json")
|
||||
|
||||
def decode_bytes_safely(raw_bytes):
|
||||
@@ -148,7 +149,7 @@ def extract_metadata_from_image(filepath):
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error extracting metadata: {e}")
|
||||
print(f"[SaturnNodes] Error extracting metadata: {e}")
|
||||
return positive_prompt, width, height
|
||||
|
||||
def load_prompts_cache():
|
||||
@@ -165,7 +166,7 @@ def save_prompts_cache(cache):
|
||||
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(cache, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Failed to save prompts cache: {e}")
|
||||
print(f"[SaturnNodes] Failed to save prompts cache: {e}")
|
||||
|
||||
routes = PromptServer.instance.routes if hasattr(PromptServer, "instance") and PromptServer.instance is not None else web.RouteTableDef()
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from PIL import Image, ImageOps
|
||||
import folder_paths
|
||||
from .utils import sanitize_folder_path
|
||||
|
||||
LOADER_CATEGORY = "🍃 LeafFlow/Loaders"
|
||||
LOADER_CATEGORY = "🪐 SaturnNodes/Loaders"
|
||||
|
||||
class LoadRecentOutputs:
|
||||
@classmethod
|
||||
@@ -57,7 +57,7 @@ class LoadRecentOutputs:
|
||||
except OSError:
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error scanning output folder '{output_dir}': {e}")
|
||||
print(f"[SaturnNodes] 🍃 Error scanning output folder '{output_dir}': {e}")
|
||||
dummy = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
|
||||
return (dummy,)
|
||||
|
||||
@@ -80,7 +80,7 @@ class LoadRecentOutputs:
|
||||
image = np.array(image).astype(np.float32) / 255.0
|
||||
image = torch.from_numpy(image).unsqueeze(0)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error loading recent output image {target_file}: {e}")
|
||||
print(f"[SaturnNodes] Error loading recent output image {target_file}: {e}")
|
||||
image = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
|
||||
|
||||
return (image,)
|
||||
|
||||
+27
-22
@@ -9,7 +9,7 @@ import folder_paths
|
||||
import comfy.model_management
|
||||
from .utils import get_env_setting, is_local_request, is_authenticated_local_request
|
||||
|
||||
AUTOMATION_CATEGORY = "🍃 LeafFlow/Automation"
|
||||
AUTOMATION_CATEGORY = "🪐 SaturnNodes/Automation"
|
||||
|
||||
def _safe_print(msg):
|
||||
try:
|
||||
@@ -18,7 +18,7 @@ def _safe_print(msg):
|
||||
print(msg.encode("ascii", "replace").decode("ascii"))
|
||||
|
||||
def is_local_file_execution_enabled():
|
||||
"""Checks whether local file execution is explicitly enabled in LeafFlow settings (default False)."""
|
||||
"""Checks whether local file execution is explicitly enabled in SaturnNodes settings (default False)."""
|
||||
val = get_env_setting("ENABLE_LOCAL_FILE_EXECUTION", "false").lower()
|
||||
return val in ["true", "1", "yes"]
|
||||
|
||||
@@ -42,10 +42,12 @@ def is_script_path_permitted(target_path):
|
||||
if not target_path:
|
||||
return False, "Target path is empty."
|
||||
|
||||
scripts_dir = get_allowed_scripts_directory()
|
||||
scripts_dir = os.path.abspath(os.path.realpath(get_allowed_scripts_directory()))
|
||||
try:
|
||||
abs_target = os.path.abspath(os.path.realpath(target_path))
|
||||
if os.path.commonpath([scripts_dir, abs_target]) == scripts_dir:
|
||||
norm_scripts = os.path.normcase(scripts_dir)
|
||||
norm_target = os.path.normcase(abs_target)
|
||||
if os.path.commonpath([norm_scripts, norm_target]) == norm_scripts:
|
||||
if not os.path.exists(abs_target):
|
||||
return False, f"Script does not exist: '{os.path.basename(target_path)}'"
|
||||
if not os.path.isfile(abs_target):
|
||||
@@ -65,7 +67,8 @@ def resolve_target_path(file_path_input):
|
||||
return None, "Script file path is empty."
|
||||
|
||||
# 1. Strictly reject absolute paths (Windows drive letters C:\, UNC \\server, or POSIX /root)
|
||||
if os.path.isabs(clean) or clean.startswith(("\\\\", "//")):
|
||||
has_drive_letter = len(clean) >= 2 and clean[0].isalpha() and clean[1] == ":"
|
||||
if os.path.isabs(clean) or clean.startswith(("\\\\", "//", "/")) or has_drive_letter:
|
||||
return None, f"Security Restriction: Absolute paths are forbidden ('{clean}'). Place your script in ComfyUI/scripts/ and specify a relative name."
|
||||
|
||||
# 2. Strictly reject directory traversal
|
||||
@@ -83,12 +86,14 @@ def resolve_target_path(file_path_input):
|
||||
if not clean_sub:
|
||||
return None, "Invalid script path."
|
||||
|
||||
scripts_base = get_allowed_scripts_directory()
|
||||
scripts_base = os.path.abspath(os.path.realpath(get_allowed_scripts_directory()))
|
||||
full_target = os.path.abspath(os.path.realpath(os.path.join(scripts_base, clean_sub)))
|
||||
|
||||
# 4. Canonical commonpath confinement
|
||||
try:
|
||||
if os.path.commonpath([scripts_base, full_target]) != scripts_base:
|
||||
norm_base = os.path.normcase(scripts_base)
|
||||
norm_target = os.path.normcase(full_target)
|
||||
if os.path.commonpath([norm_base, norm_target]) != norm_base:
|
||||
return None, f"Security Restriction: Script path escapes ComfyUI/scripts/ ('{clean}')."
|
||||
except Exception as e:
|
||||
return None, f"Security Restriction: Path validation error: {e}"
|
||||
@@ -156,13 +161,13 @@ def clear_all_authorizations():
|
||||
def setup_local_runner_routes(server):
|
||||
routes = server.routes
|
||||
|
||||
@routes.post("/leafflow/local_runner/authorize")
|
||||
@routes.post("/saturnnodes/local_runner/authorize")
|
||||
async def authorize_endpoint(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
if not is_local_file_execution_enabled():
|
||||
return web.json_response({
|
||||
"error": "Local file execution is disabled. Enable 'Allow Local File Execution' in LeafFlow settings first.",
|
||||
"error": "Local file execution is disabled. Enable 'Allow Local File Execution' in SaturnNodes settings first.",
|
||||
"enabled": False
|
||||
}, status=403)
|
||||
try:
|
||||
@@ -180,7 +185,7 @@ def setup_local_runner_routes(server):
|
||||
"expires_in": 300
|
||||
})
|
||||
|
||||
@routes.get("/leafflow/local_runner/status")
|
||||
@routes.get("/saturnnodes/local_runner/status")
|
||||
async def status_endpoint(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
|
||||
@@ -319,16 +324,16 @@ class RunLocalFileNode:
|
||||
if not is_local_file_execution_enabled():
|
||||
err = (
|
||||
"Execution Blocked: Local file execution is disabled by default for security. "
|
||||
"To run local files, enable 'Allow Local File Execution' in ComfyUI Settings -> LeafFlow -> Security."
|
||||
"To run local files, enable 'Allow Local File Execution' in ComfyUI Settings -> SaturnNodes -> Security."
|
||||
)
|
||||
_safe_print(f"[LeafFlow LocalRunner] {err}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] {err}")
|
||||
return ("", err, -1, False, passthrough_out)
|
||||
|
||||
# Check 2: Path Resolution & Strict Confinement to ComfyUI/scripts/
|
||||
resolved_path, path_msg = resolve_target_path(file_path)
|
||||
if not resolved_path:
|
||||
err = f"Execution Error: {path_msg}"
|
||||
_safe_print(f"[LeafFlow LocalRunner] {err}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] {err}")
|
||||
return ("", err, -1, False, passthrough_out)
|
||||
|
||||
# Check 3: Mandatory In-Memory Operator Authorization Gate
|
||||
@@ -336,7 +341,7 @@ class RunLocalFileNode:
|
||||
authorized, auth_msg = consume_node_authorization(node_id, resolved_path)
|
||||
if not authorized:
|
||||
err = f"Execution Blocked: {auth_msg}"
|
||||
_safe_print(f"[LeafFlow LocalRunner] {err}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] {err}")
|
||||
return ("", err, -1, False, passthrough_out)
|
||||
|
||||
# Determine working directory (confined to ComfyUI/scripts/ or script directory)
|
||||
@@ -348,12 +353,12 @@ class RunLocalFileNode:
|
||||
if os.path.commonpath([scripts_base, cand_cwd]) == scripts_base and os.path.isdir(cand_cwd):
|
||||
cwd = cand_cwd
|
||||
else:
|
||||
_safe_print(f"[LeafFlow LocalRunner] Warning: working_directory '{working_directory}' outside scripts folder. Defaulting to script folder.")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] Warning: working_directory '{working_directory}' outside scripts folder. Defaulting to script folder.")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
cmd_list = self.build_command_args(resolved_path, parameters)
|
||||
_safe_print(f"[LeafFlow LocalRunner] Executing '{os.path.basename(resolved_path)}' (mode: {run_mode}, cwd: '{cwd}')...")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] Executing '{os.path.basename(resolved_path)}' (mode: {run_mode}, cwd: '{cwd}')...")
|
||||
|
||||
# Mode: Asynchronous (Background Detached)
|
||||
if "Asynchronous" in run_mode:
|
||||
@@ -366,11 +371,11 @@ class RunLocalFileNode:
|
||||
subprocess.Popen(cmd_list, cwd=cwd, start_new_session=True)
|
||||
|
||||
msg = f"Started in background: '{os.path.basename(resolved_path)}' (PID assigned)"
|
||||
_safe_print(f"[LeafFlow LocalRunner] {msg}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] {msg}")
|
||||
return (msg, "", 0, True, passthrough_out)
|
||||
except Exception as e:
|
||||
err_msg = f"Failed to launch background process: {str(e)}"
|
||||
_safe_print(f"[LeafFlow LocalRunner] Error: {err_msg}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] Error: {err_msg}")
|
||||
return ("", err_msg, -1, False, passthrough_out)
|
||||
|
||||
# Mode: Synchronous (Wait for Process with Output & Interrupt Polling)
|
||||
@@ -413,13 +418,13 @@ class RunLocalFileNode:
|
||||
comfy.model_management.throw_exception_if_processing_interrupted()
|
||||
except Exception:
|
||||
_kill_tree()
|
||||
_safe_print(f"[LeafFlow LocalRunner] Execution of '{os.path.basename(resolved_path)}' interrupted by user.")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] Execution of '{os.path.basename(resolved_path)}' interrupted by user.")
|
||||
raise
|
||||
|
||||
if effective_timeout and (time.time() - start_time) > effective_timeout:
|
||||
_kill_tree()
|
||||
err = f"Execution timed out after {timeout} seconds."
|
||||
_safe_print(f"[LeafFlow LocalRunner] {err}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] {err}")
|
||||
return ("", err, -1, False, passthrough_out)
|
||||
|
||||
time.sleep(0.1)
|
||||
@@ -428,10 +433,10 @@ class RunLocalFileNode:
|
||||
exit_code = proc.returncode
|
||||
success = (exit_code == 0)
|
||||
|
||||
_safe_print(f"[LeafFlow LocalRunner] Completed with exit code {exit_code} (success: {success}).")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] Completed with exit code {exit_code} (success: {success}).")
|
||||
return (stdout_data or "", stderr_data or "", exit_code, success, passthrough_out)
|
||||
|
||||
except Exception as e:
|
||||
err_msg = f"Process execution error: {str(e)}"
|
||||
_safe_print(f"[LeafFlow LocalRunner] {err_msg}")
|
||||
_safe_print(f"[SaturnNodes LocalRunner] {err_msg}")
|
||||
return ("", err_msg, -1, False, passthrough_out)
|
||||
|
||||
@@ -99,7 +99,7 @@ class TextLoraFinder:
|
||||
matches.append((m.start(), display_name, sys_path))
|
||||
seen_paths.add(sys_path)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Invalid custom regex pattern '{custom_regex}': {e}")
|
||||
print(f"[SaturnNodes] 🍃 Invalid custom regex pattern '{custom_regex}': {e}")
|
||||
else:
|
||||
for display_name, sys_path in mapping.items():
|
||||
if display_name in ["[ NONE ]", "[ RANDOM ]"] or not sys_path or sys_path == "[ NONE ]":
|
||||
@@ -150,7 +150,7 @@ class TextLoraFinder:
|
||||
)
|
||||
increment_lora_usage(sys_path)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error loading LoRA '{sys_path}': {e}")
|
||||
print(f"[SaturnNodes] 🍃 Error loading LoRA '{sys_path}': {e}")
|
||||
continue
|
||||
else:
|
||||
increment_lora_usage(sys_path)
|
||||
|
||||
+11
-10
@@ -18,12 +18,13 @@ from .utils import (
|
||||
parse_pretty_name_with_version,
|
||||
format_lora_output_name,
|
||||
sanitize_folder_path,
|
||||
get_saturnnodes_user_dir,
|
||||
get_leafflow_user_dir,
|
||||
is_local_request,
|
||||
is_safe_path
|
||||
)
|
||||
|
||||
LORA_CATEGORY = "🍃 LeafFlow/Loaders"
|
||||
LORA_CATEGORY = "🪐 SaturnNodes/Loaders"
|
||||
LORA_OUTPUT_FORMAT_CHOICES = [
|
||||
"Parsed Name",
|
||||
"Filename",
|
||||
@@ -34,7 +35,7 @@ LORA_OUTPUT_FORMAT_CHOICES = [
|
||||
]
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
USER_DIR = get_leafflow_user_dir()
|
||||
USER_DIR = get_saturnnodes_user_dir()
|
||||
ENV_FILE = os.path.join(USER_DIR, ".env")
|
||||
LORA_STATE_FILE = os.path.join(USER_DIR, "lora_loader_state.json")
|
||||
|
||||
@@ -62,7 +63,7 @@ def save_lora_cycle_state(state):
|
||||
with open(LORA_STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(state, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error saving lora loader cycle state: {e}")
|
||||
print(f"[SaturnNodes] Error saving lora loader cycle state: {e}")
|
||||
|
||||
def get_api_keys():
|
||||
civitai_key = os.getenv("CIVITAI_API_KEY", "")
|
||||
@@ -98,7 +99,7 @@ def load_usage_data():
|
||||
else:
|
||||
usage_data[k] = usage_data.get(k, 0) + v
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error loading usage data: {e}")
|
||||
print(f"[SaturnNodes] Error loading usage data: {e}")
|
||||
return usage_data
|
||||
|
||||
def increment_lora_usage(lora_path):
|
||||
@@ -114,7 +115,7 @@ def increment_lora_usage(lora_path):
|
||||
with open(usage_file, "w", encoding="utf-8") as f:
|
||||
json.dump(usage_data, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error updating lora usage: {e}")
|
||||
print(f"[SaturnNodes] Error updating lora usage: {e}")
|
||||
|
||||
def find_preview_image(lora_relative_path):
|
||||
lora_full_path = folder_paths.get_full_path("loras", lora_relative_path)
|
||||
@@ -232,10 +233,10 @@ def scrape_missing_images_sync():
|
||||
try:
|
||||
with open(dest_path, "wb") as f:
|
||||
f.write(img_data)
|
||||
print(f"[LeafFlow] Scraped Civitai preview for {pretty_name}")
|
||||
print(f"[SaturnNodes] Scraped Civitai preview for {pretty_name}")
|
||||
success = True
|
||||
except (PermissionError, OSError) as write_err:
|
||||
print(f"[LeafFlow] Warning: Could not save preview image to '{dest_path}': {write_err}")
|
||||
print(f"[SaturnNodes] Warning: Could not save preview image to '{dest_path}': {write_err}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -277,11 +278,11 @@ def scrape_missing_images_sync():
|
||||
try:
|
||||
with open(dest_path, "wb") as f:
|
||||
f.write(img_data)
|
||||
print(f"[LeafFlow] Scraped TMDB preview for {pretty_name} (via '{term}')")
|
||||
print(f"[SaturnNodes] Scraped TMDB preview for {pretty_name} (via '{term}')")
|
||||
success = True
|
||||
break
|
||||
except (PermissionError, OSError) as write_err:
|
||||
print(f"[LeafFlow] Warning: Could not save preview image to '{dest_path}': {write_err}")
|
||||
print(f"[SaturnNodes] Warning: Could not save preview image to '{dest_path}': {write_err}")
|
||||
except Exception as tmdb_err:
|
||||
pass
|
||||
|
||||
@@ -296,7 +297,7 @@ def scrape_missing_images_sync():
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error in preview scraping thread: {e}")
|
||||
print(f"[SaturnNodes] Error in preview scraping thread: {e}")
|
||||
|
||||
def start_async_scraping():
|
||||
global scraping_thread
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
PREVIEW_CATEGORY = "🍃 LeafFlow/Previews"
|
||||
PREVIEW_CATEGORY = "🪐 SaturnNodes/Previews"
|
||||
|
||||
class PreviewLatentLiveNode:
|
||||
@classmethod
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .prompt_iterator import parse_prompt_blocks
|
||||
from .prompt_iterator import parse_prompt_blocks
|
||||
|
||||
class PromptCounter:
|
||||
"""
|
||||
@@ -27,7 +27,7 @@ class PromptCounter:
|
||||
RETURN_TYPES = ("STRING", "INT")
|
||||
RETURN_NAMES = ("STRING", "count")
|
||||
FUNCTION = "count_prompts"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
DESCRIPTION = "Multiline prompt text input with live prompt count display according to selectable separator or custom regex."
|
||||
|
||||
def count_prompts(self, text="", separator=">1 Empty Line", custom_regex="", **kwargs):
|
||||
|
||||
+40
-26
@@ -9,10 +9,10 @@ import subprocess
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
import folder_paths
|
||||
from .utils import get_leafflow_user_dir, is_authenticated_local_request
|
||||
from .utils import get_saturnnodes_user_dir, get_leafflow_user_dir, is_authenticated_local_request
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
USER_DIR = get_leafflow_user_dir()
|
||||
USER_DIR = get_saturnnodes_user_dir()
|
||||
STATE_FILE = os.path.join(USER_DIR, "prompt_iterator_state.json")
|
||||
ENV_FILE = os.path.join(USER_DIR, ".env")
|
||||
|
||||
@@ -43,7 +43,7 @@ def load_state():
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Warning reading prompt iterator state: {e}")
|
||||
print(f"[SaturnNodes] 🍃 Warning reading prompt iterator state: {e}")
|
||||
return {}
|
||||
|
||||
def save_state(state):
|
||||
@@ -62,17 +62,17 @@ def save_state(state):
|
||||
else:
|
||||
os.rename(tmp_file, STATE_FILE)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error saving prompt iterator state: {e}")
|
||||
print(f"[SaturnNodes] 🍃 Error saving prompt iterator state: {e}")
|
||||
|
||||
def clear_state():
|
||||
ensure_user_dir()
|
||||
try:
|
||||
with open(STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({}, f, indent=4)
|
||||
print("[LeafFlow] 🍃 Prompt iterator state cleared.")
|
||||
print("[SaturnNodes] 🍃 Prompt iterator state cleared.")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error clearing prompt iterator state: {e}")
|
||||
print(f"[SaturnNodes] 🍃 Error clearing prompt iterator state: {e}")
|
||||
return False
|
||||
|
||||
# Clear state on startup if privacy setting enabled
|
||||
@@ -83,7 +83,7 @@ if is_clear_prompt_iterator_on_launch():
|
||||
try:
|
||||
routes = PromptServer.instance.routes
|
||||
|
||||
@routes.post("/leafflow/prompt_iterator/reset_node")
|
||||
@routes.post("/saturnnodes/prompt_iterator/reset_node")
|
||||
async def reset_prompt_iterator_node(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
@@ -101,28 +101,28 @@ try:
|
||||
matched = True
|
||||
save_state(state)
|
||||
if node_id and node_id != "all":
|
||||
progress_payload = {
|
||||
"node_id": node_id,
|
||||
"current_run": 0,
|
||||
"total_runs": 0,
|
||||
"status_text": "Reset (0 / --)"
|
||||
}
|
||||
try:
|
||||
PromptServer.instance.send_sync("leafflow_prompt_iterator_progress", {
|
||||
"node_id": node_id,
|
||||
"current_run": 0,
|
||||
"total_runs": 0,
|
||||
"status_text": "Reset (0 / --)"
|
||||
})
|
||||
PromptServer.instance.send_sync("saturnnodes_prompt_iterator_progress", progress_payload)
|
||||
except Exception:
|
||||
pass
|
||||
return web.json_response({"status": "ok", "reset": matched})
|
||||
except Exception as e:
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@routes.post("/leafflow/prompt_iterator/clear")
|
||||
@routes.post("/flow_control/prompt_iterator/clear")
|
||||
@routes.post("/saturnnodes/prompt_iterator/clear")
|
||||
async def clear_prompt_iterator_endpoint(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
success = clear_state()
|
||||
return web.json_response({"status": "ok" if success else "error"})
|
||||
|
||||
@routes.post("/leafflow/prompt_iterator/open_file")
|
||||
@routes.post("/saturnnodes/prompt_iterator/open_file")
|
||||
async def open_prompt_iterator_file(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
@@ -149,7 +149,7 @@ try:
|
||||
subprocess.Popen(["xdg-open", canonical_state])
|
||||
return web.json_response({"status": "ok", "path": canonical_state})
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Error opening state file: {e}")
|
||||
print(f"[SaturnNodes] 🍃 Error opening state file: {e}")
|
||||
return web.json_response({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
except Exception:
|
||||
@@ -174,7 +174,7 @@ def parse_prompt_blocks(text_str, separator, custom_regex=""):
|
||||
results.append(b_str)
|
||||
return results
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow Prompt] Invalid custom regex pattern '{pattern}': {e}. Falling back to empty line separator.")
|
||||
print(f"[SaturnNodes Prompt] Invalid custom regex pattern '{pattern}': {e}. Falling back to empty line separator.")
|
||||
raw_blocks = re.split(r'\n\s*\n+', clean_text)
|
||||
return [b.strip() for b in raw_blocks if b.strip()]
|
||||
elif "newline" in sep_lower:
|
||||
@@ -228,7 +228,7 @@ class PromptQueueIterator:
|
||||
RETURN_TYPES = ("STRING", "STRING", "INT")
|
||||
RETURN_NAMES = ("prompt", "remaining_text", "remaining_count")
|
||||
FUNCTION = "process_queue"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
DESCRIPTION = "Deterministically iterates over multiline prompt text blocks per queue run with live progress display and index controls."
|
||||
|
||||
@classmethod
|
||||
@@ -306,6 +306,16 @@ class PromptQueueIterator:
|
||||
selected_prompt = original_blocks[idx]
|
||||
next_index = current_index + 1
|
||||
display_run = (current_index % total_count) + 1
|
||||
elif pop_mode in ["Random (Cycle)"]:
|
||||
shuffled_order = list(node_state.get("shuffled_order", [])) if isinstance(node_state, dict) else []
|
||||
cycle_index = current_index % total_count
|
||||
if len(shuffled_order) != total_count or (cycle_index == 0 and current_index > 0) or not shuffled_order:
|
||||
shuffled_order = list(range(total_count))
|
||||
random.shuffle(shuffled_order)
|
||||
idx = shuffled_order[cycle_index]
|
||||
selected_prompt = original_blocks[idx]
|
||||
next_index = (current_index + 1) % total_count
|
||||
display_run = cycle_index + 1
|
||||
else: # Default fallback
|
||||
idx = current_index % total_count
|
||||
selected_prompt = original_blocks[idx]
|
||||
@@ -335,7 +345,7 @@ class PromptQueueIterator:
|
||||
remaining_text = join_delim.join(original_blocks[display_run:]) if display_run < total_count else ""
|
||||
|
||||
# Update state dictionary
|
||||
state[state_key] = {
|
||||
new_entry = {
|
||||
"index": next_index,
|
||||
"total": total_count,
|
||||
"last_run": display_run,
|
||||
@@ -343,6 +353,9 @@ class PromptQueueIterator:
|
||||
"preview": selected_prompt[:60].replace('\n', ' '),
|
||||
"assigned_prompts": assigned_prompts
|
||||
}
|
||||
if "shuffled_order" in locals():
|
||||
new_entry["shuffled_order"] = shuffled_order
|
||||
state[state_key] = new_entry
|
||||
|
||||
# Keep at most 3 most recent text hashes per node_id
|
||||
node_prefix = f"node_{unique_id}_"
|
||||
@@ -355,13 +368,14 @@ class PromptQueueIterator:
|
||||
save_state(state)
|
||||
|
||||
# Send live progress update to UI
|
||||
progress_data = {
|
||||
"node_id": str(unique_id),
|
||||
"current_run": display_run,
|
||||
"total_runs": total_count,
|
||||
"status_text": f"Run {display_run} / {total_count}"
|
||||
}
|
||||
try:
|
||||
PromptServer.instance.send_sync("leafflow_prompt_iterator_progress", {
|
||||
"node_id": str(unique_id),
|
||||
"current_run": display_run,
|
||||
"total_runs": total_count,
|
||||
"status_text": f"Run {display_run} / {total_count}"
|
||||
})
|
||||
PromptServer.instance.send_sync("saturnnodes_prompt_iterator_progress", progress_data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
+39
-38
@@ -9,12 +9,12 @@ from server import PromptServer
|
||||
import nodes
|
||||
from .tray_icon import TrayIconManager, is_tray_icon_enabled
|
||||
from .assets_restore import assets_restore_manager, is_assets_restore_enabled, get_assets_restore_count
|
||||
from .utils import get_leafflow_user_dir, is_local_request, is_authenticated_local_request
|
||||
from .utils import get_saturnnodes_user_dir, get_leafflow_user_dir, is_local_request, is_authenticated_local_request
|
||||
|
||||
QUEUE_CATEGORY = "🍃 LeafFlow/Queue"
|
||||
QUEUE_CATEGORY = "🪐 SaturnNodes/Queue"
|
||||
|
||||
CURRENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
USER_DIR = get_leafflow_user_dir()
|
||||
USER_DIR = get_saturnnodes_user_dir()
|
||||
PERSISTENT_FILE = os.path.join(USER_DIR, "persistent_queue.json")
|
||||
ENV_FILE = os.path.join(USER_DIR, ".env")
|
||||
|
||||
@@ -39,7 +39,7 @@ def is_process_management_enabled():
|
||||
|
||||
class PauseQueueManager:
|
||||
def __init__(self):
|
||||
default_state = get_env_setting("DEFAULT_PAUSE_STATE", "Paused")
|
||||
default_state = get_env_setting("DEFAULT_PAUSE_STATE", "Running")
|
||||
default_mode = get_env_setting("DEFAULT_PAUSE_MODE", "after_finish")
|
||||
|
||||
self.paused = (default_state.lower() != "running")
|
||||
@@ -57,11 +57,11 @@ class PauseQueueManager:
|
||||
return
|
||||
server = getattr(PromptServer, "instance", None)
|
||||
if not server:
|
||||
print("[LeafFlow] Queue control: Skipped pause patching (PromptServer instance not ready).")
|
||||
print("[SaturnNodes] Queue control: Skipped pause patching (PromptServer instance not ready).")
|
||||
return
|
||||
|
||||
if not hasattr(server, "prompt_queue") or not hasattr(server.prompt_queue, "get"):
|
||||
print("[LeafFlow] Queue control: Skipped patching prompt_queue.get (incompatible ComfyUI version).")
|
||||
print("[SaturnNodes] Queue control: Skipped patching prompt_queue.get (incompatible ComfyUI version).")
|
||||
return
|
||||
|
||||
queue = server.prompt_queue
|
||||
@@ -113,10 +113,10 @@ class PauseQueueManager:
|
||||
|
||||
server.send_sync = patched_send_sync
|
||||
else:
|
||||
print("[LeafFlow] Queue control: Skipped patching send_sync (not found on server).")
|
||||
print("[SaturnNodes] Queue control: Skipped patching send_sync (not found on server).")
|
||||
|
||||
self._patched = True
|
||||
print("[LeafFlow] Queue control: Pause manager initialized successfully.")
|
||||
print("[SaturnNodes] Queue control: Pause manager initialized successfully.")
|
||||
|
||||
def is_currently_executing(self):
|
||||
try:
|
||||
@@ -281,16 +281,16 @@ class PersistentQueueManager:
|
||||
if self._patched:
|
||||
return
|
||||
if not is_persistent_queue_enabled():
|
||||
print("[LeafFlow] Persistent queue disabled in settings; core prompt_queue patching skipped.")
|
||||
print("[SaturnNodes] Persistent queue disabled in settings; core prompt_queue patching skipped.")
|
||||
return
|
||||
server = getattr(PromptServer, "instance", None)
|
||||
if not server or not hasattr(server, "prompt_queue"):
|
||||
print("[LeafFlow] Queue control: Skipped persistent queue patching (prompt_queue not found on server).")
|
||||
print("[SaturnNodes] Queue control: Skipped persistent queue patching (prompt_queue not found on server).")
|
||||
return
|
||||
|
||||
queue = server.prompt_queue
|
||||
if not hasattr(queue, "put") or not hasattr(queue, "task_done"):
|
||||
print("[LeafFlow] Queue control: Skipped persistent queue patching (incompatible queue interface).")
|
||||
print("[SaturnNodes] Queue control: Skipped persistent queue patching (incompatible queue interface).")
|
||||
return
|
||||
|
||||
# 1. Patch queue.put
|
||||
@@ -361,7 +361,7 @@ class PersistentQueueManager:
|
||||
server.send_sync = patched_send_sync
|
||||
|
||||
self._patched = True
|
||||
print("[LeafFlow] Queue control: Persistent queue manager initialized successfully.")
|
||||
print("[SaturnNodes] Queue control: Persistent queue manager initialized successfully.")
|
||||
|
||||
def restore_queue(self, active_client_id=None):
|
||||
if self.has_claimed_once:
|
||||
@@ -465,16 +465,16 @@ class PowerControlManager:
|
||||
with self.lock:
|
||||
if action in ["restart", "shutdown"]:
|
||||
if not is_process_management_enabled():
|
||||
print(f"[LeafFlow Power] Arm rejected: Process Management is disabled in LeafFlow settings.")
|
||||
print(f"[SaturnNodes Power] Arm rejected: Process Management is disabled in LeafFlow settings.")
|
||||
return False
|
||||
self.pending_action = action
|
||||
import time
|
||||
self.armed_at = time.time()
|
||||
print(f"[LeafFlow Power] Armed action '{action}' after queue completion.")
|
||||
print(f"[SaturnNodes Power] Armed action '{action}' after queue completion.")
|
||||
else:
|
||||
self.pending_action = None
|
||||
self.armed_at = None
|
||||
print("[LeafFlow Power] Cancelled armed queue power action.")
|
||||
print("[SaturnNodes Power] Cancelled armed queue power action.")
|
||||
self.notify_clients()
|
||||
return True
|
||||
|
||||
@@ -489,7 +489,8 @@ class PowerControlManager:
|
||||
|
||||
def notify_clients(self):
|
||||
try:
|
||||
PromptServer.instance.send_sync("leafflow_power_status", self.get_status())
|
||||
status = self.get_status()
|
||||
PromptServer.instance.send_sync("saturnnodes_power_status", status)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -520,7 +521,7 @@ class PowerControlManager:
|
||||
action = self.pending_action
|
||||
self._executing = True
|
||||
self.pending_action = None
|
||||
print(f"[LeafFlow Power] Queue is empty and idle. Executing '{action}' now...")
|
||||
print(f"[SaturnNodes Power] Queue is empty and idle. Executing '{action}' now...")
|
||||
|
||||
if action == "restart":
|
||||
self.execute_restart()
|
||||
@@ -529,12 +530,12 @@ class PowerControlManager:
|
||||
|
||||
def execute_restart(self):
|
||||
if not is_process_management_enabled():
|
||||
print("[LeafFlow Power] Rejected restart: Process Management is disabled in LeafFlow settings.")
|
||||
print("[SaturnNodes Power] Rejected restart: Process Management is disabled in LeafFlow settings.")
|
||||
return False
|
||||
|
||||
def _run():
|
||||
import subprocess, sys, os, time
|
||||
print("[LeafFlow Power] Restarting ComfyUI server process...")
|
||||
print("[SaturnNodes Power] Restarting ComfyUI server process...")
|
||||
time.sleep(0.6)
|
||||
cmd = [sys.executable]
|
||||
if getattr(sys.flags, "no_user_site", 0):
|
||||
@@ -554,12 +555,12 @@ class PowerControlManager:
|
||||
|
||||
def execute_shutdown(self):
|
||||
if not is_process_management_enabled():
|
||||
print("[LeafFlow Power] Rejected shutdown: Process Management is disabled in LeafFlow settings.")
|
||||
print("[SaturnNodes Power] Rejected shutdown: Process Management is disabled in LeafFlow settings.")
|
||||
return False
|
||||
|
||||
def _run():
|
||||
import os, time
|
||||
print("[LeafFlow Power] Shutting down ComfyUI server...")
|
||||
print("[SaturnNodes Power] Shutting down ComfyUI server...")
|
||||
time.sleep(0.6)
|
||||
os._exit(0)
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
@@ -612,19 +613,19 @@ def setup_queue_control_routes(server):
|
||||
|
||||
routes = server.routes
|
||||
|
||||
@routes.get("/leafflow/power/status")
|
||||
@routes.get("/saturnnodes/power/status")
|
||||
async def get_power_status(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
|
||||
return web.json_response(power_manager.get_status())
|
||||
|
||||
@routes.post("/leafflow/power/arm")
|
||||
@routes.post("/saturnnodes/power/arm")
|
||||
async def arm_power_action(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
if not is_process_management_enabled():
|
||||
return web.json_response({
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in LeafFlow settings to use restart or shutdown.",
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in SaturnNodes settings to use restart or shutdown.",
|
||||
"enabled": False
|
||||
}, status=403)
|
||||
try:
|
||||
@@ -635,13 +636,13 @@ def setup_queue_control_routes(server):
|
||||
power_manager.arm(action)
|
||||
return web.json_response(power_manager.get_status())
|
||||
|
||||
@routes.post("/leafflow/power/request_token")
|
||||
@routes.post("/saturnnodes/power/request_token")
|
||||
async def request_power_token(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
if not is_process_management_enabled():
|
||||
return web.json_response({
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in LeafFlow settings.",
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in SaturnNodes settings.",
|
||||
"enabled": False
|
||||
}, status=403)
|
||||
try:
|
||||
@@ -659,13 +660,13 @@ def setup_queue_control_routes(server):
|
||||
"expires_in": 30
|
||||
})
|
||||
|
||||
@routes.post("/leafflow/power/confirm_action")
|
||||
@routes.post("/saturnnodes/power/confirm_action")
|
||||
async def confirm_power_action(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
if not is_process_management_enabled():
|
||||
return web.json_response({
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in LeafFlow settings.",
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in SaturnNodes settings.",
|
||||
"enabled": False
|
||||
}, status=403)
|
||||
try:
|
||||
@@ -684,13 +685,13 @@ def setup_queue_control_routes(server):
|
||||
return web.json_response({"status": "shutting_down", "success": True})
|
||||
return web.json_response({"error": "Unknown action"}, status=400)
|
||||
|
||||
@routes.post("/leafflow/power/restart")
|
||||
@routes.post("/saturnnodes/power/restart")
|
||||
async def trigger_restart(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
if not is_process_management_enabled():
|
||||
return web.json_response({
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in LeafFlow settings to use restart.",
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in SaturnNodes settings to use restart.",
|
||||
"enabled": False
|
||||
}, status=403)
|
||||
try:
|
||||
@@ -699,17 +700,17 @@ def setup_queue_control_routes(server):
|
||||
data = {}
|
||||
ticket = data.get("ticket")
|
||||
if not _consume_power_ticket(ticket, "restart"):
|
||||
return web.json_response({"error": "Confirmation ticket required. Use /leafflow/power/request_token first."}, status=403)
|
||||
return web.json_response({"error": "Confirmation ticket required. Use /saturnnodes/power/request_token first."}, status=403)
|
||||
power_manager.execute_restart()
|
||||
return web.json_response({"status": "restarting"})
|
||||
|
||||
@routes.post("/leafflow/power/shutdown")
|
||||
@routes.post("/saturnnodes/power/shutdown")
|
||||
async def trigger_shutdown(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
if not is_process_management_enabled():
|
||||
return web.json_response({
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in LeafFlow settings to use shutdown.",
|
||||
"error": "Process Management is disabled. Enable 'Allow Process Management' in SaturnNodes settings to use shutdown.",
|
||||
"enabled": False
|
||||
}, status=403)
|
||||
try:
|
||||
@@ -718,11 +719,11 @@ def setup_queue_control_routes(server):
|
||||
data = {}
|
||||
ticket = data.get("ticket")
|
||||
if not _consume_power_ticket(ticket, "shutdown"):
|
||||
return web.json_response({"error": "Confirmation ticket required. Use /leafflow/power/request_token first."}, status=403)
|
||||
return web.json_response({"error": "Confirmation ticket required. Use /saturnnodes/power/request_token first."}, status=403)
|
||||
power_manager.execute_shutdown()
|
||||
return web.json_response({"status": "shutting_down"})
|
||||
|
||||
@routes.post("/leafflow/assets/restore")
|
||||
@routes.post("/saturnnodes/assets/restore")
|
||||
async def restore_assets_endpoint(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
@@ -735,19 +736,19 @@ def setup_queue_control_routes(server):
|
||||
count = assets_restore_manager.restore_on_launch(server, limit=limit, force=force)
|
||||
return web.json_response({"success": True, "restored": count, "debug": assets_restore_manager.last_debug_report})
|
||||
|
||||
@routes.get("/leafflow/assets/debug")
|
||||
@routes.get("/saturnnodes/assets/debug")
|
||||
async def get_assets_debug(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
|
||||
return web.json_response(assets_restore_manager.last_debug_report)
|
||||
|
||||
@routes.get("/leafflow/batch_queue/data")
|
||||
@routes.get("/saturnnodes/batch_queue/data")
|
||||
async def get_batch_queue_data(request):
|
||||
if not is_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local access only"}, status=403)
|
||||
return web.json_response(persistent_manager.batch_meta)
|
||||
|
||||
@routes.post("/leafflow/batch_queue/sync")
|
||||
@routes.post("/saturnnodes/batch_queue/sync")
|
||||
async def sync_batch_queue_data(request):
|
||||
if not is_authenticated_local_request(request):
|
||||
return web.json_response({"error": "Forbidden: Local authenticated access only"}, status=403)
|
||||
|
||||
+26
-13
@@ -1,18 +1,31 @@
|
||||
import re
|
||||
import csv
|
||||
import io
|
||||
|
||||
def parse_find_targets(find_str):
|
||||
if not find_str:
|
||||
if not find_str or not find_str.strip():
|
||||
return []
|
||||
raw_items = []
|
||||
# Split strictly by comma
|
||||
parts = find_str.split(',')
|
||||
for part in parts:
|
||||
item = part.strip()
|
||||
if (item.startswith('"') and item.endswith('"')) or (item.startswith("'") and item.endswith("'")):
|
||||
item = item[1:-1].strip()
|
||||
if item:
|
||||
raw_items.append(item)
|
||||
return raw_items
|
||||
try:
|
||||
reader = csv.reader(io.StringIO(find_str.strip()), skipinitialspace=True)
|
||||
raw_items = []
|
||||
for row in reader:
|
||||
for item in row:
|
||||
s = item.strip()
|
||||
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
||||
s = s[1:-1].strip()
|
||||
if s:
|
||||
raw_items.append(s)
|
||||
return raw_items
|
||||
except Exception:
|
||||
raw_items = []
|
||||
parts = find_str.split(',')
|
||||
for part in parts:
|
||||
item = part.strip()
|
||||
if (item.startswith('"') and item.endswith('"')) or (item.startswith("'") and item.endswith("'")):
|
||||
item = item[1:-1].strip()
|
||||
if item:
|
||||
raw_items.append(item)
|
||||
return raw_items
|
||||
|
||||
class MultiTextReplacer:
|
||||
@classmethod
|
||||
@@ -32,7 +45,7 @@ class MultiTextReplacer:
|
||||
RETURN_TYPES = ("STRING", "INT")
|
||||
RETURN_NAMES = ("text", "replaced_count")
|
||||
FUNCTION = "replace_text"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
DESCRIPTION = "Searches input text for multiple search targets specified in a comma-separated list or regex, and replaces all matches with a replacement string."
|
||||
|
||||
def replace_text(
|
||||
@@ -75,6 +88,6 @@ class MultiTextReplacer:
|
||||
replaced_count += count
|
||||
result_text = re.sub(pattern_str, lambda m: replace, result_text, flags=flags)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] 🍃 Invalid regex pattern '{pattern_str}': {e}")
|
||||
print(f"[SaturnNodes] 🍃 Invalid regex pattern '{pattern_str}': {e}")
|
||||
|
||||
return (result_text, replaced_count)
|
||||
|
||||
+7
-4
@@ -1,8 +1,8 @@
|
||||
import re
|
||||
|
||||
class LeafFlowTextSplit:
|
||||
class SaturnTextSplit:
|
||||
"""
|
||||
LeafFlow Text Node:
|
||||
SaturnNodes Text Split Node:
|
||||
Splits input text into two parts (text1 and text2) by either a literal character sequence
|
||||
or a regular expression pattern, supporting forward (from start) and backward (from end) splitting.
|
||||
"""
|
||||
@@ -23,7 +23,7 @@ class LeafFlowTextSplit:
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("text1", "text2")
|
||||
FUNCTION = "split_text"
|
||||
CATEGORY = "🍃 LeafFlow/Utils"
|
||||
CATEGORY = "🪐 SaturnNodes/Utils"
|
||||
DESCRIPTION = "Splits input text into two parts (text1 and text2) by a character sequence or regex pattern, with forward/backward splitting."
|
||||
|
||||
def split_text(
|
||||
@@ -64,7 +64,7 @@ class LeafFlowTextSplit:
|
||||
else:
|
||||
t1, t2 = input_text, ""
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow Text Split] Regex error: {e}. Falling back to literal string split.")
|
||||
print(f"[SaturnNodes Text Split] Regex error: {e}. Falling back to literal string split.")
|
||||
if is_forward:
|
||||
parts = input_text.split(delimiter, 1)
|
||||
else:
|
||||
@@ -85,3 +85,6 @@ class LeafFlowTextSplit:
|
||||
t2 = t2.strip()
|
||||
|
||||
return (t1, t2)
|
||||
|
||||
LeafFlowTextSplit = SaturnTextSplit
|
||||
SaturnTextSplit = SaturnTextSplit
|
||||
|
||||
+13
-13
@@ -3,9 +3,9 @@ import sys
|
||||
import threading
|
||||
import webbrowser
|
||||
from PIL import Image, ImageDraw
|
||||
from .utils import get_leafflow_user_dir
|
||||
from .utils import get_saturnnodes_user_dir, get_leafflow_user_dir
|
||||
|
||||
USER_DIR = get_leafflow_user_dir()
|
||||
USER_DIR = get_saturnnodes_user_dir()
|
||||
ENV_FILE = os.path.join(USER_DIR, ".env")
|
||||
|
||||
def get_env_setting(key, default_val):
|
||||
@@ -68,7 +68,7 @@ class TrayIconManager:
|
||||
|
||||
def get_status_info(self):
|
||||
if not self.pause_manager:
|
||||
return "Running", "#059669", "pause", "ComfyUI LeafFlow: Running"
|
||||
return "Running", "#059669", "pause", "ComfyUI SaturnNodes: Running"
|
||||
|
||||
unpaused_color, paused_color = self.get_current_colors()
|
||||
is_paused = self.pause_manager.paused
|
||||
@@ -81,17 +81,17 @@ class TrayIconManager:
|
||||
status_text = "Paused (Waiting)"
|
||||
color = paused_color
|
||||
symbol = "play"
|
||||
title = f"ComfyUI LeafFlow: Paused ({mode_label}) - Click to Continue"
|
||||
title = f"ComfyUI SaturnNodes: Paused ({mode_label}) - Click to Continue"
|
||||
else:
|
||||
status_text = f"Pausing ({mode_label})..."
|
||||
color = paused_color
|
||||
symbol = "pause"
|
||||
title = f"ComfyUI LeafFlow: Pausing ({mode_label})... (Waiting to complete)"
|
||||
title = f"ComfyUI SaturnNodes: Pausing ({mode_label})... (Waiting to complete)"
|
||||
else:
|
||||
status_text = f"Running [Next Pause: {mode_label}]"
|
||||
color = unpaused_color
|
||||
symbol = "pause"
|
||||
title = f"ComfyUI LeafFlow: Running (Mode: {mode_label})"
|
||||
title = f"ComfyUI SaturnNodes: Running (Mode: {mode_label})"
|
||||
|
||||
return status_text, color, symbol, title
|
||||
|
||||
@@ -146,7 +146,7 @@ class TrayIconManager:
|
||||
server_url = f"http://{host}:{port}"
|
||||
webbrowser.open(server_url)
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error opening browser: {e}")
|
||||
print(f"[SaturnNodes] Error opening browser: {e}")
|
||||
|
||||
def on_disable_tray(icon, item):
|
||||
self.disable_and_stop()
|
||||
@@ -178,7 +178,7 @@ class TrayIconManager:
|
||||
try:
|
||||
import pystray
|
||||
except ImportError:
|
||||
print("[LeafFlow] System tray icon disabled: 'pystray' is not installed. Install pystray ('pip install pystray') to enable system tray support.")
|
||||
print("[SaturnNodes] System tray icon disabled: 'pystray' is not installed. Install pystray ('pip install pystray') to enable system tray support.")
|
||||
return
|
||||
|
||||
_, color, symbol, title = self.get_status_info()
|
||||
@@ -186,7 +186,7 @@ class TrayIconManager:
|
||||
menu = self.build_menu()
|
||||
|
||||
self._icon = pystray.Icon(
|
||||
name="ComfyUI-LeafFlow",
|
||||
name="ComfyUI-SaturnNodes",
|
||||
icon=icon_img,
|
||||
title=title,
|
||||
menu=menu
|
||||
@@ -195,10 +195,10 @@ class TrayIconManager:
|
||||
def _run():
|
||||
try:
|
||||
self._is_running = True
|
||||
print("[LeafFlow] System tray icon started.")
|
||||
print("[SaturnNodes] System tray icon started.")
|
||||
self._icon.run()
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] System tray icon error: {e}")
|
||||
print(f"[SaturnNodes] System tray icon error: {e}")
|
||||
finally:
|
||||
self._is_running = False
|
||||
self._icon = None
|
||||
@@ -215,7 +215,7 @@ class TrayIconManager:
|
||||
pass
|
||||
self._icon = None
|
||||
self._is_running = False
|
||||
print("[LeafFlow] System tray icon stopped.")
|
||||
print("[SaturnNodes] System tray icon stopped.")
|
||||
|
||||
def update_status(self):
|
||||
with self._lock:
|
||||
@@ -228,7 +228,7 @@ class TrayIconManager:
|
||||
self._icon.title = title
|
||||
self._icon.menu = self.build_menu()
|
||||
except Exception as e:
|
||||
print(f"[LeafFlow] Error updating system tray status: {e}")
|
||||
print(f"[SaturnNodes] Error updating system tray status: {e}")
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if enabled:
|
||||
|
||||
+24
-10
@@ -4,17 +4,16 @@ import secrets
|
||||
from urllib.parse import urlparse
|
||||
import folder_paths
|
||||
|
||||
def get_leafflow_user_dir():
|
||||
def get_saturnnodes_user_dir():
|
||||
"""
|
||||
Returns the centralized user data directory: ComfyUI/user/default/LeafFlow/
|
||||
Returns the centralized user data directory: ComfyUI/user/default/SaturnNodes/
|
||||
Automatically migrates any legacy LeafFlow directory contents if present.
|
||||
"""
|
||||
base_user = None
|
||||
try:
|
||||
if hasattr(folder_paths, "get_user_directory"):
|
||||
user_base = folder_paths.get_user_directory()
|
||||
if user_base:
|
||||
# ComfyUI get_user_directory() returns '.../ComfyUI/user'
|
||||
# Ensure the 'default' user profile directory is used
|
||||
norm_base = os.path.normpath(user_base)
|
||||
if os.path.basename(norm_base).lower() == "user":
|
||||
base_user = os.path.join(norm_base, "default")
|
||||
@@ -24,13 +23,28 @@ def get_leafflow_user_dir():
|
||||
base_user = None
|
||||
|
||||
if not base_user:
|
||||
# Fallback to ComfyUI/user/default
|
||||
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
base_user = os.path.join(root_dir, "user", "default")
|
||||
|
||||
leafflow_dir = os.path.join(base_user, "LeafFlow")
|
||||
os.makedirs(leafflow_dir, exist_ok=True)
|
||||
return leafflow_dir
|
||||
saturn_dir = os.path.join(base_user, "SaturnNodes")
|
||||
os.makedirs(saturn_dir, exist_ok=True)
|
||||
|
||||
# Seamless automatic migration from legacy LeafFlow folder if needed
|
||||
legacy_dir = os.path.join(base_user, "LeafFlow")
|
||||
if os.path.isdir(legacy_dir):
|
||||
for f in [".env", "prompt_iterator_state.json"]:
|
||||
src = os.path.join(legacy_dir, f)
|
||||
dst = os.path.join(saturn_dir, f)
|
||||
if os.path.isfile(src) and not os.path.exists(dst):
|
||||
try:
|
||||
import shutil
|
||||
shutil.copy2(src, dst)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return saturn_dir
|
||||
|
||||
get_leafflow_user_dir = get_saturnnodes_user_dir
|
||||
|
||||
_SESSION_CSRF_TOKEN = secrets.token_hex(32)
|
||||
|
||||
@@ -87,7 +101,7 @@ def is_authenticated_local_request(request):
|
||||
return False
|
||||
|
||||
headers = getattr(request, "headers", {})
|
||||
token = headers.get("X-LeafFlow-CSRF-Token")
|
||||
token = headers.get("X-SaturnNodes-CSRF-Token") or headers.get("X-LeafFlow-CSRF-Token")
|
||||
if not token or not secrets.compare_digest(token, _SESSION_CSRF_TOKEN):
|
||||
return False
|
||||
|
||||
@@ -203,7 +217,7 @@ def get_env_setting(key, default_val):
|
||||
"""
|
||||
Reads a key setting from ComfyUI/user/default/LeafFlow/.env safely.
|
||||
"""
|
||||
user_dir = get_leafflow_user_dir()
|
||||
user_dir = get_saturnnodes_user_dir()
|
||||
env_file = os.path.join(user_dir, ".env")
|
||||
if os.path.exists(env_file):
|
||||
try:
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-leafflow"
|
||||
name = "comfyui-saturnnodes"
|
||||
description = "Essential workflow controls, mid-generation pausing, real-time queue persistence, folder watcher, and visual model loaders for ComfyUI."
|
||||
version = "2.3.1"
|
||||
version = "2.3.2"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
dependencies = ["pillow", "numpy", "piexif"]
|
||||
@@ -10,9 +10,9 @@ dependencies = ["pillow", "numpy", "piexif"]
|
||||
tray = ["pystray"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/KOFiblto/ComfyUI-LeafFlow"
|
||||
Repository = "https://github.com/KOFiblto/ComfyUI-SaturnNodes"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "KOFiblto"
|
||||
DisplayName = "ComfyUI-LeafFlow"
|
||||
Icon = "https://raw.githubusercontent.com/KOFiblto/ComfyUI-LeafFlow/main/web/logo.png"
|
||||
DisplayName = "ComfyUI-SaturnNodes"
|
||||
Icon = "https://raw.githubusercontent.com/KOFiblto/ComfyUI-SaturnNodes/main/web/logo.png"
|
||||
|
||||
@@ -6,11 +6,11 @@ import threading
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import test_helper
|
||||
|
||||
from nodes.decision_node import DecisionManager, LeafFlowDecision
|
||||
from nodes.decision_node import DecisionManager, SaturnDecision
|
||||
|
||||
class TestDecisionNode(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.node = LeafFlowDecision()
|
||||
self.node = SaturnDecision()
|
||||
|
||||
def test_decision_manager_register_and_trigger(self):
|
||||
uid = "test_decision_123"
|
||||
|
||||
@@ -235,5 +235,36 @@ class TestPromptQueueIterator(unittest.TestCase):
|
||||
matching_keys = [k for k in state_db.keys() if k.startswith("node_prune_node_")]
|
||||
self.assertLessEqual(len(matching_keys), 3)
|
||||
|
||||
def test_random_cycle_permutation(self):
|
||||
"""
|
||||
Verify that Random (Cycle) mode visits all items in a permutation before repeating.
|
||||
"""
|
||||
pack = "A\n\nB\n\nC\n\nD\n\nE"
|
||||
state_db = {}
|
||||
|
||||
def mock_load():
|
||||
return dict(state_db)
|
||||
|
||||
def mock_save(new_state):
|
||||
nonlocal state_db
|
||||
state_db = dict(new_state)
|
||||
|
||||
with patch("nodes.prompt_iterator.load_state", side_effect=mock_load), \
|
||||
patch("nodes.prompt_iterator.save_state", side_effect=mock_save), \
|
||||
patch("nodes.prompt_iterator.PromptServer.instance.send_sync"):
|
||||
|
||||
picked = []
|
||||
for _ in range(5):
|
||||
p, _, _ = self.node.process_queue(
|
||||
pop_mode="Random (Cycle)",
|
||||
text=pack,
|
||||
unique_id="cycle_test_node"
|
||||
)
|
||||
picked.append(p)
|
||||
|
||||
# All 5 unique items must have been visited in 1 cycle
|
||||
self.assertEqual(len(picked), 5)
|
||||
self.assertEqual(set(picked), {"A", "B", "C", "D", "E"})
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -19,6 +19,10 @@ class TestMultiTextReplacer(unittest.TestCase):
|
||||
targets = parse_find_targets('"dog", "cat", "bird"')
|
||||
self.assertEqual(targets, ["dog", "cat", "bird"])
|
||||
|
||||
def test_parse_find_targets_with_comma_inside_quotes(self):
|
||||
targets = parse_find_targets('"hello, world", "foo, bar", baz')
|
||||
self.assertEqual(targets, ["hello, world", "foo, bar", "baz"])
|
||||
|
||||
def test_replace_comma_separated_list(self):
|
||||
text, count = self.node.replace_text(
|
||||
find="dog, cat, bird",
|
||||
|
||||
@@ -5,11 +5,11 @@ import unittest
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import test_helper
|
||||
|
||||
from nodes.text_split import LeafFlowTextSplit
|
||||
from nodes.text_split import SaturnTextSplit
|
||||
|
||||
class TestLeafFlowTextSplit(unittest.TestCase):
|
||||
class TestSaturnTextSplit(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.node = LeafFlowTextSplit()
|
||||
self.node = SaturnTextSplit()
|
||||
|
||||
def test_forward_literal_split_basic(self):
|
||||
# User standard case: "Hello --- Bye" with delimiter "--"
|
||||
|
||||
+41
-13
@@ -42,10 +42,18 @@ const BATCH_COLORS = [
|
||||
"#c4b5fd" // 36. Lilac
|
||||
];
|
||||
|
||||
const STORAGE_KEY = "leafflow_batch_queue_meta";
|
||||
const BATCH_COUNTER_KEY = "leafflow_batch_counter";
|
||||
const SETTING_ID = "LeafFlow.BatchQueue.Enabled";
|
||||
const SETTING_SNAPSHOT_GUARD = "LeafFlow.BatchQueue.SnapshotGuard";
|
||||
const STORAGE_KEY = "saturnnodes_batch_queue_meta";
|
||||
const LEGACY_STORAGE_KEY = "leafflow_batch_queue_meta";
|
||||
const BATCH_COUNTER_KEY = "saturnnodes_batch_counter";
|
||||
const LEGACY_BATCH_COUNTER_KEY = "leafflow_batch_counter";
|
||||
const SETTING_ID = "SaturnNodes.🎨 Batch Queue.01_ShowBatchLines";
|
||||
const LEGACY_SETTING_ID_0 = "SaturnNodes.8 - 🎨 Batch Queue.01_ShowBatchLines";
|
||||
const LEGACY_SETTING_ID_1 = "SaturnNodes.BatchQueue.Enabled";
|
||||
const LEGACY_SETTING_ID_2 = "LeafFlow.BatchQueue.Enabled";
|
||||
const SETTING_SNAPSHOT_GUARD = "SaturnNodes.🎨 Batch Queue.02_SnapshotGuard";
|
||||
const LEGACY_SETTING_SNAPSHOT_GUARD_0 = "SaturnNodes.8 - 🎨 Batch Queue.02_SnapshotGuard";
|
||||
const LEGACY_SETTING_SNAPSHOT_GUARD_1 = "SaturnNodes.BatchQueue.SnapshotGuard";
|
||||
const LEGACY_SETTING_SNAPSHOT_GUARD_2 = "LeafFlow.BatchQueue.SnapshotGuard";
|
||||
|
||||
// In-memory registry mapping prompt_id -> batch metadata
|
||||
let batchRegistry = new Map();
|
||||
@@ -54,7 +62,7 @@ let batchCounter = 0;
|
||||
|
||||
function loadStorage() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const raw = localStorage.getItem(STORAGE_KEY) || localStorage.getItem(LEGACY_STORAGE_KEY);
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
@@ -65,12 +73,12 @@ function loadStorage() {
|
||||
}
|
||||
}
|
||||
}
|
||||
const savedCounter = localStorage.getItem(BATCH_COUNTER_KEY);
|
||||
const savedCounter = localStorage.getItem(BATCH_COUNTER_KEY) || localStorage.getItem(LEGACY_BATCH_COUNTER_KEY);
|
||||
if (savedCounter) {
|
||||
batchCounter = parseInt(savedCounter, 10) || 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[LeafFlow BatchQueue] Failed to load batch storage:", e);
|
||||
console.warn("[SaturnNodes BatchQueue] Failed to load batch storage:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +206,21 @@ function removeBatchLineFromElement(element) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if LeafFlow Batch Queue is enabled in settings
|
||||
// Check if SaturnNodes Batch Queue is enabled in settings
|
||||
function isBatchQueueEnabled() {
|
||||
try {
|
||||
const keys = [SETTING_ID, LEGACY_SETTING_ID_0, LEGACY_SETTING_ID_1, LEGACY_SETTING_ID_2];
|
||||
if (app.extensionManager?.setting?.get) {
|
||||
return app.extensionManager.setting.get(SETTING_ID) !== false;
|
||||
for (const k of keys) {
|
||||
const val = app.extensionManager.setting.get(k);
|
||||
if (val !== undefined) return val !== false;
|
||||
}
|
||||
}
|
||||
if (app.ui?.settings?.get) {
|
||||
for (const k of keys) {
|
||||
const val = app.ui.settings.get(k);
|
||||
if (val !== undefined) return val !== false;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return true;
|
||||
@@ -210,8 +228,18 @@ function isBatchQueueEnabled() {
|
||||
|
||||
function isSnapshotGuardEnabled() {
|
||||
try {
|
||||
const keys = [SETTING_SNAPSHOT_GUARD, LEGACY_SETTING_SNAPSHOT_GUARD_0, LEGACY_SETTING_SNAPSHOT_GUARD_1, LEGACY_SETTING_SNAPSHOT_GUARD_2];
|
||||
if (app.extensionManager?.setting?.get) {
|
||||
return app.extensionManager.setting.get(SETTING_SNAPSHOT_GUARD) !== false;
|
||||
for (const k of keys) {
|
||||
const val = app.extensionManager.setting.get(k);
|
||||
if (val !== undefined) return val !== false;
|
||||
}
|
||||
}
|
||||
if (app.ui?.settings?.get) {
|
||||
for (const k of keys) {
|
||||
const val = app.ui.settings.get(k);
|
||||
if (val !== undefined) return val !== false;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return true;
|
||||
@@ -280,7 +308,7 @@ function mergeDynamicSeeds(snapPrompt, livePrompt) {
|
||||
// Sync with server if PersistentQueue is available
|
||||
async function syncBatchToServer(promptId, batchInfo) {
|
||||
try {
|
||||
await authenticatedFetch("/leafflow/batch_queue/sync", {
|
||||
await authenticatedFetch("/saturnnodes/batch_queue/sync", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -296,7 +324,7 @@ async function syncBatchToServer(promptId, batchInfo) {
|
||||
async function loadBatchFromServer() {
|
||||
try {
|
||||
if (!api || !api.fetchApi) return;
|
||||
const res = await api.fetchApi("/leafflow/batch_queue/data", { cache: "no-store" }).catch(() => null);
|
||||
const res = await api.fetchApi("/saturnnodes/batch_queue/data", { cache: "no-store" }).catch(() => null);
|
||||
if (res && res.ok) {
|
||||
const data = await res.json();
|
||||
if (data && typeof data === "object") {
|
||||
@@ -477,7 +505,7 @@ function setupQueueHooks() {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.BatchQueue",
|
||||
name: "ComfyUI.SaturnNodes.BatchQueue",
|
||||
async setup() {
|
||||
injectBatchStyles();
|
||||
setupQueueHooks();
|
||||
|
||||
@@ -12,14 +12,14 @@ export async function getCsrfToken() {
|
||||
}
|
||||
_tokenPromise = (async () => {
|
||||
try {
|
||||
const resp = await api.fetchApi("/leafflow/auth/token");
|
||||
let resp = await api.fetchApi("/saturnnodes/auth/token");
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_cachedCsrfToken = data.csrf_token || "";
|
||||
return _cachedCsrfToken;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[LeafFlow Auth] Could not fetch CSRF token:", e);
|
||||
console.warn("[SaturnNodes Auth] Could not fetch CSRF token:", e);
|
||||
} finally {
|
||||
_tokenPromise = null;
|
||||
}
|
||||
@@ -33,15 +33,18 @@ export async function authenticatedFetch(endpoint, options = {}) {
|
||||
const opts = { ...options };
|
||||
opts.headers = { ...(opts.headers || {}) };
|
||||
if (token) {
|
||||
opts.headers["X-LeafFlow-CSRF-Token"] = token;
|
||||
opts.headers["X-SaturnNodes-CSRF-Token"] = token;
|
||||
}
|
||||
return api.fetchApi(endpoint, opts);
|
||||
}
|
||||
|
||||
// Expose on window for all LeafFlow extensions
|
||||
// Expose on window for all SaturnNodes extensions (with LeafFlow alias for backward compatibility)
|
||||
if (typeof window !== "undefined") {
|
||||
window._LeafFlowAuth = {
|
||||
const authObj = {
|
||||
getToken: getCsrfToken,
|
||||
fetch: authenticatedFetch
|
||||
};
|
||||
window._SaturnNodesAuth = authObj;
|
||||
window._LeafFlowAuth = authObj;
|
||||
}
|
||||
|
||||
|
||||
+28
-114
@@ -36,11 +36,27 @@ async function copyToClipboard(text) {
|
||||
function isCopyEnabled(settingKey, defaultVal = true) {
|
||||
try {
|
||||
let val;
|
||||
const saturnNumberedKey = settingKey.replace(/^LeafFlow\./, "SaturnNodes.");
|
||||
const saturnPlainKey = saturnNumberedKey.replace(/\d+\s*-\s*/, "");
|
||||
const keysToCheck = [saturnPlainKey, saturnNumberedKey, settingKey];
|
||||
|
||||
if (app.extensionManager?.setting?.get) {
|
||||
val = app.extensionManager.setting.get(settingKey);
|
||||
for (const k of keysToCheck) {
|
||||
val = app.extensionManager.setting.get(k);
|
||||
if (val !== undefined && val !== null && val !== "") break;
|
||||
}
|
||||
}
|
||||
if ((val === undefined || val === null || val === "") && app.ui?.settings?.getSettingValue) {
|
||||
val = app.ui.settings.getSettingValue(settingKey);
|
||||
for (const k of keysToCheck) {
|
||||
val = app.ui.settings.getSettingValue(k);
|
||||
if (val !== undefined && val !== null && val !== "") break;
|
||||
}
|
||||
}
|
||||
if ((val === undefined || val === null || val === "") && app.ui?.settings?.get) {
|
||||
for (const k of keysToCheck) {
|
||||
val = app.ui.settings.get(k);
|
||||
if (val !== undefined && val !== null && val !== "") break;
|
||||
}
|
||||
}
|
||||
if (val === undefined || val === null || val === "") {
|
||||
return defaultVal;
|
||||
@@ -51,9 +67,6 @@ function isCopyEnabled(settingKey, defaultVal = true) {
|
||||
}
|
||||
}
|
||||
|
||||
const BOOKMARK_SVG = `<svg class="size-4 pointer-events-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg>`;
|
||||
const BOOKMARK_SUCCESS_SVG = `<svg class="size-4 text-emerald-600 pointer-events-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
||||
const BOOKMARK_FAIL_SVG = `<svg class="size-4 text-rose-600 pointer-events-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
||||
const INSPECT_SVG = `<svg class="size-4 pointer-events-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>`;
|
||||
|
||||
function updateButtonGroupBorders(container) {
|
||||
@@ -248,8 +261,8 @@ async function getImagePromptAndMeta(imgSrc) {
|
||||
|
||||
if (!promptText && filename) {
|
||||
try {
|
||||
const promptUrl = `/leafflow/get_image_prompt?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}&subfolder=${encodeURIComponent(subfolder)}`;
|
||||
const response = await api.fetchApi(promptUrl);
|
||||
const query = `filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}&subfolder=${encodeURIComponent(subfolder)}`;
|
||||
const response = await api.fetchApi(`/saturnnodes/get_image_prompt?${query}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.prompt) promptText = data.prompt;
|
||||
@@ -259,32 +272,12 @@ async function getImagePromptAndMeta(imgSrc) {
|
||||
|
||||
return {
|
||||
promptText: promptText || "",
|
||||
filename: filename || "bookmark",
|
||||
filename: filename || "image",
|
||||
subfolder: subfolder || "",
|
||||
type: type || "output"
|
||||
};
|
||||
}
|
||||
|
||||
async function saveImageToPromptBookmarks(imgSrc) {
|
||||
const meta = await getImagePromptAndMeta(imgSrc);
|
||||
if (!meta) return false;
|
||||
|
||||
const event = new CustomEvent("prompt-bookmarks-create", {
|
||||
detail: {
|
||||
name: meta.filename.replace(/\.[^/.]+$/, ""),
|
||||
text: meta.promptText,
|
||||
media: [{
|
||||
filename: meta.filename,
|
||||
subfolder: meta.subfolder,
|
||||
type: meta.type,
|
||||
media_type: "image"
|
||||
}]
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function copyImagePrompt(imgSrc) {
|
||||
if (!imgSrc) return false;
|
||||
let promptText = null;
|
||||
@@ -308,8 +301,8 @@ async function copyImagePrompt(imgSrc) {
|
||||
}
|
||||
|
||||
if (filename) {
|
||||
const promptUrl = `/leafflow/get_image_prompt?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}&subfolder=${encodeURIComponent(subfolder)}`;
|
||||
const response = await api.fetchApi(promptUrl);
|
||||
const query = `filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type)}&subfolder=${encodeURIComponent(subfolder)}`;
|
||||
const response = await api.fetchApi(`/saturnnodes/get_image_prompt?${query}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data && data.prompt) {
|
||||
@@ -318,7 +311,7 @@ async function copyImagePrompt(imgSrc) {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[LeafFlow] Direct prompt fetch failed:", e);
|
||||
console.warn("[SaturnNodes] Direct prompt fetch failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +332,7 @@ async function copyImagePrompt(imgSrc) {
|
||||
|
||||
// 1. Hook into Node Context Menu (Right Click)
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.CopyPrompt",
|
||||
name: "ComfyUI.SaturnNodes.CopyPrompt",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
@@ -371,18 +364,6 @@ app.registerExtension({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (isCopyEnabled("LeafFlow.3 - 📋 Prompt Actions.03_EnableSaveToPromptSaver")) {
|
||||
options.push({
|
||||
content: "🔖 Bookmark in Prompt Saver",
|
||||
callback: async () => {
|
||||
const img = imgs[this.imageIndex || 0];
|
||||
const src = typeof img === "string" ? img : (img?.src || img?.value);
|
||||
if (!src) return;
|
||||
await saveImageToPromptBookmarks(src);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -437,7 +418,7 @@ function getActiveImageSrc() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. Inject Copy Prompt, Bookmark & Inspect Directly Next to Download Button on Asset Cards
|
||||
// 3. Inject Copy Prompt & Inspect Directly Next to Download Button on Asset Cards
|
||||
function injectCopyPromptNextToDownload(downloadBtn) {
|
||||
if (!downloadBtn || !downloadBtn.parentElement) return;
|
||||
|
||||
@@ -455,10 +436,9 @@ function injectCopyPromptNextToDownload(downloadBtn) {
|
||||
|
||||
const parent = downloadBtn.parentElement;
|
||||
const enableCopy = isCopyEnabled("LeafFlow.3 - 📋 Prompt Actions.01_EnableAssetsCopyPromptButton");
|
||||
const enableBookmark = isCopyEnabled("LeafFlow.3 - 📋 Prompt Actions.03_EnableSaveToPromptSaver");
|
||||
const enableInspect = isCopyEnabled("LeafFlow.3 - 📋 Prompt Actions.04_EnableInspectAssetButton", false);
|
||||
|
||||
if (!enableCopy && !enableBookmark && !enableInspect) return;
|
||||
if (!enableCopy && !enableInspect) return;
|
||||
|
||||
let baseClasses = downloadBtn.className
|
||||
.replace(/\brounded-[a-z0-9-]+\b/g, "")
|
||||
@@ -522,35 +502,7 @@ function injectCopyPromptNextToDownload(downloadBtn) {
|
||||
copyBtn.remove();
|
||||
}
|
||||
|
||||
// 2. Bookmark Button
|
||||
let bookmarkBtn = parent.querySelector(".leafflow-hover-bookmark");
|
||||
if (enableBookmark) {
|
||||
if (!bookmarkBtn) {
|
||||
bookmarkBtn = document.createElement("button");
|
||||
bookmarkBtn.type = "button";
|
||||
bookmarkBtn.title = "Save to Prompt Bookmarks";
|
||||
bookmarkBtn.setAttribute("aria-label", "Save to Prompt Bookmarks");
|
||||
bookmarkBtn.className = `leafflow-hover-btn leafflow-hover-bookmark ${baseClasses} shrink-0`;
|
||||
bookmarkBtn.innerHTML = BOOKMARK_SVG;
|
||||
|
||||
bookmarkBtn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const activeImg = card.querySelector("img") || img;
|
||||
const success = await saveImageToPromptBookmarks(activeImg.src);
|
||||
bookmarkBtn.innerHTML = success ? BOOKMARK_SUCCESS_SVG : BOOKMARK_FAIL_SVG;
|
||||
setTimeout(() => {
|
||||
bookmarkBtn.innerHTML = BOOKMARK_SVG;
|
||||
}, 2000);
|
||||
};
|
||||
parent.insertBefore(bookmarkBtn, lastBtn.nextSibling);
|
||||
}
|
||||
lastBtn = bookmarkBtn;
|
||||
} else if (bookmarkBtn) {
|
||||
bookmarkBtn.remove();
|
||||
}
|
||||
|
||||
// 3. Inspect Asset (Zoom) Button
|
||||
// 2. Inspect Asset (Zoom) Button
|
||||
let inspectBtn = parent.querySelector(".leafflow-hover-inspect");
|
||||
if (enableInspect) {
|
||||
if (!inspectBtn) {
|
||||
@@ -649,44 +601,6 @@ function injectContextMenuCopy(contextMenu) {
|
||||
});
|
||||
|
||||
downloadLi.parentElement.insertBefore(copyLi, downloadLi.nextSibling);
|
||||
|
||||
if (isCopyEnabled("LeafFlow.3 - 📋 Prompt Actions.03_EnableSaveToPromptSaver") && !contextMenu.querySelector(".leafflow-contextmenu-bookmark")) {
|
||||
const bookmarkLi = document.createElement("li");
|
||||
bookmarkLi.className = "p-contextmenu-item leafflow-contextmenu-bookmark";
|
||||
bookmarkLi.setAttribute("role", "menuitem");
|
||||
bookmarkLi.setAttribute("aria-label", "Save to prompt bookmarks");
|
||||
bookmarkLi.setAttribute("data-pc-section", "item");
|
||||
bookmarkLi.setAttribute("data-p-active", "false");
|
||||
bookmarkLi.setAttribute("data-p-focused", "false");
|
||||
|
||||
const bIconHtml = BOOKMARK_SVG;
|
||||
|
||||
bookmarkLi.innerHTML = `
|
||||
<div class="p-contextmenu-item-content" data-pc-section="itemcontent">
|
||||
<button class="${btnClass}" tabindex="-1" data-pc-section="itemlink">
|
||||
${bIconHtml}
|
||||
<span>Save to prompt bookmarks</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
bookmarkLi.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const span = bookmarkLi.querySelector("span");
|
||||
const src = getActiveImageSrc();
|
||||
if (src) {
|
||||
const success = await saveImageToPromptBookmarks(src);
|
||||
if (span) span.textContent = success ? "Bookmarked! ✅" : "Failed ❌";
|
||||
}
|
||||
setTimeout(() => {
|
||||
contextMenu.style.display = "none";
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
|
||||
}, 500);
|
||||
});
|
||||
|
||||
downloadLi.parentElement.insertBefore(bookmarkLi, copyLi.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Fallback Hook for Older Frontend Action Bars
|
||||
|
||||
+11
-8
@@ -39,9 +39,9 @@ styles.textContent = `
|
||||
document.head.appendChild(styles);
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.LeafFlowDecision",
|
||||
name: "Comfy.SaturnNodesDecision",
|
||||
async setup() {
|
||||
api.addEventListener("leafflow_decision_waiting", (event) => {
|
||||
const handleWaiting = (event) => {
|
||||
const data = event.detail;
|
||||
if (data && data.node_id) {
|
||||
const node = app.graph.getNodeById(data.node_id);
|
||||
@@ -49,9 +49,9 @@ app.registerExtension({
|
||||
node.enableDecisionButtons();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
api.addEventListener("leafflow_decision_resolved", (event) => {
|
||||
const handleResolved = (event) => {
|
||||
const data = event.detail;
|
||||
if (data && data.node_id) {
|
||||
const node = app.graph.getNodeById(data.node_id);
|
||||
@@ -59,10 +59,13 @@ app.registerExtension({
|
||||
node.disableDecisionButtons();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
api.addEventListener("saturnnodes_decision_waiting", handleWaiting);
|
||||
api.addEventListener("saturnnodes_decision_resolved", handleResolved);
|
||||
},
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "LeafFlowDecision") {
|
||||
if (node.comfyClass === "SaturnDecision" || node.comfyClass === "LeafFlowDecision") {
|
||||
const container = document.createElement("div");
|
||||
container.className = "leafflow-decision-container";
|
||||
|
||||
@@ -88,7 +91,7 @@ app.registerExtension({
|
||||
const sendDecision = async (action) => {
|
||||
node.disableDecisionButtons();
|
||||
try {
|
||||
await authenticatedFetch("/leafflow/decision", {
|
||||
await authenticatedFetch("/saturnnodes/decision", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -97,7 +100,7 @@ app.registerExtension({
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[LeafFlow] Failed to send decision:", e);
|
||||
console.error("[SaturnNodes] Failed to send decision:", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+149
-20
@@ -1,5 +1,6 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { isSaturnColorsEnabled } from "../saturnnodes_colors.js";
|
||||
|
||||
// Hash folder name to get a consistent color for the border
|
||||
function getFolderBorderColor(folderName) {
|
||||
@@ -550,10 +551,9 @@ app.registerExtension({
|
||||
};
|
||||
}
|
||||
|
||||
// Adjust sizing constraints dynamically
|
||||
domWidget.computeSize = function() {
|
||||
return [node.size[0] - 30, node.size[1] - 160];
|
||||
};
|
||||
// In ComfyUI LiteGraph (V1), computeLayoutSize handles dynamic widget space distribution.
|
||||
// Removing computeSize avoids an infinite expansion loop in _arrangeWidgets.
|
||||
delete domWidget.computeSize;
|
||||
|
||||
// Add Zoom slider widget
|
||||
const zoomWidget = node.addWidget(
|
||||
@@ -808,14 +808,6 @@ app.registerExtension({
|
||||
if (node.comfyClass === "FolderLoraLoaderVisualPrettyV2" || node.comfyClass === "VisualLoraLoader" || node.type === "FolderLoraLoaderVisualPrettyV2" || node.type === "VisualLoraLoader") {
|
||||
node.size = [380, 360];
|
||||
|
||||
node.computeSize = function() {
|
||||
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
|
||||
if (displayModeWidget && displayModeWidget.value === "Show All") {
|
||||
const viewContainer = document.querySelector(".lora-visual-container");
|
||||
}
|
||||
return [node.size[0], Math.max(360, node.size[1])];
|
||||
};
|
||||
|
||||
const folderWidget = node.widgets.find(w => w.name === "folder");
|
||||
|
||||
const getHiddenWidget = () => {
|
||||
@@ -919,7 +911,7 @@ app.registerExtension({
|
||||
viewContainer.classList.remove("show-all-mode");
|
||||
viewContainer.classList.add("scrollable-mode");
|
||||
const restoredH = node.userCustomHeight || userSavedHeight || 420;
|
||||
const containerH = Math.max(200, restoredH - 100);
|
||||
const containerH = Math.max(180, restoredH - 150);
|
||||
|
||||
viewContainer.style.setProperty("height", `${containerH}px`, "important");
|
||||
viewContainer.style.setProperty("max-height", `${containerH}px`, "important");
|
||||
@@ -938,9 +930,9 @@ app.registerExtension({
|
||||
node.computeSize = function() {
|
||||
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
|
||||
if (displayModeWidget && displayModeWidget.value === "Show All" && gridContainer) {
|
||||
return [node.size[0], Math.max(360, (gridContainer.scrollHeight || 0) + 160)];
|
||||
return [Math.max(380, node.size[0]), Math.max(360, (gridContainer.scrollHeight || 0) + 160)];
|
||||
}
|
||||
return [node.size[0], Math.max(360, node.size[1])];
|
||||
return [Math.max(380, node.size[0]), 360];
|
||||
};
|
||||
|
||||
const updateNodeSize = () => {
|
||||
@@ -1054,12 +1046,14 @@ app.registerExtension({
|
||||
const domWidget = node.addDOMWidget("lora_visual_picker", "HTML", viewContainer, {
|
||||
getValue() { return getHiddenWidget().value; },
|
||||
setValue(val) { getHiddenWidget().value = val; },
|
||||
serialize: false
|
||||
serialize: false,
|
||||
getMinHeight() { return 180; }
|
||||
});
|
||||
|
||||
domWidget.computeSize = function() {
|
||||
return [node.size[0] - 30, node.size[1] - 190];
|
||||
};
|
||||
// In ComfyUI LiteGraph (V1), computeLayoutSize handles dynamic widget space distribution.
|
||||
// A computeSize returning (node.size[1] - offset) causes an infinite loop in _arrangeWidgets
|
||||
// because LiteGraph sees (l > t) and expands node.size on every animation frame.
|
||||
delete domWidget.computeSize;
|
||||
|
||||
const onResize = node.onResize;
|
||||
node.onResize = function(size) {
|
||||
@@ -1070,7 +1064,7 @@ app.registerExtension({
|
||||
node.userCustomHeight = size[1];
|
||||
userSavedHeight = size[1];
|
||||
if (viewContainer && viewContainer.style) {
|
||||
const h = Math.max(200, size[1] - 100);
|
||||
const h = Math.max(180, size[1] - 150);
|
||||
viewContainer.style.setProperty("height", `${h}px`, "important");
|
||||
viewContainer.style.setProperty("max-height", `${h}px`, "important");
|
||||
}
|
||||
@@ -1091,6 +1085,78 @@ app.registerExtension({
|
||||
);
|
||||
zoomWidget.serialize = false;
|
||||
|
||||
// Live Selected LoRAs count preview widget matching ResolutionSelector styling
|
||||
let loraCountWidget = node.widgets?.find(w => w.name === "lora_count_preview" || w._isLoraCount);
|
||||
let loraBadgeSpan = node._loraBadgeSpan;
|
||||
if (!loraCountWidget) {
|
||||
const loraBadgeEl = document.createElement("div");
|
||||
loraBadgeEl.className = "not-disabled:bg-component-node-widget-background not-disabled:text-component-node-foreground [[readonly]]:bg-component-node-widget-background-disabled border-none rounded-md flex w-full items-center justify-center gap-2 px-2 h-6 col-span-2";
|
||||
loraBadgeEl.setAttribute("data-widget-name", "lora_count_preview");
|
||||
loraBadgeEl.setAttribute("node-type", "VisualLoraLoader");
|
||||
loraBadgeEl.style.cssText = "display: flex; align-items: center; justify-content: center; gap: 6px; padding: 0 8px; height: 24px !important; min-height: 24px !important; max-height: 24px !important; flex-grow: 0 !important; flex-shrink: 0 !important; align-self: center !important; overflow: hidden !important; border-radius: 6px; background: var(--component-node-widget-background, rgba(255, 255, 255, 0.06)); color: var(--component-node-foreground, #e4e4e7); font-size: 12px; font-weight: 500; width: 100%; box-sizing: border-box; user-select: none;";
|
||||
|
||||
loraBadgeSpan = document.createElement("span");
|
||||
loraBadgeSpan.className = "text-xs";
|
||||
loraBadgeSpan.style.cssText = "font-weight: 600;";
|
||||
loraBadgeSpan.textContent = "0 LoRAs Selected";
|
||||
loraBadgeEl.appendChild(loraBadgeSpan);
|
||||
node._loraBadgeSpan = loraBadgeSpan;
|
||||
node._loraBadgeEl = loraBadgeEl;
|
||||
|
||||
const enforceFixedLoraBadgeSize = () => {
|
||||
loraBadgeEl.style.setProperty("height", "24px", "important");
|
||||
loraBadgeEl.style.setProperty("min-height", "24px", "important");
|
||||
loraBadgeEl.style.setProperty("max-height", "24px", "important");
|
||||
loraBadgeEl.style.setProperty("flex-grow", "0", "important");
|
||||
loraBadgeEl.style.setProperty("flex-shrink", "0", "important");
|
||||
loraBadgeEl.style.setProperty("align-self", "center", "important");
|
||||
const widgetContainer = loraBadgeEl.closest?.(".lg-node-widget");
|
||||
if (widgetContainer) {
|
||||
widgetContainer.style.setProperty("align-self", "center", "important");
|
||||
widgetContainer.style.setProperty("flex-grow", "0", "important");
|
||||
}
|
||||
};
|
||||
node._enforceFixedLoraBadgeSize = enforceFixedLoraBadgeSize;
|
||||
|
||||
loraCountWidget = node.addDOMWidget("lora_count_preview", "preview", loraBadgeEl, {
|
||||
serialize: false,
|
||||
getValue() { return loraBadgeSpan.textContent; },
|
||||
setValue(v) { loraBadgeSpan.textContent = v; }
|
||||
});
|
||||
loraCountWidget._isLoraCount = true;
|
||||
}
|
||||
|
||||
if (loraCountWidget) {
|
||||
// Crucial: shadow computeLayoutSize so ComfyUI Vue deriveWidgetRenderState evaluates hasLayoutSize to false.
|
||||
// This forces grid-template-rows to use 'min-content' (24px) instead of 'auto' (which was stealing half the node height).
|
||||
Object.defineProperty(loraCountWidget, "computeLayoutSize", {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
loraCountWidget.hasLayoutSize = false;
|
||||
loraCountWidget.computeSize = function() {
|
||||
return [node.size ? (node.size[0] - 20) : 200, 24];
|
||||
};
|
||||
}
|
||||
|
||||
const updateLoraBadgeColor = () => {
|
||||
const enabled = isSaturnColorsEnabled();
|
||||
if (node._loraBadgeSpan) {
|
||||
node._loraBadgeSpan.style.color = enabled ? "#f59e0b" : "var(--text-muted, #a1a1aa)";
|
||||
}
|
||||
const badgeEl = node._loraBadgeEl;
|
||||
if (badgeEl) {
|
||||
badgeEl.style.background = enabled
|
||||
? "rgba(180, 140, 95, 0.12)"
|
||||
: "var(--component-node-widget-background, rgba(255, 255, 255, 0.06))";
|
||||
badgeEl.style.border = enabled
|
||||
? "1px solid rgba(180, 140, 95, 0.35)"
|
||||
: "none";
|
||||
}
|
||||
};
|
||||
node._updateCounterBadgeColor = updateLoraBadgeColor;
|
||||
|
||||
let activeRequest = null;
|
||||
let debounceTimer = null;
|
||||
let foldersMap = {};
|
||||
@@ -1115,6 +1181,21 @@ app.registerExtension({
|
||||
return currentSelected;
|
||||
};
|
||||
|
||||
const updateLoraCountDisplay = () => {
|
||||
node._enforceFixedLoraBadgeSize?.();
|
||||
const count = getSelectedLoras().length;
|
||||
node._selectedLoraCount = count;
|
||||
const label = `${count} LoRA${count === 1 ? "" : "s"} Selected`;
|
||||
if (node._loraBadgeSpan) {
|
||||
node._loraBadgeSpan.textContent = label;
|
||||
}
|
||||
if (loraCountWidget) {
|
||||
loraCountWidget.value = label;
|
||||
}
|
||||
updateLoraBadgeColor();
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const updateFolderCheckboxes = () => {
|
||||
const selectedList = getSelectedLoras();
|
||||
|
||||
@@ -1166,6 +1247,7 @@ app.registerExtension({
|
||||
});
|
||||
|
||||
updateFolderCheckboxes();
|
||||
updateLoraCountDisplay();
|
||||
node.triggerSlotEvent?.(0);
|
||||
};
|
||||
|
||||
@@ -1553,9 +1635,55 @@ app.registerExtension({
|
||||
|
||||
syncModeToggleUI();
|
||||
syncScrapeToggleUI();
|
||||
updateLoraCountDisplay();
|
||||
setTimeout(() => updateVisualGrid(), 100);
|
||||
};
|
||||
|
||||
const origOnDrawForeground = node.onDrawForeground;
|
||||
node.onDrawForeground = function(ctx) {
|
||||
if (node.flags?.collapsed) {
|
||||
const isSaturn = isSaturnColorsEnabled();
|
||||
const count = node._selectedLoraCount ?? getSelectedLoras().length;
|
||||
const label = `${count} LoRA${count === 1 ? "" : "s"}`;
|
||||
ctx.save();
|
||||
ctx.font = "bold 11px Inter, system-ui, sans-serif";
|
||||
const badgeText = label;
|
||||
const tw = ctx.measureText(badgeText).width;
|
||||
const bx = node.size[0] - tw - 16;
|
||||
const by = -LiteGraph.NODE_TITLE_HEIGHT + 3;
|
||||
|
||||
if (isSaturn) {
|
||||
ctx.fillStyle = count > 0 ? "rgba(180, 140, 95, 0.25)" : "rgba(255, 255, 255, 0.08)";
|
||||
ctx.strokeStyle = count > 0 ? "rgba(180, 140, 95, 0.6)" : "rgba(255, 255, 255, 0.2)";
|
||||
} else {
|
||||
ctx.fillStyle = count > 0 ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.08)";
|
||||
ctx.strokeStyle = count > 0 ? "rgba(255, 255, 255, 0.25)" : "rgba(255, 255, 255, 0.2)";
|
||||
}
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) {
|
||||
ctx.roundRect(bx, by, tw + 10, 18, 4);
|
||||
} else {
|
||||
ctx.rect(bx, by, tw + 10, 18);
|
||||
}
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = isSaturn ? (count > 0 ? "#decbb2" : "#cbd5e1") : (count > 0 ? "#e2e8f0" : "#cbd5e1");
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(badgeText, bx + 5, by + 9);
|
||||
ctx.restore();
|
||||
}
|
||||
if (origOnDrawForeground) origOnDrawForeground.apply(this, arguments);
|
||||
};
|
||||
|
||||
const origOnResize = node.onResize;
|
||||
node.onResize = function() {
|
||||
if (origOnResize) origOnResize.apply(this, arguments);
|
||||
node._enforceFixedLoraBadgeSize?.();
|
||||
};
|
||||
|
||||
syncModeToggleUI();
|
||||
syncScrapeToggleUI();
|
||||
|
||||
@@ -1570,6 +1698,7 @@ app.registerExtension({
|
||||
applyDisplayMode(displayModeWidget.value || "Scrollable");
|
||||
}
|
||||
updateVisualGrid();
|
||||
updateLoraCountDisplay();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-19
@@ -216,14 +216,6 @@ app.registerExtension({
|
||||
if (isVisualImageLoader) {
|
||||
node.size = [380, 480];
|
||||
|
||||
node.computeSize = function() {
|
||||
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
|
||||
if (displayModeWidget && displayModeWidget.value === "Show All") {
|
||||
// Logic added below
|
||||
}
|
||||
return [node.size[0], Math.max(480, node.size[1])];
|
||||
};
|
||||
|
||||
const folderWidget = node.widgets ? node.widgets.find(w => w.name === "folder_path") : null;
|
||||
|
||||
const getHiddenWidget = (name, defaultVal) => {
|
||||
@@ -288,8 +280,6 @@ app.registerExtension({
|
||||
gridContainer.className = "img-grid-container";
|
||||
viewContainer.appendChild(gridContainer);
|
||||
|
||||
node.computeSize = function() {
|
||||
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
|
||||
let userSavedHeight = 480;
|
||||
|
||||
const applyDisplayMode = (mode) => {
|
||||
@@ -314,7 +304,7 @@ app.registerExtension({
|
||||
viewContainer.classList.remove("show-all-mode");
|
||||
viewContainer.classList.add("scrollable-mode");
|
||||
const restoredH = node.userCustomHeight || userSavedHeight || 480;
|
||||
const containerH = Math.max(200, restoredH - 100);
|
||||
const containerH = Math.max(180, restoredH - 150);
|
||||
|
||||
viewContainer.style.setProperty("height", `${containerH}px`, "important");
|
||||
viewContainer.style.setProperty("max-height", `${containerH}px`, "important");
|
||||
@@ -333,9 +323,9 @@ app.registerExtension({
|
||||
node.computeSize = function() {
|
||||
const displayModeWidget = node.widgets ? node.widgets.find(w => w.name === "display_mode") : null;
|
||||
if (displayModeWidget && displayModeWidget.value === "Show All" && gridContainer) {
|
||||
return [node.size[0], Math.max(420, (gridContainer.scrollHeight || 0) + 160)];
|
||||
return [Math.max(380, node.size[0]), Math.max(420, (gridContainer.scrollHeight || 0) + 160)];
|
||||
}
|
||||
return [node.size[0], Math.max(420, node.size[1])];
|
||||
return [Math.max(380, node.size[0]), 420];
|
||||
};
|
||||
|
||||
const updateNodeSize = () => {
|
||||
@@ -355,9 +345,14 @@ app.registerExtension({
|
||||
const domWidget = node.addDOMWidget("img_visual_picker", "HTML", viewContainer, {
|
||||
getValue() { return getHiddenWidget("_selected_image", "").value; },
|
||||
setValue(val) { getHiddenWidget("_selected_image", "").value = val; },
|
||||
serialize: false
|
||||
serialize: false,
|
||||
getMinHeight() { return 180; }
|
||||
});
|
||||
|
||||
// In ComfyUI LiteGraph (V1), computeLayoutSize handles dynamic widget space distribution.
|
||||
// Removing computeSize avoids an infinite expansion loop in _arrangeWidgets.
|
||||
delete domWidget.computeSize;
|
||||
|
||||
const onResize = node.onResize;
|
||||
node.onResize = function(size) {
|
||||
if (onResize) onResize.apply(this, arguments);
|
||||
@@ -367,17 +362,13 @@ app.registerExtension({
|
||||
node.userCustomHeight = size[1];
|
||||
userSavedHeight = size[1];
|
||||
if (viewContainer && viewContainer.style) {
|
||||
const h = Math.max(200, size[1] - 100);
|
||||
const h = Math.max(180, size[1] - 150);
|
||||
viewContainer.style.setProperty("height", `${h}px`, "important");
|
||||
viewContainer.style.setProperty("max-height", `${h}px`, "important");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
domWidget.computeSize = function() {
|
||||
return [node.size[0] - 30, node.size[1] - 140];
|
||||
};
|
||||
|
||||
// Zoom Slider Widget
|
||||
const zoomWidget = node.addWidget(
|
||||
"slider",
|
||||
|
||||
+10
-10
@@ -1,9 +1,9 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { authenticatedFetch } from "./auth_helper.js";
|
||||
|
||||
// LeafFlow Local File Runner In-Memory Consent Gate Extension
|
||||
// SaturnNodes Local File Runner In-Memory Consent Gate Extension
|
||||
app.registerExtension({
|
||||
name: "LeafFlow.LocalRunner.Security",
|
||||
name: "SaturnNodes.LocalRunner.Security",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "RunLocalFileNode") return;
|
||||
|
||||
@@ -19,7 +19,7 @@ app.registerExtension({
|
||||
if (isArmed) {
|
||||
const remainingSec = Math.max(0, Math.round((node._authExpiresAt - now) / 1000));
|
||||
const remainingMin = Math.ceil(remainingSec / 60);
|
||||
node.title = `🍃 ⚡ Run Local File [ARMED (${remainingMin}m)]`;
|
||||
node.title = `🪐 ⚡ Run Local File [ARMED (${remainingMin}m)]`;
|
||||
node.color = "#047857";
|
||||
node.bgcolor = "#064e3b";
|
||||
if (authButton) {
|
||||
@@ -27,7 +27,7 @@ app.registerExtension({
|
||||
}
|
||||
} else {
|
||||
node._isAuthorized = false;
|
||||
node.title = "🍃 ⚡ Run Local File [DISARMED]";
|
||||
node.title = "🪐 ⚡ Run Local File [DISARMED]";
|
||||
node.color = "#7f1d1d";
|
||||
node.bgcolor = "#18181b";
|
||||
if (authButton) {
|
||||
@@ -45,9 +45,9 @@ app.registerExtension({
|
||||
async () => {
|
||||
const scriptName = filePathWidget?.value?.trim() || "configured script";
|
||||
const confirmed = confirm(
|
||||
"🍃 LeafFlow Security Verification\n\n" +
|
||||
"🪐 SaturnNodes Security Verification\n\n" +
|
||||
`Authorize a SINGLE run of local script:\n'${scriptName}'\n\n` +
|
||||
"⚠️ Notice: Scripts can execute shell commands on your system.\n" +
|
||||
"Notice: Scripts can execute shell commands on your system.\n" +
|
||||
"• The script must reside inside 'ComfyUI/scripts/'.\n" +
|
||||
"• Authorization is temporary (expires in 5 minutes) and single-use.\n" +
|
||||
"• External workflows can NEVER run scripts without your live authorization.\n\n" +
|
||||
@@ -57,7 +57,7 @@ app.registerExtension({
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const resp = await authenticatedFetch("/leafflow/local_runner/authorize", {
|
||||
const resp = await authenticatedFetch("/saturnnodes/local_runner/authorize", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
@@ -72,7 +72,7 @@ app.registerExtension({
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Authorization Failed",
|
||||
summary: "🪐 Authorization Failed",
|
||||
detail: errMsg,
|
||||
life: 6000
|
||||
});
|
||||
@@ -95,7 +95,7 @@ app.registerExtension({
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[LeafFlow Security] Error authorizing script run:", e);
|
||||
console.error("[SaturnNodes Security] Error authorizing script run:", e);
|
||||
alert("❌ Error communicating with authorization service: " + e.message);
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ app.registerExtension({
|
||||
ctx.textAlign = "center";
|
||||
ctx.font = "italic 9px sans-serif";
|
||||
ctx.fillStyle = "#9ca3af";
|
||||
ctx.fillText("📂 Script Location: ComfyUI/scripts/", node.size[0] / 2, node.size[1] - 19);
|
||||
ctx.fillText("Script Location: ComfyUI/scripts/", node.size[0] / 2, node.size[1] - 19);
|
||||
|
||||
ctx.font = "bold 10px sans-serif";
|
||||
if (!isArmed) {
|
||||
|
||||
+26
-21
@@ -2,7 +2,7 @@ import { app } from "/scripts/app.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { authenticatedFetch } from "./auth_helper.js";
|
||||
|
||||
const EXTENSION_NAME = "LeafFlow.PowerControlSidebar";
|
||||
const EXTENSION_NAME = "SaturnNodes.PowerControlSidebar";
|
||||
const STYLE_ID = "leafflow-power-sidebar-styles";
|
||||
|
||||
let currentPowerState = {
|
||||
@@ -190,7 +190,7 @@ function updateSidebarButtonVisuals() {
|
||||
|
||||
async function fetchPowerStatus() {
|
||||
try {
|
||||
const resp = await api.fetchApi("/leafflow/power/status");
|
||||
const resp = await api.fetchApi("/saturnnodes/power/status");
|
||||
if (resp.ok) {
|
||||
currentPowerState = await resp.json();
|
||||
updateSidebarButtonVisuals();
|
||||
@@ -279,14 +279,14 @@ function checkProcessManagementEnabled() {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "warn",
|
||||
summary: "🍃 Process Management Disabled",
|
||||
detail: "Enable 'Allow Process Management' in LeafFlow settings to restart or shut down ComfyUI.",
|
||||
summary: "🪐 Process Management Disabled",
|
||||
detail: "Enable 'Allow Process Management' in SaturnNodes settings to restart or shut down ComfyUI.",
|
||||
life: 7000
|
||||
});
|
||||
}
|
||||
openConfirmModal(
|
||||
"Process Management Disabled",
|
||||
"Server restart and shutdown controls are currently disabled for security.\n\nTo enable them, go to ComfyUI Settings ⚙️ -> LeafFlow -> Pause Controls -> Turn ON 'Allow Process Management (Restart / Shutdown)'.",
|
||||
"Server restart and shutdown controls are currently disabled for security.\n\nTo enable them, go to ComfyUI Settings ⚙️ -> SaturnNodes -> Pause Controls -> Turn ON 'Allow Process Management (Restart / Shutdown)'.",
|
||||
null,
|
||||
false
|
||||
);
|
||||
@@ -302,6 +302,10 @@ function closePowerPopup() {
|
||||
}
|
||||
}
|
||||
|
||||
async function powerFetch(endpointSuffix, options = {}) {
|
||||
return authenticatedFetch(`/saturnnodes/power/${endpointSuffix}`, options);
|
||||
}
|
||||
|
||||
function showPowerSidePopup(anchorElement) {
|
||||
if (activePopup) {
|
||||
closePowerPopup();
|
||||
@@ -341,7 +345,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
cancelBtn.onclick = async (e) => {
|
||||
e.stopPropagation();
|
||||
closePowerPopup();
|
||||
await authenticatedFetch("/leafflow/power/arm", {
|
||||
await powerFetch("arm", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: null })
|
||||
@@ -350,7 +354,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "info",
|
||||
summary: "🍃 LeafFlow Power",
|
||||
summary: "🪐 SaturnNodes Power",
|
||||
detail: "Scheduled power action has been cancelled.",
|
||||
life: 3000
|
||||
});
|
||||
@@ -376,7 +380,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
"Restart ComfyUI Immediately?",
|
||||
"Are you sure you want to restart ComfyUI immediately? Any active prompt generation will be interrupted.",
|
||||
async () => {
|
||||
const tokenResp = await authenticatedFetch("/leafflow/power/request_token", {
|
||||
const tokenResp = await powerFetch("request_token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "restart" })
|
||||
@@ -386,7 +390,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Restart Denied",
|
||||
summary: "🪐 Restart Denied",
|
||||
detail: err.error || "Failed to obtain power action ticket.",
|
||||
life: 6000
|
||||
});
|
||||
@@ -394,7 +398,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
return;
|
||||
}
|
||||
const tokenData = await tokenResp.json();
|
||||
const resp = await authenticatedFetch("/leafflow/power/confirm_action", {
|
||||
const resp = await powerFetch("confirm_action", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ticket: tokenData.ticket, action: "restart" })
|
||||
@@ -404,7 +408,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Restart Failed",
|
||||
summary: "🪐 Restart Failed",
|
||||
detail: err.error || "Failed to trigger restart.",
|
||||
life: 6000
|
||||
});
|
||||
@@ -443,7 +447,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
"Shutdown ComfyUI Server?",
|
||||
"Are you sure you want to SHUT DOWN the ComfyUI server immediately? The process will exit.",
|
||||
async () => {
|
||||
const tokenResp = await authenticatedFetch("/leafflow/power/request_token", {
|
||||
const tokenResp = await powerFetch("request_token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "shutdown" })
|
||||
@@ -453,7 +457,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Shutdown Denied",
|
||||
summary: "🪐 Shutdown Denied",
|
||||
detail: err.error || "Failed to obtain power action ticket.",
|
||||
life: 6000
|
||||
});
|
||||
@@ -461,7 +465,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
return;
|
||||
}
|
||||
const tokenData = await tokenResp.json();
|
||||
const resp = await authenticatedFetch("/leafflow/power/confirm_action", {
|
||||
const resp = await powerFetch("confirm_action", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ticket: tokenData.ticket, action: "shutdown" })
|
||||
@@ -471,7 +475,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Shutdown Failed",
|
||||
summary: "🪐 Shutdown Failed",
|
||||
detail: err.error || "Failed to trigger shutdown.",
|
||||
life: 6000
|
||||
});
|
||||
@@ -514,7 +518,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
"Schedule Restart After Queue Finish?",
|
||||
"ComfyUI will wait until all remaining prompts in the queue finish and the system returns to idle before restarting. (Note: If queue is paused, it will not restart).",
|
||||
async () => {
|
||||
const resp = await authenticatedFetch("/leafflow/power/arm", {
|
||||
const resp = await powerFetch("arm", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "restart" })
|
||||
@@ -524,7 +528,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Scheduling Failed",
|
||||
summary: "🪐 Scheduling Failed",
|
||||
detail: err.error || "Failed to schedule restart.",
|
||||
life: 6000
|
||||
});
|
||||
@@ -562,7 +566,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
"Schedule Shutdown After Queue Finish?",
|
||||
"ComfyUI will wait until all queued prompts complete and the system returns to idle before shutting down completely.",
|
||||
async () => {
|
||||
const resp = await authenticatedFetch("/leafflow/power/arm", {
|
||||
const resp = await powerFetch("arm", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action: "shutdown" })
|
||||
@@ -572,7 +576,7 @@ function showPowerSidePopup(anchorElement) {
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "error",
|
||||
summary: "🍃 Scheduling Failed",
|
||||
summary: "🪐 Scheduling Failed",
|
||||
detail: err.error || "Failed to schedule shutdown.",
|
||||
life: 6000
|
||||
});
|
||||
@@ -683,10 +687,11 @@ app.registerExtension({
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
api.addEventListener("leafflow_power_status", (e) => {
|
||||
const handlePowerStatus = (e) => {
|
||||
currentPowerState = e.detail || currentPowerState;
|
||||
updateSidebarButtonVisuals();
|
||||
});
|
||||
};
|
||||
api.addEventListener("saturnnodes_power_status", handlePowerStatus);
|
||||
} catch (_) {}
|
||||
|
||||
setInterval(fetchPowerStatus, 3000);
|
||||
|
||||
@@ -296,7 +296,7 @@ app.registerExtension({
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("leafflow_update_preview_aspect_ratio", (event) => {
|
||||
api.addEventListener("saturnnodes_update_preview_aspect_ratio", (event) => {
|
||||
const detail = event.detail;
|
||||
if (!detail || !detail.node_id) return;
|
||||
|
||||
|
||||
+91
-44
@@ -1,4 +1,5 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { app } from "/scripts/app.js";
|
||||
import { isSaturnColorsEnabled } from "../saturnnodes_colors.js";
|
||||
|
||||
function countPromptsInText(text, separator, customRegex) {
|
||||
if (!text || !text.trim()) return 0;
|
||||
@@ -46,50 +47,77 @@ app.registerExtension({
|
||||
const sepWidget = node.widgets?.find(w => w.name === "separator");
|
||||
const regexWidget = node.widgets?.find(w => w.name === "custom_regex");
|
||||
|
||||
// Add the live count display widget
|
||||
const countWidget = node.addWidget("text", "🍃 Prompts", "0 Prompts", () => {}, { serialize: false });
|
||||
countWidget.disabled = true;
|
||||
// Live Prompt Count preview widget matching ResolutionSelector styling
|
||||
let countWidget = node.widgets?.find(w => w.name === "prompt_count_preview" || w._isPromptCount);
|
||||
let badgeSpan = node._promptBadgeSpan;
|
||||
if (!countWidget) {
|
||||
const badgeEl = document.createElement("div");
|
||||
badgeEl.className = "not-disabled:bg-component-node-widget-background not-disabled:text-component-node-foreground [[readonly]]:bg-component-node-widget-background-disabled border-none rounded-md flex w-full items-center justify-center gap-2 px-2 h-6 col-span-2";
|
||||
badgeEl.setAttribute("data-widget-name", "prompt_count_preview");
|
||||
badgeEl.setAttribute("node-type", "PromptCounter");
|
||||
badgeEl.style.cssText = "display: flex; align-items: center; justify-content: center; gap: 6px; padding: 0 8px; height: 24px !important; min-height: 24px !important; max-height: 24px !important; flex-grow: 0 !important; flex-shrink: 0 !important; align-self: center !important; overflow: hidden !important; border-radius: 6px; background: var(--component-node-widget-background, rgba(255, 255, 255, 0.06)); color: var(--component-node-foreground, #e4e4e7); font-size: 12px; font-weight: 500; width: 100%; box-sizing: border-box; user-select: none;";
|
||||
|
||||
// Custom drawing for counter widget
|
||||
countWidget.computeSize = function(width) {
|
||||
return [width, 28];
|
||||
};
|
||||
badgeSpan = document.createElement("span");
|
||||
badgeSpan.className = "text-xs";
|
||||
badgeSpan.style.cssText = "font-weight: 600;";
|
||||
badgeSpan.textContent = "0 Prompts";
|
||||
badgeEl.appendChild(badgeSpan);
|
||||
node._promptBadgeSpan = badgeSpan;
|
||||
node._promptBadgeEl = badgeEl;
|
||||
|
||||
countWidget.draw = function(ctx, n, widget_width, y, widget_height) {
|
||||
ctx.save();
|
||||
const count = n._promptCount ?? 0;
|
||||
const label = `${count} Prompt${count === 1 ? "" : "s"}`;
|
||||
const enforceFixedBadgeSize = () => {
|
||||
badgeEl.style.setProperty("height", "24px", "important");
|
||||
badgeEl.style.setProperty("min-height", "24px", "important");
|
||||
badgeEl.style.setProperty("max-height", "24px", "important");
|
||||
badgeEl.style.setProperty("flex-grow", "0", "important");
|
||||
badgeEl.style.setProperty("flex-shrink", "0", "important");
|
||||
badgeEl.style.setProperty("align-self", "center", "important");
|
||||
const widgetContainer = badgeEl.closest?.(".lg-node-widget");
|
||||
if (widgetContainer) {
|
||||
widgetContainer.style.setProperty("align-self", "center", "important");
|
||||
widgetContainer.style.setProperty("flex-grow", "0", "important");
|
||||
}
|
||||
};
|
||||
node._enforceFixedBadgeSize = enforceFixedBadgeSize;
|
||||
|
||||
const margin = 8;
|
||||
const badgeX = margin;
|
||||
const badgeY = y + 2;
|
||||
const badgeW = widget_width - margin * 2;
|
||||
const badgeH = Math.max(24, widget_height - 4);
|
||||
const radius = 6;
|
||||
countWidget = node.addDOMWidget("prompt_count_preview", "preview", badgeEl, {
|
||||
serialize: false,
|
||||
getValue() { return badgeSpan.textContent; },
|
||||
setValue(v) { badgeSpan.textContent = v; }
|
||||
});
|
||||
countWidget._isPromptCount = true;
|
||||
}
|
||||
|
||||
// Subtle dark background pill
|
||||
ctx.fillStyle = count > 0 ? "rgba(34, 197, 94, 0.15)" : "rgba(255, 255, 255, 0.05)";
|
||||
ctx.strokeStyle = count > 0 ? "rgba(34, 197, 94, 0.5)" : "rgba(255, 255, 255, 0.15)";
|
||||
ctx.lineWidth = 1;
|
||||
if (countWidget) {
|
||||
// Crucial: shadow computeLayoutSize so ComfyUI Vue deriveWidgetRenderState evaluates hasLayoutSize to false.
|
||||
// This forces grid-template-rows to use 'min-content' (24px) instead of 'auto' (which was stealing half the node height).
|
||||
Object.defineProperty(countWidget, "computeLayoutSize", {
|
||||
value: undefined,
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
countWidget.hasLayoutSize = false;
|
||||
countWidget.computeSize = function() {
|
||||
return [node.size ? (node.size[0] - 20) : 200, 24];
|
||||
};
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) {
|
||||
ctx.roundRect(badgeX, badgeY, badgeW, badgeH, radius);
|
||||
} else {
|
||||
ctx.rect(badgeX, badgeY, badgeW, badgeH);
|
||||
function updatePromptBadgeColor() {
|
||||
const enabled = isSaturnColorsEnabled();
|
||||
if (node._promptBadgeSpan) {
|
||||
node._promptBadgeSpan.style.color = enabled ? "#f59e0b" : "var(--text-muted, #a1a1aa)";
|
||||
}
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Text
|
||||
ctx.font = "bold 13px Inter, system-ui, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = count > 0 ? "#4ade80" : "#94a3b8";
|
||||
ctx.fillText(`🍃 ${label}`, badgeX + badgeW / 2, badgeY + badgeH / 2);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
const badgeEl = node._promptBadgeEl;
|
||||
if (badgeEl) {
|
||||
badgeEl.style.background = enabled
|
||||
? "rgba(180, 140, 95, 0.12)"
|
||||
: "var(--component-node-widget-background, rgba(255, 255, 255, 0.06))";
|
||||
badgeEl.style.border = enabled
|
||||
? "1px solid rgba(180, 140, 95, 0.35)"
|
||||
: "none";
|
||||
}
|
||||
}
|
||||
node._updateCounterBadgeColor = updatePromptBadgeColor;
|
||||
|
||||
function updateCount() {
|
||||
const text = (textWidget?.inputEl ? textWidget.inputEl.value : textWidget?.value) || "";
|
||||
@@ -98,7 +126,13 @@ app.registerExtension({
|
||||
const count = countPromptsInText(text, sep, regex);
|
||||
node._promptCount = count;
|
||||
const label = `${count} Prompt${count === 1 ? "" : "s"}`;
|
||||
countWidget.value = label;
|
||||
if (node._promptBadgeSpan) {
|
||||
node._promptBadgeSpan.textContent = label;
|
||||
}
|
||||
if (countWidget) {
|
||||
countWidget.value = label;
|
||||
}
|
||||
updatePromptBadgeColor();
|
||||
|
||||
// Manage custom_regex enable/disable
|
||||
if (regexWidget) {
|
||||
@@ -189,17 +223,23 @@ app.registerExtension({
|
||||
}
|
||||
|
||||
if (node.flags?.collapsed) {
|
||||
const isSaturn = isSaturnColorsEnabled();
|
||||
const count = node._promptCount ?? 0;
|
||||
const label = `${count} Prompt${count === 1 ? "" : "s"}`;
|
||||
ctx.save();
|
||||
ctx.font = "bold 11px Inter, system-ui, sans-serif";
|
||||
const badgeText = `🍃 ${label}`;
|
||||
const badgeText = label;
|
||||
const tw = ctx.measureText(badgeText).width;
|
||||
const bx = node.size[0] - tw - 16;
|
||||
const by = -LiteGraph.NODE_TITLE_HEIGHT + 3;
|
||||
|
||||
ctx.fillStyle = count > 0 ? "rgba(34, 197, 94, 0.2)" : "rgba(255, 255, 255, 0.08)";
|
||||
ctx.strokeStyle = count > 0 ? "rgba(34, 197, 94, 0.6)" : "rgba(255, 255, 255, 0.2)";
|
||||
if (isSaturn) {
|
||||
ctx.fillStyle = count > 0 ? "rgba(180, 140, 95, 0.25)" : "rgba(255, 255, 255, 0.08)";
|
||||
ctx.strokeStyle = count > 0 ? "rgba(180, 140, 95, 0.6)" : "rgba(255, 255, 255, 0.2)";
|
||||
} else {
|
||||
ctx.fillStyle = count > 0 ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.08)";
|
||||
ctx.strokeStyle = count > 0 ? "rgba(255, 255, 255, 0.25)" : "rgba(255, 255, 255, 0.2)";
|
||||
}
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
if (ctx.roundRect) {
|
||||
@@ -210,7 +250,7 @@ app.registerExtension({
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = count > 0 ? "#4ade80" : "#cbd5e1";
|
||||
ctx.fillStyle = isSaturn ? (count > 0 ? "#decbb2" : "#cbd5e1") : (count > 0 ? "#e2e8f0" : "#cbd5e1");
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(badgeText, bx + 5, by + 9);
|
||||
@@ -220,11 +260,18 @@ app.registerExtension({
|
||||
if (origOnDrawForeground) origOnDrawForeground.apply(this, arguments);
|
||||
};
|
||||
|
||||
const origOnResize = node.onResize;
|
||||
node.onResize = function() {
|
||||
if (origOnResize) origOnResize.apply(this, arguments);
|
||||
node._enforceFixedBadgeSize?.();
|
||||
};
|
||||
|
||||
const origOnConfigure = node.onConfigure;
|
||||
node.onConfigure = function() {
|
||||
if (origOnConfigure) origOnConfigure.apply(this, arguments);
|
||||
setTimeout(() => {
|
||||
hookInputEl();
|
||||
node._enforceFixedBadgeSize?.();
|
||||
updateCount();
|
||||
}, 40);
|
||||
};
|
||||
|
||||
+16
-15
@@ -3,35 +3,36 @@ import { api } from "/scripts/api.js";
|
||||
import { authenticatedFetch } from "./auth_helper.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.PromptQueueIterator",
|
||||
name: "ComfyUI.SaturnNodes.PromptQueueIterator",
|
||||
async setup() {
|
||||
if (api && api.addEventListener) {
|
||||
api.addEventListener("leafflow_prompt_iterator_progress", (event) => {
|
||||
const handleProgress = (event) => {
|
||||
const data = event.detail;
|
||||
if (!data || !data.node_id) return;
|
||||
const node = app.graph?.getNodeById(data.node_id);
|
||||
if (node && node.widgets) {
|
||||
const statusWidget = node.widgets.find(w => w.name === "🍃 Progress");
|
||||
const statusWidget = node.widgets.find(w => w.name === "Progress");
|
||||
if (statusWidget) {
|
||||
statusWidget.value = `🍃 Run ${data.current_run} / ${data.total_runs}`;
|
||||
statusWidget.value = `Run ${data.current_run} / ${data.total_runs}`;
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
api.addEventListener("saturnnodes_prompt_iterator_progress", handleProgress);
|
||||
}
|
||||
},
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "PromptQueueIterator") {
|
||||
// 1. Live Progress Status Widget
|
||||
const statusWidget = node.addWidget("text", "🍃 Progress", "🍃 Ready (-- / --)", () => {}, { serialize: false });
|
||||
const statusWidget = node.addWidget("text", "Progress", "Ready (-- / --)", () => {}, { serialize: false });
|
||||
statusWidget.disabled = true;
|
||||
|
||||
// 2. Reset Counter (0) Button
|
||||
const resetBtn = node.addWidget("button", "🔄 Reset Counter (0)", null, async () => {
|
||||
const resetBtn = node.addWidget("button", "Reset Counter (0)", null, async () => {
|
||||
try {
|
||||
resetBtn.name = "⏳ Resetting...";
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
const resp = await authenticatedFetch("/leafflow/prompt_iterator/reset_node", {
|
||||
const resp = await authenticatedFetch("/saturnnodes/prompt_iterator/reset_node", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ node_id: String(node.id) })
|
||||
@@ -39,27 +40,27 @@ app.registerExtension({
|
||||
const res = await resp.json();
|
||||
if (res && res.status === "ok") {
|
||||
resetBtn.name = "✅ Reset to 0!";
|
||||
statusWidget.value = "🍃 Reset (0 / --)";
|
||||
statusWidget.value = "Reset (0 / --)";
|
||||
} else {
|
||||
resetBtn.name = "⚠️ Error";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[LeafFlow] 🍃 Error resetting counter:", e);
|
||||
console.error("[SaturnNodes] Error resetting counter:", e);
|
||||
resetBtn.name = "❌ Failed";
|
||||
}
|
||||
setTimeout(() => {
|
||||
resetBtn.name = "🔄 Reset Counter (0)";
|
||||
resetBtn.name = "Reset Counter (0)";
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
}, 1200);
|
||||
});
|
||||
resetBtn.serialize = false;
|
||||
|
||||
// 3. Open Queue File in Default OS Editor Button
|
||||
const openFileBtn = node.addWidget("button", "📂 Open Queue File", null, async () => {
|
||||
const openFileBtn = node.addWidget("button", "Open Queue File", null, async () => {
|
||||
try {
|
||||
openFileBtn.name = "⏳ Opening...";
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
const resp = await authenticatedFetch("/leafflow/prompt_iterator/open_file", { method: "POST" });
|
||||
const resp = await authenticatedFetch("/saturnnodes/prompt_iterator/open_file", { method: "POST" });
|
||||
const res = await resp.json();
|
||||
if (res && res.status === "ok") {
|
||||
openFileBtn.name = "✅ Opened in Editor!";
|
||||
@@ -67,11 +68,11 @@ app.registerExtension({
|
||||
openFileBtn.name = "⚠️ Error";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[LeafFlow] 🍃 Error opening state file:", e);
|
||||
console.error("[SaturnNodes] Error opening state file:", e);
|
||||
openFileBtn.name = "❌ Failed";
|
||||
}
|
||||
setTimeout(() => {
|
||||
openFileBtn.name = "📂 Open Queue File";
|
||||
openFileBtn.name = "Open Queue File";
|
||||
app.graph?.setDirtyCanvas(true, true);
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
// Vibrant Leaf Green palette matching the 🍃 emoji
|
||||
const LEAF_GREEN = { color: "#16a34a", bgcolor: "#14532d" };
|
||||
const LEAF_LIGHT = { color: "#22c55e", bgcolor: "#166534" };
|
||||
const LEAF_EMERALD = { color: "#059669", bgcolor: "#064e3b" };
|
||||
|
||||
const COLOR_MAP = {
|
||||
// Visual Loaders
|
||||
"VisualLoraLoader": LEAF_GREEN,
|
||||
"FolderLoraLoader": LEAF_GREEN,
|
||||
"FolderLoraLoaderPretty": LEAF_GREEN,
|
||||
"VisualImageLoader": LEAF_GREEN,
|
||||
"LoadRecentOutputs": LEAF_GREEN,
|
||||
|
||||
// Automation, Flow & Utilities
|
||||
"LoadImageFromFolder": LEAF_LIGHT,
|
||||
"TextAspectRatioFinder": LEAF_LIGHT,
|
||||
"PreviewImageSizeAspectRatio": LEAF_LIGHT,
|
||||
"TextLoraFinder": LEAF_LIGHT,
|
||||
"PromptQueueIterator": LEAF_LIGHT,
|
||||
"PromptCounter": LEAF_LIGHT,
|
||||
"MultiTextReplacer": LEAF_LIGHT,
|
||||
"LeafFlowTextSplit": LEAF_LIGHT,
|
||||
"LeafFlowDecision": LEAF_LIGHT,
|
||||
|
||||
// Queue & Previews
|
||||
"PreviewLatentLive": LEAF_EMERALD,
|
||||
"PauseQueueNode": LEAF_EMERALD,
|
||||
"PersistentQueueNode": LEAF_EMERALD
|
||||
};
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.Colors",
|
||||
async nodeCreated(node) {
|
||||
const enabled = app.ui.settings.getSettingValue(
|
||||
"LeafFlow.1. 🖼️ Visual Loaders.00_EnableCustomColors",
|
||||
true
|
||||
);
|
||||
if (!enabled) return;
|
||||
|
||||
if (node && node.type && COLOR_MAP[node.type]) {
|
||||
const theme = COLOR_MAP[node.type];
|
||||
node.color = theme.color;
|
||||
node.bgcolor = theme.bgcolor;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,674 +0,0 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
import { api } from "/scripts/api.js";
|
||||
import { authenticatedFetch } from "./js/auth_helper.js";
|
||||
|
||||
async function postLeafFlowSettings(bodyObj) {
|
||||
try {
|
||||
return await authenticatedFetch("/leafflow/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(bodyObj)
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("[LeafFlow Settings] Failed to save setting:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Inject CSS to ensure settings buttons look distinct, styled with an emerald theme, and never get text cut off
|
||||
if (typeof document !== "undefined") {
|
||||
const styleId = "leafflow-settings-button-styles";
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
/* ComfyUI V2 Vue & LiteGraph Settings Button Styling */
|
||||
button.leafflow-settings-btn,
|
||||
tr:has([id*="LeafFlow"]) button,
|
||||
tr:has([id*="leafflow"]) button,
|
||||
div:has(> [id*="LeafFlow"]) button,
|
||||
div:has(> [id*="leafflow"]) button,
|
||||
[data-setting-id*="LeafFlow"] button,
|
||||
div[class*="setting"]:has(span:contains("LeafFlow")) button {
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%) !important;
|
||||
border: 1px solid #10b981 !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12px !important;
|
||||
padding: 6px 14px !important;
|
||||
border-radius: 6px !important;
|
||||
width: auto !important;
|
||||
min-width: max-content !important;
|
||||
max-width: none !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
cursor: pointer !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
button.leafflow-settings-btn:hover,
|
||||
tr:has([id*="LeafFlow"]) button:hover,
|
||||
div:has(> [id*="LeafFlow"]) button:hover,
|
||||
[data-setting-id*="LeafFlow"] button:hover {
|
||||
background: linear-gradient(135deg, #047857 0%, #059669 100%) !important;
|
||||
border-color: #34d399 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.45) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
}
|
||||
|
||||
button.leafflow-settings-btn:active,
|
||||
tr:has([id*="LeafFlow"]) button:active {
|
||||
transform: translateY(0) !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// Global Non-Fatal Warning & Native Node Error Highlighter
|
||||
try {
|
||||
api.addEventListener("leafflow_node_error_state", (e) => {
|
||||
const { node_id, title, message, fallback } = e.detail || {};
|
||||
if (node_id && app.graph) {
|
||||
const targetNode = app.graph.getNodeById(Number(node_id)) || app.graph.getNodeById(String(node_id));
|
||||
if (targetNode) {
|
||||
targetNode.has_errors = true;
|
||||
targetNode.bgcolor = "#7f1d1d";
|
||||
targetNode.color = "#ef4444";
|
||||
targetNode.setDirtyCanvas(true, true);
|
||||
if (typeof targetNode.onExecutionError === "function") {
|
||||
targetNode.onExecutionError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: "warn",
|
||||
summary: `🍃 ${title || "LeafFlow"}`,
|
||||
detail: `${message} (Auto-resolved with ${fallback})`,
|
||||
life: 6000
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.addEventListener("leafflow_toast", (e) => {
|
||||
const data = e.detail || {};
|
||||
const title = data.title || "LeafFlow Notice";
|
||||
const message = data.message || "";
|
||||
const type = data.type || "warn";
|
||||
if (app.extensionManager?.toast?.add) {
|
||||
app.extensionManager.toast.add({
|
||||
severity: type === "error" ? "error" : "warn",
|
||||
summary: `🍃 ${title}`,
|
||||
detail: message,
|
||||
life: 6000
|
||||
});
|
||||
} else if (app.ui?.dialog) {
|
||||
console.warn(`[LeafFlow] ${title}: ${message}`);
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
|
||||
/**
|
||||
* Custom renderer for real, styled action buttons in ComfyUI Settings Modal.
|
||||
*/
|
||||
function renderSettingButton(label, workingText, successText, onClickHandler) {
|
||||
return (name, setter, value) => {
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText = "display: flex; align-items: center; justify-content: flex-end; width: 100%; padding: 2px 0;";
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.textContent = label;
|
||||
btn.className = "p-button leafflow-settings-btn";
|
||||
btn.style.cssText = `
|
||||
padding: 6px 14px !important;
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%) !important;
|
||||
color: #ffffff !important;
|
||||
border: 1px solid #10b981 !important;
|
||||
border-radius: 6px !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 12px !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
outline: none !important;
|
||||
min-width: max-content !important;
|
||||
width: auto !important;
|
||||
white-space: nowrap !important;
|
||||
text-align: center !important;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4) !important;
|
||||
`;
|
||||
|
||||
btn.onmouseover = () => {
|
||||
if (!btn.disabled) {
|
||||
btn.style.background = "#047857";
|
||||
btn.style.borderColor = "#34d399";
|
||||
btn.style.boxShadow = "0 0 10px rgba(16, 185, 129, 0.45)";
|
||||
}
|
||||
};
|
||||
btn.onmouseout = () => {
|
||||
if (!btn.disabled) {
|
||||
btn.style.background = "linear-gradient(135deg, #059669 0%, #047857 100%)";
|
||||
btn.style.borderColor = "#10b981";
|
||||
btn.style.boxShadow = "0 1px 3px rgba(0,0,0,0.4)";
|
||||
}
|
||||
};
|
||||
|
||||
btn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = "0.7";
|
||||
btn.textContent = workingText || "⏳ Working...";
|
||||
try {
|
||||
await onClickHandler();
|
||||
btn.textContent = successText || "✅ Done!";
|
||||
btn.style.background = "#1b5e20";
|
||||
btn.style.borderColor = "#2e7d32";
|
||||
btn.style.color = "#ffffff";
|
||||
} catch (err) {
|
||||
console.error("[LeafFlow] Button action failed:", err);
|
||||
btn.textContent = "❌ Error";
|
||||
btn.style.background = "#c62828";
|
||||
btn.style.borderColor = "#e53935";
|
||||
}
|
||||
setTimeout(() => {
|
||||
btn.textContent = label;
|
||||
btn.disabled = false;
|
||||
btn.style.opacity = "1";
|
||||
btn.style.background = "linear-gradient(135deg, #059669 0%, #047857 100%)";
|
||||
btn.style.borderColor = "#10b981";
|
||||
}, 1800);
|
||||
};
|
||||
|
||||
container.appendChild(btn);
|
||||
return container;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic Settings Migration:
|
||||
* Seamlessly copies over existing user settings from older LeafFlow setting ID formats
|
||||
* so users never lose their saved API keys, tokens, or preferences.
|
||||
*/
|
||||
function migrateSavedSettings() {
|
||||
try {
|
||||
if (typeof localStorage === "undefined") return;
|
||||
const keysToMigrate = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i);
|
||||
if (k && (k.includes("LeafFlow.") || k.includes("leafflow."))) {
|
||||
keysToMigrate.push(k);
|
||||
}
|
||||
}
|
||||
for (const oldKey of keysToMigrate) {
|
||||
const val = localStorage.getItem(oldKey);
|
||||
if (val === null) continue;
|
||||
// Convert dot-numbered formats like "LeafFlow.1. 🖼️..." -> "LeafFlow.1 - 🖼️..."
|
||||
const newKey = oldKey.replace(/LeafFlow\.(\d+)\.\s+/g, "LeafFlow.$1 - ");
|
||||
if (newKey !== oldKey && localStorage.getItem(newKey) === null) {
|
||||
localStorage.setItem(newKey, val);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[LeafFlow] Settings migration skipped:", e);
|
||||
}
|
||||
}
|
||||
migrateSavedSettings();
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.Settings",
|
||||
async setup() {
|
||||
migrateSavedSettings();
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 1: 1 - 🖼️ Visual Loaders
|
||||
// =========================================================================
|
||||
|
||||
// 1.0 Custom Node Colors Toggle
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.00_EnableCustomColors",
|
||||
name: "Enable Custom LeafFlow Node Colors",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Applies a fresh Leaf Green color theme to LeafFlow nodes on the canvas. When disabled, nodes use default ComfyUI colors.",
|
||||
});
|
||||
|
||||
// 1.1 Civitai API Key
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.01_CivitaiApiKey",
|
||||
name: "Civitai API Key",
|
||||
type: "text",
|
||||
defaultValue: "",
|
||||
tooltip: "Optional. Civitai SHA256 search works publicly without a key for normal models. Only needed for NSFW/private models or higher rate limits. Whitespace is automatically stripped.",
|
||||
onChange(value) {
|
||||
const cleanKey = (value || "").trim();
|
||||
postLeafFlowSettings({ civitai_api_key: cleanKey });
|
||||
}
|
||||
});
|
||||
|
||||
// 1.2 Enable Civitai Auto-Scraping Toggle (default: true)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.02_EnableCivitaiScraping",
|
||||
name: "Enable Civitai Auto-Scraping",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Toggles automated downloading of preview thumbnails for new LoRAs from Civitai via SHA256 file hashes. Note: SHA256 hash searching will always work regardless of this setting when matching local models.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_civitai_scraping: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 1.3 TMDB Access Token
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.03_TMDBApiKey",
|
||||
name: "TMDB Access Token",
|
||||
type: "text",
|
||||
defaultValue: "",
|
||||
tooltip: "Optional. Accepts TMDB v3 API keys or TMDB v4 Read Access Tokens (eyJ...). Used for celebrity poster and preview image lookup. Whitespace is automatically stripped.",
|
||||
onChange(value) {
|
||||
const cleanKey = (value || "").trim();
|
||||
postLeafFlowSettings({ tmdb_api_key: cleanKey });
|
||||
}
|
||||
});
|
||||
|
||||
// 1.4 Enable TMDB Auto-Scraping Toggle (default: false)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.04_EnableTMDBScraping",
|
||||
name: "Enable TMDB Auto-Scraping",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
tooltip: "Toggles automated downloading of celebrity preview thumbnails from TMDB. Default is disabled.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_tmdb_scraping: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 1.5 Enable LoRA Usage Tracking (default: true)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.05_EnableLoraUsage",
|
||||
name: "Enable LoRA Usage Tracking",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Toggles tracking and displaying LoRA usage counts & visual rank badges (🔥, Gold, Silver, Bronze) in the LoRA picker. Existing usage history is preserved when disabled.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_lora_usage: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 1.6 Reset Scrapes Cache Button
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.1 - 🖼️ Visual Loaders.06_ResetScrapesCache",
|
||||
name: "Reset Failed Scrapes Cache",
|
||||
type: "button",
|
||||
defaultValue: "🗑️ Clear Scrapes Cache",
|
||||
tooltip: "Immediately clears failed_scrapes.json so Civitai and TMDB can retry downloading missing preview thumbnails on the next folder scan.",
|
||||
attrs: {
|
||||
className: "leafflow-settings-btn",
|
||||
class: "leafflow-settings-btn",
|
||||
onClick: async () => {
|
||||
try {
|
||||
const resp = await authenticatedFetch("/leafflow/scrapes/clear", { method: "POST" });
|
||||
const data = await resp.json();
|
||||
if (data && data.status === "ok") {
|
||||
alert("LeafFlow: Failed scrapes cache successfully reset!");
|
||||
} else {
|
||||
alert("LeafFlow: Failed to reset cache.");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("LeafFlow: Error resetting cache: " + err);
|
||||
}
|
||||
}
|
||||
},
|
||||
render: renderSettingButton("🗑️ Clear Scrapes Cache", "⏳ Clearing...", "✅ Cache Reset!", async () => {
|
||||
const resp = await authenticatedFetch("/leafflow/scrapes/clear", { method: "POST" });
|
||||
const data = await resp.json();
|
||||
if (data.status !== "ok") {
|
||||
throw new Error(data.message || "Failed to reset scrapes cache");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 2: 2 - 🔄 Prompt Queue Iterator
|
||||
// =========================================================================
|
||||
|
||||
// 2.1 Clear Prompt Iterator State on Launch (default: false)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.2 - 🔄 Prompt Iterator.01_ClearOnLaunch",
|
||||
name: "Clear State on Launch",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
tooltip: "Privacy setting. When enabled, prompt_iterator_state.json will be emptied automatically every time ComfyUI starts up.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ clear_prompt_iterator_on_launch: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 2.2 Reset Prompt Iterator Queues Now
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.2 - 🔄 Prompt Iterator.02_ResetActiveQueues",
|
||||
name: "Reset Active Queues State",
|
||||
type: "button",
|
||||
defaultValue: "🔄 Reset All Queues",
|
||||
tooltip: "Immediately empties all active prompt queues and resets iterator state across all workflows.",
|
||||
attrs: {
|
||||
className: "leafflow-settings-btn",
|
||||
class: "leafflow-settings-btn",
|
||||
onClick: async () => {
|
||||
try {
|
||||
const resp = await authenticatedFetch("/leafflow/prompt_iterator/clear", { method: "POST" });
|
||||
const data = await resp.json();
|
||||
if (data && data.status === "ok") {
|
||||
alert("LeafFlow: Prompt Iterator queues successfully reset!");
|
||||
} else {
|
||||
alert("LeafFlow: Failed to reset queues.");
|
||||
}
|
||||
} catch (err) {
|
||||
alert("LeafFlow: Error resetting queues: " + err);
|
||||
}
|
||||
}
|
||||
},
|
||||
render: renderSettingButton("🔄 Reset All Queues", "⏳ Resetting...", "✅ State Reset!", async () => {
|
||||
const resp = await authenticatedFetch("/leafflow/prompt_iterator/clear", { method: "POST" });
|
||||
const data = await resp.json();
|
||||
if (data.status !== "ok") {
|
||||
throw new Error(data.message || "Failed to clear prompt iterator state");
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 3: 3 - 📋 Prompt Actions
|
||||
// =========================================================================
|
||||
|
||||
// 3.1 Show "Copy Prompt" Button on Image Overlays
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.3 - 📋 Prompt Actions.01_EnableAssetsCopyPromptButton",
|
||||
name: "Show \"Copy Prompt\" Button on Images",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Shows the 📋 'Copy Prompt' overlay action button when hovering over generated images in the Assets / History pane and preview nodes.",
|
||||
});
|
||||
|
||||
// 3.2 Show Right-Click "Copy Prompt" Menu Action
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.3 - 📋 Prompt Actions.02_EnableContextMenuCopyPrompt",
|
||||
name: "Show Right-Click \"Copy Prompt\" Menu Action",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Adds '📋 Copy Prompt' to node right-click context menus.",
|
||||
});
|
||||
|
||||
// 3.3 Show "Save to Prompt Bookmarks" Button on Images & Context Menu
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.3 - 📋 Prompt Actions.03_EnableSaveToPromptSaver",
|
||||
name: "Show \"Save to Prompt Saver\" Action",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Adds 🔖 'Save to Prompt Bookmarks' to image hover overlay bars and node context menus.",
|
||||
});
|
||||
|
||||
// 3.4 Show "Inspect Asset" (Zoom) Button on Image Overlays
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.3 - 📋 Prompt Actions.04_EnableInspectAssetButton",
|
||||
name: "Show \"Inspect Asset\" (Zoom) Button on Images",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
tooltip: "Restores the 🔍 'Inspect asset' (zoom in) button directly on image cards in the Assets pane next to Download, Copy Prompt, and Bookmarks (moved behind the 3-dots menu in newer ComfyUI versions).",
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 4: 4 - ⏸️ Pause & Resume Controls
|
||||
// =========================================================================
|
||||
|
||||
// 4.1 Default Pause Queue State on Launch
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.01_DefaultStateOnLaunch",
|
||||
name: "Default State on Launch",
|
||||
type: "combo",
|
||||
options: ["Paused", "Running"],
|
||||
defaultValue: "Paused",
|
||||
tooltip: "Choose whether execution starts in Paused state or Running state on ComfyUI startup.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ default_pause_state: value });
|
||||
}
|
||||
});
|
||||
|
||||
// 4.2 Default Pause Queue Mode on Launch
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.02_DefaultPauseAction",
|
||||
name: "Default Pause Action",
|
||||
type: "combo",
|
||||
options: ["Finish Active Prompt", "Instant Resume Node"],
|
||||
defaultValue: "Finish Active Prompt",
|
||||
tooltip: "Choose default pause behavior when the pause button or hotkey is triggered.",
|
||||
onChange(value) {
|
||||
const modeKey = (value === "Instant Resume Node" || value === "Pause (Instant)") ? "instantly" : "after_finish";
|
||||
postLeafFlowSettings({ default_pause_mode: modeKey });
|
||||
}
|
||||
});
|
||||
|
||||
// 4.3 Enable Pause Queue Toolbar Button
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.03_EnableToolbarButton",
|
||||
name: "Enable Top Toolbar Button",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Displays the green/orange Pause & Continue button group in the top action bar.",
|
||||
onChange(value) {
|
||||
const group = document.querySelector(".pq-button-group");
|
||||
if (group) {
|
||||
group.style.display = value ? "inline-flex" : "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4.4 Toolbar Button Unpaused Color
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.04_ToolbarButtonUnpausedColor",
|
||||
name: "Toolbar Button Unpaused Color",
|
||||
type: "text",
|
||||
defaultValue: "#16a34a",
|
||||
tooltip: "Hex color code for the Pause toolbar button when execution is unpaused/running (default: #16a34a).",
|
||||
onChange(value) {
|
||||
document.documentElement.style.setProperty("--pq-unpaused-color", value || "#16a34a");
|
||||
}
|
||||
});
|
||||
|
||||
// 4.5 Toolbar Button Paused Color
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.05_ToolbarButtonPausedColor",
|
||||
name: "Toolbar Button Paused Color",
|
||||
type: "text",
|
||||
defaultValue: "#ea580c",
|
||||
tooltip: "Hex color code for the Pause toolbar button when execution is paused (default: #ea580c).",
|
||||
onChange(value) {
|
||||
document.documentElement.style.setProperty("--pq-paused-color", value || "#ea580c");
|
||||
}
|
||||
});
|
||||
|
||||
// 4.6 Enable System Tray Icon
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.06_EnableTrayIcon",
|
||||
name: "Enable System Tray Icon",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
tooltip: "Displays an OS system tray icon with real-time queue status colors and outside-browser queue controls.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_tray_icon: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 4.7 Allow Process Management (Restart / Shutdown)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.4 - ⏸️ Pause Controls.07_AllowProcessManagement",
|
||||
name: "Allow Process Management (Restart / Shutdown)",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
tooltip: "Enables server restart and shutdown actions from the LeafFlow power controls. Disabled by default for security.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_process_management: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 5: 5 - 💾 Persistent Queue (Auto-Recovery)
|
||||
// =========================================================================
|
||||
|
||||
// 5.1 Enable Persistent Queue (Auto-Recovery)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.5 - 💾 Persistent Queue.01_EnablePersistentQueue",
|
||||
name: "Enable Persistent Queue (Auto-Recovery)",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Automatically persists unfinished batch queue items to disk and restores them after server or browser crashes.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_persistent_queue: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 5.2 Persistent Queue Restored Launch State
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.5 - 💾 Persistent Queue.02_RecoveryLaunchState",
|
||||
name: "Recovery Launch State",
|
||||
type: "combo",
|
||||
options: ["Match Default", "Force Paused", "Force Running"],
|
||||
defaultValue: "Match Default",
|
||||
tooltip: "Override launch state when unfinished queue items are recovered on startup.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ persistent_queue_restored_state: value });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 6: 6 - 🖼️ Assets & History Restore
|
||||
// =========================================================================
|
||||
|
||||
// 6.1 Enable Assets / History Restore on Launch
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.6 - 🖼️ Assets Restore.01_RestoreAssetsOnLaunch",
|
||||
name: "Restore Assets on Launch",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Automatically populates the Assets / History pane upon ComfyUI launch with your latest generated images.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_assets_restore: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
|
||||
// 6.2 Restored Assets Count
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.6 - 🖼️ Assets Restore.02_RestoredAssetsCount",
|
||||
name: "Restored Assets Count",
|
||||
type: "number",
|
||||
defaultValue: 64,
|
||||
tooltip: "Number of newest images from the output folder to restore into the Assets / History pane on launch (default: 64).",
|
||||
onChange(value) {
|
||||
const count = parseInt(value, 10) || 64;
|
||||
postLeafFlowSettings({ restore_assets_count: count });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 7: 7 - 🩺 Diagnostics & Debug
|
||||
// =========================================================================
|
||||
|
||||
// 7.1 Export Debug Profile Button
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.7 - 🩺 Diagnostics.01_ExportDebugProfile",
|
||||
name: "Export Debug Profile",
|
||||
type: "button",
|
||||
defaultValue: "📥 Export Debug Profile",
|
||||
tooltip: "Exports non-sensitive environment diagnostics (OS, Python, PyTorch, LeafFlow settings, local counts) as a JSON file to share when troubleshooting issues.",
|
||||
attrs: {
|
||||
className: "leafflow-settings-btn",
|
||||
class: "leafflow-settings-btn",
|
||||
onClick: async () => {
|
||||
const approved = confirm("🍃 ComfyUI-LeafFlow Diagnostics Export\n\nExport system diagnostics for troubleshooting?\n\nNOTE: Sensitive API keys, tokens, file paths, and private prompt texts are automatically stripped and NEVER exported.");
|
||||
if (!approved) return;
|
||||
|
||||
try {
|
||||
const resp = await api.fetchApi("/leafflow/debug/export");
|
||||
const data = await resp.json();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `leafflow_debug_profile_${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
alert("✅ Diagnostics profile exported successfully! You can attach the downloaded JSON file to your bug report or GitHub issue.");
|
||||
} catch (e) {
|
||||
console.error("[LeafFlow] 🍃 Error exporting debug profile:", e);
|
||||
alert("❌ Failed to export debug profile: " + e.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
render: renderSettingButton("📥 Export Debug Profile", "⏳ Exporting...", "✅ Exported!", async () => {
|
||||
const resp = await api.fetchApi("/leafflow/debug/export");
|
||||
const data = await resp.json();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `leafflow_debug_profile_${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
})
|
||||
});
|
||||
// =========================================================================
|
||||
// GRUPPE 8: 8 - 🎨 Batch Queue Visuals
|
||||
// =========================================================================
|
||||
|
||||
// 8.1 Enable Batch Queue Grouping Lines
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.BatchQueue.Enabled",
|
||||
name: "Show Batch Queue 1D Lines",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "Renders 1D git-graph style colored lines indicating batch groupings and contiguous segments on queued items."
|
||||
});
|
||||
|
||||
// 8.2 Batch Graph Snapshot Guard
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.BatchQueue.SnapshotGuard",
|
||||
name: "Batch Graph Snapshot Guard",
|
||||
type: "boolean",
|
||||
defaultValue: true,
|
||||
tooltip: "When queueing multi-item batches, snapshots prompt and node inputs (like text and LoRAs) in the background so mid-queue canvas edits do not corrupt queued items. Random seeds continue to randomize."
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// GRUPPE 9: 9 - 🛡️ Security & Script Execution
|
||||
// =========================================================================
|
||||
|
||||
// 9.1 Allow Local File Execution (Default Disabled)
|
||||
app.ui.settings.addSetting({
|
||||
id: "LeafFlow.9 - 🛡️ Security.01_AllowLocalFileExecution",
|
||||
name: "Allow Local File Execution",
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
tooltip: "Controls whether the '🍃 ⚡ Run Local File' node is permitted to run executable files (.bat, .ps1, .exe, .sh). Disabled by default for operator security. When enabled, scripts are strictly confined to the 'ComfyUI/scripts/' directory. Execution is blocked if this setting is disabled or if the node is not interactively authorized.",
|
||||
onChange(value) {
|
||||
postLeafFlowSettings({ enable_local_file_execution: value ? "true" : "false" });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -320,7 +320,7 @@ class PauseQueueUI {
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.PauseQueue",
|
||||
name: "ComfyUI.SaturnNodes.PauseQueue",
|
||||
async setup() {
|
||||
new PauseQueueUI();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { api } from "/scripts/api.js";
|
||||
import { authenticatedFetch } from "./js/auth_helper.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.LeafFlow.PersistentQueue",
|
||||
name: "ComfyUI.SaturnNodes.PersistentQueue",
|
||||
async setup() {
|
||||
const patchRgthree = async () => {
|
||||
try {
|
||||
@@ -36,7 +36,7 @@ app.registerExtension({
|
||||
}
|
||||
if (patched) {
|
||||
rgthree.SERVICE.dispatchProgressUpdate();
|
||||
console.log("[LeafFlow] Synced recovered queue with rgthree-comfy progress bar.");
|
||||
console.log("[SaturnNodes] Synced recovered queue with rgthree-comfy progress bar.");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
// Default Amber theme matching the Saturn theme (warm, deep amber, not blinding neon and not muddy brown)
|
||||
export const DEFAULT_HEADER_COLOR = "#6a3f20";
|
||||
export const DEFAULT_BG_COLOR = "#472a15";
|
||||
|
||||
export const SATURN_NODE_TYPES = new Set([
|
||||
// Visual Loaders
|
||||
"VisualLoraLoader",
|
||||
"FolderLoraLoader",
|
||||
"FolderLoraLoaderPretty",
|
||||
"FolderLoraLoaderVisualPrettyV2",
|
||||
"VisualImageLoader",
|
||||
"ImageLoaderVisualPrettyV2",
|
||||
"LoadRecentOutputs",
|
||||
|
||||
// Automation, Flow & Utilities
|
||||
"LoadImageFromFolder",
|
||||
"TextAspectRatioFinder",
|
||||
"AspectRatioFinder",
|
||||
"PreviewImageSizeAspectRatio",
|
||||
"TextLoraFinder",
|
||||
"LoraTextFinder",
|
||||
"PromptQueueIterator",
|
||||
"PromptCounter",
|
||||
"MultiTextReplacer",
|
||||
"SaturnTextSplit",
|
||||
"SaturnDecision",
|
||||
"RunLocalFileNode",
|
||||
|
||||
// Queue & Previews
|
||||
"PreviewLatentLive",
|
||||
"PauseQueueNode",
|
||||
"PersistentQueueNode"
|
||||
]);
|
||||
|
||||
export function getSaturnTheme() {
|
||||
let headerColor = DEFAULT_HEADER_COLOR;
|
||||
let bgColor = DEFAULT_BG_COLOR;
|
||||
try {
|
||||
const getSetting = (plainId, numId) => {
|
||||
if (app.ui?.settings?.get) {
|
||||
const val = app.ui.settings.get(plainId) || app.ui.settings.get(numId);
|
||||
if (val) return val;
|
||||
}
|
||||
if (typeof localStorage !== "undefined") {
|
||||
for (const k of [plainId, numId]) {
|
||||
for (const prefix of ["Comfy.Settings.", ""]) {
|
||||
const raw = localStorage.getItem(`${prefix}${k}`);
|
||||
if (raw !== null && raw !== undefined) {
|
||||
try { return JSON.parse(raw); } catch (_) { return raw; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const hc = getSetting("SaturnNodes.🖼️ Visual Loaders.00_CustomNodeHeaderColor", "SaturnNodes.1 - 🖼️ Visual Loaders.00_CustomNodeHeaderColor");
|
||||
if (hc) headerColor = hc;
|
||||
const bg = getSetting("SaturnNodes.🖼️ Visual Loaders.00_CustomNodeBgColor", "SaturnNodes.1 - 🖼️ Visual Loaders.00_CustomNodeBgColor");
|
||||
if (bg) bgColor = bg;
|
||||
} catch (_) {}
|
||||
return { color: headerColor, bgcolor: bgColor };
|
||||
}
|
||||
|
||||
export function isSaturnColorsEnabled() {
|
||||
try {
|
||||
const checkVal = (k) => {
|
||||
if (app.ui?.settings?.get) {
|
||||
const val = app.ui.settings.get(k);
|
||||
if (val !== undefined && val !== null) return Boolean(val);
|
||||
}
|
||||
if (app.ui?.settings?.getSettingValue) {
|
||||
const val = app.ui.settings.getSettingValue(k);
|
||||
if (val !== undefined && val !== null) return Boolean(val);
|
||||
}
|
||||
if (typeof localStorage !== "undefined") {
|
||||
for (const prefix of ["Comfy.Settings.", ""]) {
|
||||
const raw = localStorage.getItem(`${prefix}${k}`);
|
||||
if (raw !== null && raw !== undefined) {
|
||||
try { return JSON.parse(raw); } catch (_) { return Boolean(raw); }
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
for (const k of [
|
||||
"SaturnNodes.🖼️ Visual Loaders.00_EnableCustomColors",
|
||||
"SaturnNodes.1 - 🖼️ Visual Loaders.00_EnableCustomColors"
|
||||
]) {
|
||||
const res = checkVal(k);
|
||||
if (res !== null) return res;
|
||||
}
|
||||
} catch (_) {}
|
||||
return false; // Default is disabled (false)
|
||||
}
|
||||
|
||||
export function applySaturnColorToNode(node) {
|
||||
if (!node) return;
|
||||
const type = node.comfyClass || node.type || node.constructor?.comfyClass || node.constructor?.type;
|
||||
if (!SATURN_NODE_TYPES.has(type)) return;
|
||||
|
||||
if (isSaturnColorsEnabled()) {
|
||||
const theme = getSaturnTheme();
|
||||
node.color = theme.color;
|
||||
node.bgcolor = theme.bgcolor;
|
||||
} else {
|
||||
node.color = undefined;
|
||||
node.bgcolor = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAllSaturnNodeColors() {
|
||||
if (!app.graph?._nodes) return;
|
||||
for (const node of app.graph._nodes) {
|
||||
applySaturnColorToNode(node);
|
||||
if (typeof node._updateCounterBadgeColor === "function") {
|
||||
node._updateCounterBadgeColor();
|
||||
}
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "ComfyUI.SaturnNodes.Colors",
|
||||
beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData && nodeData.name && SATURN_NODE_TYPES.has(nodeData.name)) {
|
||||
if (isSaturnColorsEnabled()) {
|
||||
const theme = getSaturnTheme();
|
||||
nodeType.color = theme.color;
|
||||
nodeType.bgcolor = theme.bgcolor;
|
||||
nodeType.prototype.color = theme.color;
|
||||
nodeType.prototype.bgcolor = theme.bgcolor;
|
||||
} else {
|
||||
nodeType.color = undefined;
|
||||
nodeType.bgcolor = undefined;
|
||||
nodeType.prototype.color = undefined;
|
||||
nodeType.prototype.bgcolor = undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
async nodeCreated(node) {
|
||||
applySaturnColorToNode(node);
|
||||
},
|
||||
async loadedGraphNode(node) {
|
||||
applySaturnColorToNode(node);
|
||||
},
|
||||
afterConfigureGraph() {
|
||||
updateAllSaturnNodeColors();
|
||||
},
|
||||
async setup() {
|
||||
setTimeout(() => updateAllSaturnNodeColors(), 100);
|
||||
setTimeout(() => updateAllSaturnNodeColors(), 500);
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user