Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40955f2822 | ||
|
|
de6da654ca |
@@ -1,5 +1,184 @@
|
||||
# DEVLOG - ComfyUI-Workflow-Studio
|
||||
|
||||
## 2026-04-06: v0.2.6 UI改善・バッジ保存修正・フォルダフィルター追加
|
||||
|
||||
### 概要
|
||||
- ワークフロー/モデル/ノードタブのサイドパネルを常時表示固定に変更
|
||||
- グリッドをページネーションからスクロール全件表示に変更
|
||||
- ワークフロータブのバッジが再起動後に消える問題を修正
|
||||
- バッジフィルターボタンをパレット定義バッジで統一(ワークフロータブ)
|
||||
- モデルタブにフォルダ(サブディレクトリ)フィルターを追加
|
||||
- モデルカードビューから拡張子・フォルダバッジを削除(詳細パネルで確認可能)
|
||||
- サムネイルカードサイズを 178×200px に調整、グリッドレイアウトを修正
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `py/services/workflow_service.py` — バッジ保存修正
|
||||
|
||||
**`save_metadata` のキーリストに `badges` を追加:**
|
||||
- 変更前: `("tags", "memo", "summary", "modelTypesOverride", "favorite")`
|
||||
- 変更後: `("tags", "memo", "summary", "modelTypesOverride", "favorite", "badges")`
|
||||
|
||||
**`list_workflows` のレスポンスに `badges` フィールドを追加:**
|
||||
- `metadata` dict に `"badges": meta.get("badges", [])` を追加
|
||||
- これにより再起動後もバッジが保持される
|
||||
|
||||
#### `static/js/workflow-tab.js` — バッジフィルター・サイドパネル・スクロール
|
||||
|
||||
**バッジフィルターをパレット定義バッジで表示:**
|
||||
- `renderModelFilters()`: `getAllBadges()`(ワークフローに付いているバッジのみ)→ `getBadgePalette()`(パレット全バッジ)に変更
|
||||
- フィルターボタンにバッジカラーを適用
|
||||
|
||||
**サイドパネル常時表示化:**
|
||||
- `showSidePanel()`: `panel.style.display = "flex"` を削除
|
||||
- `closeSidePanel()`: `panel.style.display = "none"` を削除、タイトルをクリアするのみ
|
||||
- ×ボタンのイベントリスナー削除
|
||||
|
||||
**ページネーション廃止・スクロール全件表示:**
|
||||
- `WF_PER_PAGE` 定数削除
|
||||
- `updateWfPagination()` 関数削除
|
||||
- `renderGrid()`: ページスライス処理を削除、全件描画に変更
|
||||
|
||||
#### `static/js/models-tab.js` — サイドパネル・スクロール・フォルダフィルター・カードビュー
|
||||
|
||||
**サイドパネル常時表示化:**
|
||||
- `showSidePanel()`: `panel.style.display = "flex"` を削除
|
||||
- `closeSidePanel()`: `panel.style.display = "none"` を削除
|
||||
|
||||
**ページネーション廃止・スクロール全件表示:**
|
||||
- `MODELS_PER_PAGE` 定数削除、`updatePagination()` 関数削除
|
||||
- `renderThumbView()` / `renderCardView()` / `renderTableView()`: `totalPages` 引数削除、全件描画に変更
|
||||
|
||||
**フォルダフィルター追加:**
|
||||
- `state.dirFilter: ""` 追加
|
||||
- `filterModels()` に `dirFilter` による絞り込みを追加
|
||||
- `renderDirFilter()` 関数追加(現在のモデルタイプのサブディレクトリ一覧を `<select>` に反映)
|
||||
- モデルロード時・モデルタイプ切替時に呼び出し
|
||||
- `wfm-models-dir-filter` change イベントリスナー追加
|
||||
|
||||
**モデルカードビューからバッジ削除:**
|
||||
- `renderCardView()`: `dir` バッジ・`ext` バッジを削除。ユーザーバッジ・タグのみ表示
|
||||
|
||||
#### `static/js/nodes-tab.js` — サイドパネル・スクロール
|
||||
|
||||
**サイドパネル常時表示化:**
|
||||
- `showNodeSidePanel()`: `panel.style.display = "flex"` を削除
|
||||
- `closeNodeSidePanel()`: `panel.style.display = "none"` を削除
|
||||
|
||||
**ページネーション廃止・スクロール全件表示:**
|
||||
- `NODES_PER_PAGE` / `renderPagination()` / `clearPagination()` / `scrollGridToTop()` 削除
|
||||
- `renderNodeGrid()`: 全件描画に変更
|
||||
|
||||
#### `templates/index.html` — サイドパネル・ページネーション・フォルダフィルター
|
||||
|
||||
- 3タブ(ワークフロー/ノード/モデル)のサイドパネルから `style="display:none"` と×ボタンを削除
|
||||
- ページネーション用 `<div>` を3箇所から削除
|
||||
- モデルタブのツールバーに `<select id="wfm-models-dir-filter">` を追加(TagとGroupの間)
|
||||
|
||||
#### `static/css/main.css` — グリッド・カードサイズ・サイドパネル
|
||||
|
||||
**グリッド・カードサイズ変更:**
|
||||
- グリッド列幅: `161px` → `178px`
|
||||
- カード幅: `161px` → `178px`
|
||||
- サムネイル: `161×162px` → `178×200px`
|
||||
- `grid-auto-rows: max-content` / `align-items: start` / `justify-items: start` 追加(行高さ圧縮防止)
|
||||
|
||||
**カードビュー:**
|
||||
- グリッド列幅: `minmax(180px, 1fr)` → `repeat(auto-fill, 220px)` 固定幅に変更(横伸び防止)
|
||||
- カード: `flex-direction: row` / `min-height: 56px` / `width: 220px` 固定
|
||||
|
||||
**サイドパネル:**
|
||||
- `.wfm-side-panel-close` スタイル削除
|
||||
- `.wfm-side-panel-empty` クラス追加(選択前の空状態表示用)
|
||||
|
||||
#### `static/js/i18n.js` — フォルダフィルター翻訳追加
|
||||
|
||||
- `modelsAllDirs`: 英語 `"All Folders"` / 日本語 `"すべてのフォルダ"` / 中国語 `"所有文件夹"` を追加
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-04: v0.2.5 追加修正
|
||||
|
||||
### 概要
|
||||
- サイドパネルModels→By TypeでTextEncoderが表示されない問題を修正
|
||||
- サイドパネルのWorkflowsタブでも `.index.json` を非表示に
|
||||
- 未使用の `buildDropdown` 関数を削除
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `web/comfyui/node_sets_menu.js`
|
||||
|
||||
**TextEncoder取得ロジック修正:**
|
||||
- 変更前: `/api/wfm/models?type=textencoder`(存在しないエンドポイント)
|
||||
- 変更後: `DualCLIPLoader` → `CLIPLoader` の順に `/object_info/{cls}` を試して `clip_name1` を取得(`comfyui-client.js` の `fetchTextEncoders()` と同じロジック)
|
||||
|
||||
**`.index.json` 非表示:**
|
||||
- `loadWfData()`: `state.wfList = workflows.filter(w => w.filename !== ".index.json")` でサイドパネルのWorkflowsタブからも除外
|
||||
|
||||
**未使用関数削除:**
|
||||
- `buildDropdown()` 関数を削除(Category/Packageドロップダウンは個別実装に置き換え済みだったため不要)
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-03: v0.2.5 サイドパネル Category/Package サブタブ・テーマ設定・Workflow修正
|
||||
|
||||
### 概要
|
||||
- サイドパネル(Workflow Studio Library)のNodesタブに📂 Category / 🧩 Packageサブタブを追加
|
||||
- サイドパネルのヘッダーに⚙テーマ設定ボタンを追加(背景・文字・ボーダー色カスタマイズ)
|
||||
- ワークフロータブで `.index.json` を非表示に
|
||||
|
||||
### 変更内容
|
||||
|
||||
#### `web/comfyui/node_sets_menu.js` — Nodesサブタブ追加・テーマ設定
|
||||
|
||||
**Nodesタブのrow2サブタブ構成変更:**
|
||||
- 変更前: row2 = `☰ Sets`
|
||||
- 変更後: row2 = `☰ Sets` / `📂 Category` / `🧩 Package`
|
||||
|
||||
**state追加:**
|
||||
- `activeNodeCategory: ""` — Categoryサブタブで選択中のカテゴリ値
|
||||
- `activeNodePackage: ""` — Packageサブタブで選択中のパッケージ値
|
||||
- `objectInfo: {}` — `/object_info` APIから取得した全ノード情報(パッケージ判定に使用)
|
||||
|
||||
**`loadData()` 変更:**
|
||||
- `fetchObjectInfo()` を追加して `/object_info` を並行取得
|
||||
- `state.objectInfo` に保存
|
||||
|
||||
**新規関数:**
|
||||
- `fetchObjectInfo()` — `GET /object_info` で全ノード情報を取得
|
||||
- `extractPackageName(pythonModule)` — `python_module` からパッケージ名を抽出
|
||||
- `getNodeCategory(nodeType)` — `state.objectInfo` 優先でカテゴリを取得(LiteGraphフォールバック)
|
||||
- `getNodePackage(nodeType)` — `state.objectInfo[nodeType].python_module` からパッケージ名を取得
|
||||
- `renderNodesByCategory(container)` — カテゴリドロップダウンをcontent上部に挿入してリスト表示
|
||||
- `renderNodesByCategoryList(container)` — 選択カテゴリ+検索テキストでフィルタしてカード表示
|
||||
- `renderNodesByPackage(container)` — パッケージドロップダウンをcontent上部に挿入してリスト表示
|
||||
- `renderNodesByPackageList(container)` — 選択パッケージ+検索テキストでフィルタしてカード表示
|
||||
|
||||
**ドロップダウンのDOM挿入方式:**
|
||||
- `.wfm-nlp-filter-row` クラスのdivを `.wfm-nlp-content` の直前に `insertBefore` で挿入
|
||||
- `renderContent()` 冒頭で `panelEl.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove())` により他タブ移動時に削除
|
||||
|
||||
**テーマ設定:**
|
||||
- `THEME_KEY = "wfm_nlp_theme"` — localStorage保存キー
|
||||
- `THEME_VARS` — 設定可能な5変数: `--comfy-menu-bg`(背景)、`--comfy-input-bg`(サブヘッダーBG)、`--input-text`(文字色)、`--border-color`(ボーダー)、`--descrip-text`(補助テキスト色)
|
||||
- `loadTheme()` — localStorageから読み込み
|
||||
- `applyTheme(panel, theme)` — パネル要素にCSS変数をインラインsetPropertyで上書き
|
||||
- `buildThemePanel(panel)` — カラーピッカーUIを動的生成(ライブプレビュー・Save・Resetボタン付き)
|
||||
- パネルのHTMLに `.wfm-nlp-theme-btn`(⚙)と `.wfm-nlp-theme-panel` を追加
|
||||
- `createPanel()` 内でテーマボタンのイベントリスナーを登録、`applyTheme(panel, loadTheme())` で起動時に保存済みテーマを適用
|
||||
|
||||
#### `static/js/workflow-tab.js` — `.index.json` 非表示
|
||||
|
||||
- `filterWorkflows()` の先頭に `if (wf.filename === ".index.json") return false;` を追加
|
||||
|
||||
### 技術的な判断
|
||||
|
||||
- **`python_module` の取得元をAPIに変更:** `LiteGraph.registered_node_types` にはバックエンド由来の `python_module` が存在しないため、全ノードが `ComfyUI (Built-in)` 判定になる問題があった。`/object_info` APIを `loadData()` 時に並行取得することで正確なパッケージ判定を実現
|
||||
- **CSS変数のインライン上書き:** テーマ色はパネル要素自体に `style.setProperty` で適用。ComfyUI本体の `:root` 変数を書き換えず、パネルスコープ内のみに影響させる設計
|
||||
- **ドロップダウンのDOMインジェクション:** Category/Packageビューのドロップダウンは `renderContent` の都度生成・削除する方式を採用。タブ切替時のクリーンアップを `renderContent` 冒頭の `.wfm-nlp-filter-row` 削除処理で一元管理
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-29: v0.2.4 GenerateUIタブ レイアウト再設計
|
||||
|
||||
### 概要
|
||||
|
||||
@@ -5,7 +5,7 @@ A comprehensive workflow management and generation UI plugin for [ComfyUI](https
|
||||
Browse, organize, and execute workflows directly from a dedicated studio interface — without switching between windows or manually editing JSON.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -104,9 +104,11 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Table view memo** — memo column displayed in table view for quick reference
|
||||
- **Preview images** — auto-detect `{model_stem}.preview.png` next to model files
|
||||
|
||||
### Workflow Studio Library (ComfyUI Side Panel) (v0.2.3)
|
||||
### Workflow Studio Library (ComfyUI Side Panel) (v0.2.5)
|
||||
- **Workflows tab** — browse favorite workflows (All / ★ Favorites / Groups / By Badge sub-tabs), ★ star shown for favorites in All view
|
||||
- **Nodes tab** — browse favorite nodes (All / ★ Favorites / Groups / Sets sub-tabs), ★ star shown for favorites in All view
|
||||
- **Nodes tab** — browse favorite nodes (All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs), ★ star shown for favorites in All view
|
||||
- **Category sub-tab** — dropdown to filter nodes by top-level category
|
||||
- **Package sub-tab** — dropdown to filter nodes by custom node package name
|
||||
- **Models tab** — browse installed models (All / ★ Favorites / Groups / By Type sub-tabs)
|
||||
- **Prompts tab** — browse prompt presets with All / Favorites / Categories sub-tabs
|
||||
- **Drag & drop workflows** — drag a workflow onto the canvas to load it
|
||||
@@ -115,6 +117,7 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
|
||||
- **Copy prompts** — copy individual positive (P) or negative (N) prompts from sidebar items
|
||||
- **Double-click** — load workflows or place nodes without dragging
|
||||
- **Search** — search within each sub-tab to quickly find items
|
||||
- **⚙ Theme settings** — customize panel background, sub-header background, text, border, and secondary text colors; saved to localStorage and applied on every open
|
||||
|
||||
### Help & Support Tab (v0.1.3)
|
||||
- **Feature list** — overview of all features organized by tab
|
||||
@@ -191,6 +194,12 @@ Click the **camera icon** (next to the W button) in ComfyUI's top bar to capture
|
||||
|
||||
## Changelog
|
||||
|
||||
### v0.2.5
|
||||
- **Sidebar Category/Package sub-tabs** — added 📂 Category and 🧩 Package sub-tabs to the Nodes tab in Workflow Studio Library; each shows a dropdown to filter nodes by category or custom node package (reads `/object_info` for accurate package detection)
|
||||
- **Sidebar theme settings** — added ⚙ button in the panel header to customize background, sub-header background, text, border, and secondary text colors with live preview; settings saved to localStorage
|
||||
- **Sidebar TextEncoder (By Type)** — fixed TextEncoder not appearing in Models → By Type; now uses `DualCLIPLoader` → `CLIPLoader` fallback to retrieve `clip_name1` list
|
||||
- **Hide `.index.json`** — excluded `.index.json` from both the Workflow tab and the Workflow Studio Library side panel
|
||||
|
||||
### v0.2.4
|
||||
- **GenerateUI tab redesign** — reorganized from 5 tabs (Prompt/Image/Model/Settings/Raw JSON) to 3 tabs (Input/Model/Settings); each tab shows a Raw JSON column (540px) for instant JSON preview and direct editing from any tab
|
||||
- **Input tab** — Prompt and Image stacked vertically in the left column
|
||||
|
||||
@@ -112,6 +112,7 @@ class WorkflowService:
|
||||
"summary": meta.get("summary", ""),
|
||||
"modelTypesOverride": override_types,
|
||||
"favorite": bool(meta.get("favorite", False)),
|
||||
"badges": meta.get("badges", []),
|
||||
},
|
||||
"mtime": stat.st_mtime,
|
||||
"thumbnail": thumbnail,
|
||||
@@ -132,7 +133,7 @@ class WorkflowService:
|
||||
"""Save metadata (tags, memo, summary, etc.) for a workflow."""
|
||||
metadata = self._load_metadata()
|
||||
entry = metadata.get(filename, {})
|
||||
for key in ("tags", "memo", "summary", "modelTypesOverride", "favorite"):
|
||||
for key in ("tags", "memo", "summary", "modelTypesOverride", "favorite", "badges"):
|
||||
if key in updates:
|
||||
entry[key] = updates[key]
|
||||
entry["updatedAt"] = self._now_iso()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[project]
|
||||
name = "comfyui-workflow-studio"
|
||||
description = "A comprehensive workflow management and generation UI plugin for ComfyUI. Browse, organize, and execute workflows from a dedicated studio interface."
|
||||
version = "0.2.6"
|
||||
license = "MIT"
|
||||
requires-python = ">= 3.10"
|
||||
dependencies = ["jinja2"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/ketle-man/ComfyUI-Workflow-Studio"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "ketle-man"
|
||||
DisplayName = "Workflow Studio"
|
||||
Icon = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/assets/icon.svg"
|
||||
Banner = "https://raw.githubusercontent.com/ketle-man/ComfyUI-Workflow-Studio/main/assets/banner.svg"
|
||||
+43
-24
@@ -250,12 +250,15 @@ body {
|
||||
|
||||
.wfm-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 161px);
|
||||
grid-template-columns: repeat(auto-fill, 178px);
|
||||
grid-auto-rows: max-content;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
align-content: start;
|
||||
align-items: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -263,8 +266,8 @@ body {
|
||||
============================================ */
|
||||
|
||||
.wfm-card {
|
||||
width: 161px;
|
||||
min-width: 161px;
|
||||
width: 178px;
|
||||
min-width: 178px;
|
||||
background: var(--wfm-surface);
|
||||
border: 1px solid var(--wfm-border);
|
||||
border-radius: var(--wfm-radius);
|
||||
@@ -280,8 +283,8 @@ body {
|
||||
}
|
||||
|
||||
.wfm-card-thumb {
|
||||
width: 161px;
|
||||
height: 162px;
|
||||
width: 178px;
|
||||
height: 200px;
|
||||
background: var(--wfm-bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -827,26 +830,38 @@ body {
|
||||
============================================ */
|
||||
|
||||
.wfm-view-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important;
|
||||
gap: 6px !important;
|
||||
grid-template-columns: repeat(auto-fill, 220px) !important;
|
||||
grid-auto-rows: max-content !important;
|
||||
gap: 8px !important;
|
||||
justify-items: stretch !important;
|
||||
align-items: start !important;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 220px !important;
|
||||
min-width: 220px !important;
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-fav-btn {
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.wfm-view-card .wfm-card-title {
|
||||
margin-bottom: 4px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
margin-bottom: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -878,6 +893,13 @@ body {
|
||||
.wfm-view-table {
|
||||
display: block !important;
|
||||
overflow-y: auto;
|
||||
justify-items: stretch !important;
|
||||
}
|
||||
|
||||
.wfm-view-table .wfm-card,
|
||||
.wfm-view-table .wfm-model-card {
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.wfm-table {
|
||||
@@ -1007,6 +1029,7 @@ body {
|
||||
border-bottom: 1px solid var(--wfm-border);
|
||||
background: var(--wfm-surface);
|
||||
flex-shrink: 0;
|
||||
min-height: 37px;
|
||||
}
|
||||
|
||||
.wfm-side-panel-title {
|
||||
@@ -1018,18 +1041,13 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wfm-side-panel-close {
|
||||
background: none;
|
||||
border: none;
|
||||
.wfm-side-panel-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--wfm-text-secondary);
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wfm-side-panel-close:hover {
|
||||
color: var(--wfm-text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Side Panel Tabs */
|
||||
@@ -2885,6 +2903,7 @@ body {
|
||||
#wfm-nodes-grid.wfm-view-card {
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 8px;
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
/* Node card: package color left border, auto width */
|
||||
|
||||
+1
-1
@@ -279,7 +279,7 @@ function applyI18nToHtml() {
|
||||
"wfm-help-sidepanel-5": "helpSidepanel5", "wfm-help-sidepanel-6": "helpSidepanel6",
|
||||
"wfm-help-sidepanel-7": "helpSidepanel7", "wfm-help-sidepanel-8": "helpSidepanel8",
|
||||
"wfm-help-sidepanel-9": "helpSidepanel9", "wfm-help-sidepanel-10": "helpSidepanel10",
|
||||
"wfm-help-sidepanel-11": "helpSidepanel11",
|
||||
"wfm-help-sidepanel-11": "helpSidepanel11", "wfm-help-sidepanel-12": "helpSidepanel12",
|
||||
"wfm-help-shortcuts-title": "helpShortcutsTitle",
|
||||
"wfm-help-shortcuts-1": "helpShortcuts1", "wfm-help-shortcuts-2": "helpShortcuts2",
|
||||
"wfm-help-shortcuts-3": "helpShortcuts3", "wfm-help-shortcuts-4": "helpShortcuts4",
|
||||
|
||||
+21
-3
@@ -167,6 +167,10 @@ const LANGUAGES = {
|
||||
tabNodes: "Nodes",
|
||||
nodesAllNodes: "All Nodes",
|
||||
nodesNodeSets: "Node Sets",
|
||||
nodesCategoryTab: "Category",
|
||||
nodesPackageTab: "Package",
|
||||
nodesSelectCategory: "Select a category...",
|
||||
nodesSelectPackage: "Select a package...",
|
||||
nodesSearchPlaceholder: "Search nodes...",
|
||||
nodesAllCategories: "All Categories",
|
||||
nodesAllPackages: "All Packages",
|
||||
@@ -251,6 +255,7 @@ const LANGUAGES = {
|
||||
modelsCopiedPath: "File path copied",
|
||||
modelsNoPreview: "No preview",
|
||||
modelsTagsHint: "(comma separated)",
|
||||
modelsAllDirs: "All Folders",
|
||||
modelsAllGroups: "All Groups",
|
||||
modelsCurrentGroups: "Current Groups",
|
||||
modelsNoGroup: "Not assigned to any group",
|
||||
@@ -342,7 +347,7 @@ const LANGUAGES = {
|
||||
helpSidepanelTitle: "Workflow Studio Library (ComfyUI Side Panel)",
|
||||
helpSidepanel1: "Open from the Workflow Studio Library button in the ComfyUI toolbar",
|
||||
helpSidepanel2: "Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view",
|
||||
helpSidepanel3: "Nodes tab: All / ★ Favorites / Groups / Sets sub-tabs; ★ shown for favorites in All view",
|
||||
helpSidepanel3: "Nodes tab: All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs; Category and Package show a dropdown to filter nodes by category or custom node package",
|
||||
helpSidepanel4: "Prompts tab: browse prompt presets with All/Favorites/Categories sub-tabs",
|
||||
helpSidepanel5: "Drag & drop workflows onto the canvas to load them",
|
||||
helpSidepanel6: "Drag & drop nodes/node sets onto the canvas to place them",
|
||||
@@ -351,6 +356,7 @@ const LANGUAGES = {
|
||||
helpSidepanel9: "Double-click items to load/place without dragging",
|
||||
helpSidepanel10: "Search within each sub-tab to quickly find items",
|
||||
helpSidepanel11: "Models tab: All / ★ Favorites / Groups / By Type sub-tabs",
|
||||
helpSidepanel12: "⚙ Theme button (header): customize panel background, sub-header background, text, border, and secondary text colors — changes are saved to localStorage and applied on next open",
|
||||
helpShortcutsTitle: "Keyboard Shortcuts",
|
||||
helpShortcuts1: "Escape: Close modal/panel",
|
||||
helpShortcuts2: "Enter: Confirm rename, send chat",
|
||||
@@ -628,6 +634,10 @@ const LANGUAGES = {
|
||||
tabNodes: "ノード",
|
||||
nodesAllNodes: "全ノード",
|
||||
nodesNodeSets: "ノードセット",
|
||||
nodesCategoryTab: "カテゴリ",
|
||||
nodesPackageTab: "パッケージ",
|
||||
nodesSelectCategory: "カテゴリを選択...",
|
||||
nodesSelectPackage: "パッケージを選択...",
|
||||
nodesSearchPlaceholder: "ノードを検索...",
|
||||
nodesAllCategories: "全カテゴリ",
|
||||
nodesAllPackages: "全パッケージ",
|
||||
@@ -712,6 +722,7 @@ const LANGUAGES = {
|
||||
modelsCopiedPath: "ファイルパスをコピーしました",
|
||||
modelsNoPreview: "プレビューなし",
|
||||
modelsTagsHint: "(カンマ区切り)",
|
||||
modelsAllDirs: "すべてのフォルダ",
|
||||
modelsAllGroups: "すべてのグループ",
|
||||
modelsCurrentGroups: "所属グループ",
|
||||
modelsNoGroup: "グループに未所属",
|
||||
@@ -803,7 +814,7 @@ const LANGUAGES = {
|
||||
helpSidepanelTitle: "Workflow Studio Library(ComfyUIサイドパネル)",
|
||||
helpSidepanel1: "ComfyUIツールバーのWorkflow Studio Libraryボタンから開く",
|
||||
helpSidepanel2: "Workflowsタブ:All / ★ Favorites / Groupsサブタブ、Allビューでお気に入りに★表示",
|
||||
helpSidepanel3: "Nodesタブ:All / ★ Favorites / Groups / Setsサブタブ、Allビューでお気に入りに★表示",
|
||||
helpSidepanel3: "Nodesタブ:All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Packageサブタブ;CategoryとPackageはドロップダウンでカテゴリ・カスタムノードパッケージ別に絞り込み可能",
|
||||
helpSidepanel4: "Promptsタブ:プロンプトプリセットの閲覧(All/お気に入り/カテゴリ サブタブ)",
|
||||
helpSidepanel5: "ワークフローをキャンバスにドラッグ&ドロップで読み込み",
|
||||
helpSidepanel6: "ノード/ノードセットをキャンバスにドラッグ&ドロップで配置",
|
||||
@@ -812,6 +823,7 @@ const LANGUAGES = {
|
||||
helpSidepanel9: "ダブルクリックでもアイテムの読み込み・配置が可能",
|
||||
helpSidepanel10: "各サブタブ内で検索してアイテムをすばやく検索",
|
||||
helpSidepanel11: "Modelsタブ:All / ★ Favorites / Groups / By Typeサブタブ",
|
||||
helpSidepanel12: "⚙ テーマボタン(ヘッダー):パネル背景・サブヘッダー背景・文字色・ボーダー・補助テキスト色をカスタマイズ可能、設定はlocalStorageに保存され次回起動時も反映",
|
||||
helpShortcutsTitle: "キーボード操作",
|
||||
helpShortcuts1: "Escape:モーダル・パネルを閉じる",
|
||||
helpShortcuts2: "Enter:名前変更の確定、チャット送信",
|
||||
@@ -1085,6 +1097,10 @@ const LANGUAGES = {
|
||||
tabNodes: "节点",
|
||||
nodesAllNodes: "所有节点",
|
||||
nodesNodeSets: "节点集",
|
||||
nodesCategoryTab: "分类",
|
||||
nodesPackageTab: "包",
|
||||
nodesSelectCategory: "选择分类...",
|
||||
nodesSelectPackage: "选择包...",
|
||||
nodesSearchPlaceholder: "搜索节点...",
|
||||
nodesAllCategories: "所有分类",
|
||||
nodesAllPackages: "所有包",
|
||||
@@ -1169,6 +1185,7 @@ const LANGUAGES = {
|
||||
modelsCopiedPath: "文件路径已复制",
|
||||
modelsNoPreview: "无预览",
|
||||
modelsTagsHint: "(逗号分隔)",
|
||||
modelsAllDirs: "所有文件夹",
|
||||
modelsAllGroups: "所有分组",
|
||||
modelsCurrentGroups: "当前分组",
|
||||
modelsNoGroup: "未分配到任何分组",
|
||||
@@ -1260,7 +1277,7 @@ const LANGUAGES = {
|
||||
helpSidepanelTitle: "Workflow Studio Library(ComfyUI侧面板)",
|
||||
helpSidepanel1: "从ComfyUI工具栏的Workflow Studio Library按钮打开",
|
||||
helpSidepanel2: "Workflows标签:All / ★ Favorites / Groups子标签,All视图中收藏项显示★",
|
||||
helpSidepanel3: "Nodes标签:All / ★ Favorites / Groups / Sets子标签,All视图中收藏项显示★",
|
||||
helpSidepanel3: "Nodes标签:All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package子标签;Category和Package可通过下拉菜单按分类或自定义节点包筛选节点",
|
||||
helpSidepanel4: "Prompts标签:浏览提示词预设(全部/收藏/分类 子标签)",
|
||||
helpSidepanel5: "将工作流拖放到画布上即可加载",
|
||||
helpSidepanel6: "将节点/节点集拖放到画布上即可放置",
|
||||
@@ -1269,6 +1286,7 @@ const LANGUAGES = {
|
||||
helpSidepanel9: "双击项目也可加载/放置",
|
||||
helpSidepanel10: "在各子标签内搜索快速查找项目",
|
||||
helpSidepanel11: "Models标签:All / ★ Favorites / Groups / By Type子标签",
|
||||
helpSidepanel12: "⚙ 主题按钮(标题栏):可自定义面板背景、子标题背景、文字颜色、边框及辅助文字颜色,设置保存至localStorage并在下次打开时生效",
|
||||
helpShortcutsTitle: "键盘操作",
|
||||
helpShortcuts1: "Escape:关闭弹窗/面板",
|
||||
helpShortcuts2: "Enter:确认重命名、发送聊天",
|
||||
|
||||
+41
-45
@@ -27,6 +27,7 @@ const state = {
|
||||
searchText: "",
|
||||
tagFilter: "",
|
||||
badgeFilter: "",
|
||||
dirFilter: "",
|
||||
groupFilter: "",
|
||||
showFavoritesOnly: false,
|
||||
viewMode: localStorage.getItem("wfm_models_view") || "thumb",
|
||||
@@ -36,7 +37,6 @@ const state = {
|
||||
currentPage: 0,
|
||||
};
|
||||
|
||||
const MODELS_PER_PAGE = 24;
|
||||
|
||||
const FETCH_MAP = {
|
||||
checkpoint: () => comfyUI.fetchCheckpoints(),
|
||||
@@ -257,6 +257,13 @@ function filterModels() {
|
||||
});
|
||||
}
|
||||
|
||||
if (state.dirFilter) {
|
||||
models = models.filter((m) => {
|
||||
const { dir } = parseModelPath(m);
|
||||
return dir === state.dirFilter;
|
||||
});
|
||||
}
|
||||
|
||||
if (state.groupFilter) {
|
||||
const members = state.modelGroups[state.groupFilter] || [];
|
||||
models = models.filter((m) => members.includes(m));
|
||||
@@ -428,6 +435,20 @@ function renderGroupFilter() {
|
||||
select.value = state.groupFilter;
|
||||
}
|
||||
|
||||
function renderDirFilter() {
|
||||
const select = document.getElementById("wfm-models-dir-filter");
|
||||
if (!select) return;
|
||||
const dirs = [...new Set(
|
||||
getCurrentModels()
|
||||
.map((m) => parseModelPath(m).dir)
|
||||
.filter((d) => d !== "")
|
||||
)].sort();
|
||||
select.innerHTML =
|
||||
`<option value="">${t("modelsAllDirs")}</option>` +
|
||||
dirs.map((d) => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join("");
|
||||
select.value = state.dirFilter;
|
||||
}
|
||||
|
||||
async function fetchModelGroups() {
|
||||
try {
|
||||
const res = await fetch("/api/wfm/models/groups");
|
||||
@@ -466,23 +487,18 @@ function renderModelGrid() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const start = state.currentPage * MODELS_PER_PAGE;
|
||||
const page = filtered.slice(start, start + MODELS_PER_PAGE);
|
||||
const totalPages = Math.ceil(filtered.length / MODELS_PER_PAGE);
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderTableView(grid, page, totalPages);
|
||||
renderTableView(grid, filtered);
|
||||
} else if (state.viewMode === "card") {
|
||||
renderCardView(grid, page, totalPages);
|
||||
renderCardView(grid, filtered);
|
||||
} else {
|
||||
renderThumbView(grid, page, totalPages);
|
||||
renderThumbView(grid, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Thumbnail View (same as Workflow tab) ─────────────────
|
||||
|
||||
function renderThumbView(grid, models, totalPages) {
|
||||
function renderThumbView(grid, models) {
|
||||
grid.innerHTML = "";
|
||||
models.forEach((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
@@ -524,13 +540,11 @@ function renderThumbView(grid, models, totalPages) {
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
updatePagination(totalPages);
|
||||
}
|
||||
|
||||
// ── Card View (no thumbnail, compact) ─────────────────────
|
||||
|
||||
function renderCardView(grid, models, totalPages) {
|
||||
function renderCardView(grid, models) {
|
||||
grid.innerHTML = "";
|
||||
models.forEach((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
@@ -550,8 +564,6 @@ function renderCardView(grid, models, totalPages) {
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${escapeHtml(modelName)}">${escapeHtml(name)}</div>
|
||||
<div class="wfm-card-meta">
|
||||
${dir ? `<span class="wfm-badge wfm-badge-sm wfm-badge-dir">${escapeHtml(dir)}</span>` : ""}
|
||||
<span class="wfm-badge wfm-badge-sm">${escapeHtml(ext)}</span>
|
||||
${userBadges} ${tagsHtml}
|
||||
</div>
|
||||
</div>
|
||||
@@ -568,13 +580,11 @@ function renderCardView(grid, models, totalPages) {
|
||||
});
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
updatePagination(totalPages);
|
||||
}
|
||||
|
||||
// ── Table View ────────────────────────────────────────────
|
||||
|
||||
function renderTableView(grid, models, totalPages) {
|
||||
function renderTableView(grid, models) {
|
||||
const rows = models
|
||||
.map((modelName) => {
|
||||
const meta = state.modelMetadata[modelName] || {};
|
||||
@@ -621,25 +631,6 @@ function renderTableView(grid, models, totalPages) {
|
||||
toggleFavorite(mn);
|
||||
});
|
||||
});
|
||||
|
||||
updatePagination(totalPages);
|
||||
}
|
||||
|
||||
function updatePagination(totalPages) {
|
||||
const container = document.getElementById("wfm-models-pagination");
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ""; return; }
|
||||
container.innerHTML = `
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage === 0 ? "disabled" : ""} data-page="prev">«</button>
|
||||
<span>${state.currentPage + 1} / ${totalPages}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">»</button>
|
||||
`;
|
||||
container.querySelector('[data-page="prev"]').addEventListener("click", () => {
|
||||
if (state.currentPage > 0) { state.currentPage--; renderModelGrid(); }
|
||||
});
|
||||
container.querySelector('[data-page="next"]').addEventListener("click", () => {
|
||||
if (state.currentPage < totalPages - 1) { state.currentPage++; renderModelGrid(); }
|
||||
});
|
||||
}
|
||||
|
||||
// ── Favorite toggle ───────────────────────────────────────
|
||||
@@ -755,6 +746,7 @@ function openDetailModal(modelName) {
|
||||
await saveModelMetadata(modelName, { tags, memo, badges });
|
||||
showToast(t("modelsSaved"), "success");
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
@@ -805,9 +797,6 @@ function openDetailModal(modelName) {
|
||||
|
||||
function showSidePanel(modelName) {
|
||||
state.selectedModel = modelName;
|
||||
const panel = document.getElementById("wfm-models-side-panel");
|
||||
if (!panel) return;
|
||||
panel.style.display = "flex";
|
||||
|
||||
// Highlight selected
|
||||
document.querySelectorAll("#wfm-models-grid .wfm-card, #wfm-models-grid .wfm-models-table-row").forEach((el) => {
|
||||
@@ -830,9 +819,9 @@ function showSidePanel(modelName) {
|
||||
}
|
||||
|
||||
function closeSidePanel() {
|
||||
const panel = document.getElementById("wfm-models-side-panel");
|
||||
if (panel) panel.style.display = "none";
|
||||
state.selectedModel = null;
|
||||
const titleEl = document.getElementById("wfm-models-panel-title");
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
document.querySelectorAll("#wfm-models-grid .wfm-card, #wfm-models-grid .wfm-models-table-row").forEach((el) => {
|
||||
el.classList.remove("wfm-card-selected");
|
||||
});
|
||||
@@ -1209,6 +1198,7 @@ async function loadModelsForCurrentType() {
|
||||
|
||||
if (state.loaded[type] && state.modelsByType[type].length > 0) {
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
renderModelGrid();
|
||||
return;
|
||||
}
|
||||
@@ -1224,6 +1214,7 @@ async function loadModelsForCurrentType() {
|
||||
state.loaded[type] = true;
|
||||
|
||||
renderTagFilter();
|
||||
renderDirFilter();
|
||||
renderModelGrid();
|
||||
} catch (err) {
|
||||
console.error("Failed to load models:", err);
|
||||
@@ -1343,6 +1334,7 @@ export function initModelsTab() {
|
||||
state.searchText = "";
|
||||
state.tagFilter = "";
|
||||
state.badgeFilter = "";
|
||||
state.dirFilter = "";
|
||||
state.currentPage = 0;
|
||||
state.selectedModel = null;
|
||||
closeSidePanel();
|
||||
@@ -1374,6 +1366,13 @@ export function initModelsTab() {
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
// Dir filter
|
||||
document.getElementById("wfm-models-dir-filter")?.addEventListener("change", (e) => {
|
||||
state.dirFilter = e.target.value;
|
||||
state.currentPage = 0;
|
||||
renderModelGrid();
|
||||
});
|
||||
|
||||
// Group filter
|
||||
document.getElementById("wfm-models-group-filter")?.addEventListener("change", (e) => {
|
||||
state.groupFilter = e.target.value;
|
||||
@@ -1424,9 +1423,6 @@ export function initModelsTab() {
|
||||
batchFetchCivitai();
|
||||
});
|
||||
|
||||
// Side panel close
|
||||
document.getElementById("wfm-models-panel-close")?.addEventListener("click", closeSidePanel);
|
||||
|
||||
// Side panel tab switching
|
||||
document.querySelectorAll(".wfm-models-side-tab-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
|
||||
+30
-77
@@ -8,7 +8,6 @@ import { t } from "./i18n.js";
|
||||
|
||||
// ── State ─────────────────────────────────────────────────
|
||||
|
||||
const NODES_PER_PAGE = 50;
|
||||
|
||||
const state = {
|
||||
allNodes: [],
|
||||
@@ -257,91 +256,50 @@ function renderNodeGrid() {
|
||||
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML = `<p class="wfm-placeholder">${t("noNodesFound")}</p>`;
|
||||
clearPagination();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderNodeTableView(grid, filtered);
|
||||
clearPagination();
|
||||
return;
|
||||
}
|
||||
|
||||
// Card view with pagination
|
||||
const totalPages = Math.ceil(filtered.length / NODES_PER_PAGE);
|
||||
if (state.currentPage >= totalPages) state.currentPage = totalPages - 1;
|
||||
if (state.currentPage < 0) state.currentPage = 0;
|
||||
|
||||
const start = state.currentPage * NODES_PER_PAGE;
|
||||
const pageItems = filtered.slice(start, start + NODES_PER_PAGE);
|
||||
|
||||
grid.innerHTML = "";
|
||||
pageItems.forEach(node => {
|
||||
const meta = state.nodeMetadata[node.name] || {};
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-card wfm-node-card";
|
||||
if (state.selectedNode && state.selectedNode.name === node.name) {
|
||||
card.classList.add("wfm-card-selected");
|
||||
}
|
||||
card.dataset.nodeName = node.name;
|
||||
card.style.borderLeft = `3px solid ${packageColor(node.package)}`;
|
||||
filtered.forEach(node => grid.appendChild(createNodeCard(node)));
|
||||
}
|
||||
|
||||
const inputCount = Object.keys(node.input.required || {}).length +
|
||||
Object.keys(node.input.optional || {}).length;
|
||||
const outputCount = (node.output || []).length;
|
||||
const tags = (meta.tags || []).map(tg => `<span class="wfm-tag">#${escapeHtml(tg)}</span>`).join("");
|
||||
const favStar = meta.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
function createNodeCard(node) {
|
||||
const meta = state.nodeMetadata[node.name] || {};
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-card wfm-node-card";
|
||||
if (state.selectedNode && state.selectedNode.name === node.name) {
|
||||
card.classList.add("wfm-card-selected");
|
||||
}
|
||||
card.dataset.nodeName = node.name;
|
||||
card.style.borderLeft = `3px solid ${packageColor(node.package)}`;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${escapeHtml(node.name)}">${escapeHtml(node.display_name)}</div>
|
||||
<div class="wfm-card-meta">
|
||||
${categoryBadgeHtml(node.category)}
|
||||
</div>
|
||||
${tags ? `<div class="wfm-card-tags">${tags}</div>` : ""}
|
||||
const tags = (meta.tags || []).map(tg => `<span class="wfm-tag">#${escapeHtml(tg)}</span>`).join("");
|
||||
const favStar = meta.favorite ? "\u2605" : "\u2606";
|
||||
const favClass = meta.favorite ? "wfm-fav-btn active" : "wfm-fav-btn";
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="wfm-card-body">
|
||||
<div class="wfm-card-title" title="${escapeHtml(node.name)}">${escapeHtml(node.display_name)}</div>
|
||||
<div class="wfm-card-meta">
|
||||
${categoryBadgeHtml(node.category)}
|
||||
</div>
|
||||
<button class="${favClass}" title="Favorite">${favStar}</button>`;
|
||||
${tags ? `<div class="wfm-card-tags">${tags}</div>` : ""}
|
||||
</div>
|
||||
<button class="${favClass}" title="Favorite">${favStar}</button>`;
|
||||
|
||||
card.querySelector(".wfm-fav-btn").addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(node.name, e.currentTarget);
|
||||
});
|
||||
card.addEventListener("click", () => showNodeSidePanel(node));
|
||||
grid.appendChild(card);
|
||||
card.querySelector(".wfm-fav-btn").addEventListener("click", e => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(node.name, e.currentTarget);
|
||||
});
|
||||
|
||||
renderPagination(filtered.length, totalPages);
|
||||
card.addEventListener("click", () => showNodeSidePanel(node));
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderPagination(totalItems, totalPages) {
|
||||
const container = document.getElementById("wfm-nodes-pagination");
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ""; return; }
|
||||
|
||||
const start = state.currentPage * NODES_PER_PAGE + 1;
|
||||
const end = Math.min((state.currentPage + 1) * NODES_PER_PAGE, totalItems);
|
||||
container.innerHTML = `
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage === 0 ? "disabled" : ""} data-page="prev">«</button>
|
||||
<span>${start}-${end} / ${totalItems}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">»</button>
|
||||
`;
|
||||
container.querySelector('[data-page="prev"]').addEventListener("click", () => {
|
||||
if (state.currentPage > 0) { state.currentPage--; renderNodeGrid(); scrollGridToTop(); }
|
||||
});
|
||||
container.querySelector('[data-page="next"]').addEventListener("click", () => {
|
||||
if (state.currentPage < totalPages - 1) { state.currentPage++; renderNodeGrid(); scrollGridToTop(); }
|
||||
});
|
||||
}
|
||||
|
||||
function clearPagination() {
|
||||
const container = document.getElementById("wfm-nodes-pagination");
|
||||
if (container) container.innerHTML = "";
|
||||
}
|
||||
|
||||
function scrollGridToTop() {
|
||||
document.getElementById("wfm-nodes-grid")?.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function renderNodeTableView(grid, filtered) {
|
||||
grid.innerHTML = "";
|
||||
@@ -399,9 +357,6 @@ async function toggleFavorite(nodeName, btnEl) {
|
||||
|
||||
function showNodeSidePanel(node) {
|
||||
state.selectedNode = node;
|
||||
const panel = document.getElementById("wfm-nodes-side-panel");
|
||||
if (!panel) return;
|
||||
panel.style.display = "flex";
|
||||
|
||||
// Highlight selected card
|
||||
document.querySelectorAll("#wfm-nodes-grid .wfm-card, #wfm-nodes-grid tr").forEach(el => {
|
||||
@@ -621,9 +576,9 @@ function renderSideGroups(node) {
|
||||
}
|
||||
|
||||
function closeNodeSidePanel() {
|
||||
const panel = document.getElementById("wfm-nodes-side-panel");
|
||||
if (panel) panel.style.display = "none";
|
||||
state.selectedNode = null;
|
||||
const titleEl = document.getElementById("wfm-nodes-panel-title");
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
document.querySelectorAll("#wfm-nodes-grid .wfm-card, #wfm-nodes-grid tr").forEach(el => {
|
||||
el.classList.remove("wfm-card-selected");
|
||||
});
|
||||
@@ -1083,8 +1038,6 @@ export function initNodesTab() {
|
||||
loadNodesData();
|
||||
});
|
||||
|
||||
// Side panel close
|
||||
document.getElementById("wfm-nodes-panel-close")?.addEventListener("click", closeNodeSidePanel);
|
||||
|
||||
// Side panel tab switching
|
||||
document.querySelectorAll(".wfm-nodes-side-tab-btn").forEach(btn => {
|
||||
|
||||
+12
-37
@@ -25,7 +25,6 @@ const state = {
|
||||
currentPage: 0,
|
||||
};
|
||||
|
||||
const WF_PER_PAGE = 24;
|
||||
|
||||
function getBadgePalette() {
|
||||
try { return JSON.parse(localStorage.getItem("wfm_models_badge_palette") || "{}"); }
|
||||
@@ -245,12 +244,17 @@ function renderModelFilters() {
|
||||
const container = document.getElementById("wfm-model-filters");
|
||||
if (!container) return;
|
||||
|
||||
const badges = getAllBadges();
|
||||
const palette = getBadgePalette();
|
||||
const badges = Object.keys(palette).sort();
|
||||
|
||||
container.innerHTML = `
|
||||
<button class="wfm-filter-btn ${state.activeBadge === "ALL" ? "active" : ""}" data-filter="ALL">ALL</button>
|
||||
<button class="wfm-filter-btn ${state.activeBadge === "FAVORITE" ? "active" : ""}" data-filter="FAVORITE">★</button>
|
||||
${badges.map((b) => `<button class="wfm-filter-btn ${state.activeBadge === b ? "active" : ""}" data-filter="${escapeHtml(b)}">${escapeHtml(b)}</button>`).join("")}
|
||||
${badges.map((b) => {
|
||||
const color = palette[b] || "";
|
||||
const style = color ? ` style="background:${color};color:#fff;"` : "";
|
||||
return `<button class="wfm-filter-btn ${state.activeBadge === b ? "active" : ""}" data-filter="${escapeHtml(b)}"${style}>${escapeHtml(b)}</button>`;
|
||||
}).join("")}
|
||||
`;
|
||||
|
||||
container.querySelectorAll(".wfm-filter-btn").forEach((btn) => {
|
||||
@@ -265,6 +269,8 @@ function renderModelFilters() {
|
||||
|
||||
function filterWorkflows() {
|
||||
return state.workflows.filter((wf) => {
|
||||
if (wf.filename === ".index.json") return false;
|
||||
|
||||
if (state.activeBadge === "FAVORITE") {
|
||||
if (!wf.metadata.favorite) return false;
|
||||
} else if (state.activeBadge !== "ALL") {
|
||||
@@ -312,27 +318,16 @@ function renderGrid() {
|
||||
if (filtered.length === 0) {
|
||||
grid.innerHTML =
|
||||
'<p class="wfm-placeholder">No workflows found</p>';
|
||||
updateWfPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.viewMode === "table") {
|
||||
renderTableView(grid, filtered);
|
||||
updateWfPagination(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Thumbnail / Card view with pagination
|
||||
const totalPages = Math.ceil(filtered.length / WF_PER_PAGE);
|
||||
if (state.currentPage >= totalPages) state.currentPage = totalPages - 1;
|
||||
if (state.currentPage < 0) state.currentPage = 0;
|
||||
const start = state.currentPage * WF_PER_PAGE;
|
||||
const pageItems = filtered.slice(start, start + WF_PER_PAGE);
|
||||
|
||||
updateWfPagination(totalPages);
|
||||
|
||||
grid.innerHTML = "";
|
||||
pageItems.forEach((wf) => {
|
||||
filtered.forEach((wf) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "wfm-card";
|
||||
card.dataset.filename = wf.filename;
|
||||
@@ -382,22 +377,6 @@ function renderGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateWfPagination(totalPages) {
|
||||
const container = document.getElementById("wfm-workflow-pagination");
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) { container.innerHTML = ""; return; }
|
||||
container.innerHTML = `
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage === 0 ? "disabled" : ""} data-page="prev">«</button>
|
||||
<span>${state.currentPage + 1} / ${totalPages}</span>
|
||||
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">»</button>
|
||||
`;
|
||||
container.querySelector('[data-page="prev"]').addEventListener("click", () => {
|
||||
if (state.currentPage > 0) { state.currentPage--; renderGrid(); }
|
||||
});
|
||||
container.querySelector('[data-page="next"]').addEventListener("click", () => {
|
||||
if (state.currentPage < totalPages - 1) { state.currentPage++; renderGrid(); }
|
||||
});
|
||||
}
|
||||
|
||||
function renderTableView(grid, filtered) {
|
||||
const table = document.createElement("table");
|
||||
@@ -484,7 +463,6 @@ async function showSidePanel(wf, cardEl) {
|
||||
|
||||
titleEl.textContent = wf.filename.replace(/\.json$/, "");
|
||||
contentEl.textContent = t("loading");
|
||||
panel.style.display = "flex";
|
||||
|
||||
state.selectedWf = wf;
|
||||
|
||||
@@ -528,12 +506,12 @@ async function showSidePanel(wf, cardEl) {
|
||||
}
|
||||
|
||||
function closeSidePanel() {
|
||||
const panel = document.getElementById("wfm-side-panel");
|
||||
if (panel) panel.style.display = "none";
|
||||
document.querySelectorAll(".wfm-card, .wfm-table-row").forEach((c) =>
|
||||
c.classList.remove("wfm-card-selected")
|
||||
);
|
||||
state.selectedWf = null;
|
||||
const titleEl = document.getElementById("wfm-side-panel-title");
|
||||
if (titleEl) titleEl.textContent = "";
|
||||
const listLoadBtn = document.getElementById("wfm-list-load-btn");
|
||||
if (listLoadBtn) { listLoadBtn.disabled = true; listLoadBtn.title = t("selectCardFirst"); }
|
||||
const listOpenComfyBtn = document.getElementById("wfm-list-open-comfyui-btn");
|
||||
@@ -1088,9 +1066,6 @@ export function initWorkflowTab() {
|
||||
});
|
||||
}
|
||||
|
||||
// Side panel: close button
|
||||
document.getElementById("wfm-side-panel-close")?.addEventListener("click", closeSidePanel);
|
||||
|
||||
// Side panel: tab switching
|
||||
document.querySelectorAll(".wfm-side-tab-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
|
||||
+6
-10
@@ -59,7 +59,6 @@
|
||||
<button id="wfm-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="View settings">⚙</button>
|
||||
<div id="wfm-badge-settings-panel" class="wfm-badge-settings-panel" style="display:none;"></div>
|
||||
</div>
|
||||
<div id="wfm-workflow-pagination" class="wfm-pagination-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wfm-workflow-area">
|
||||
@@ -67,10 +66,9 @@
|
||||
<p class="wfm-placeholder">Loading workflows...</p>
|
||||
</div>
|
||||
<!-- Side Panel -->
|
||||
<div id="wfm-side-panel" class="wfm-side-panel" style="display:none;">
|
||||
<div id="wfm-side-panel" class="wfm-side-panel">
|
||||
<div class="wfm-side-panel-header">
|
||||
<span id="wfm-side-panel-title" class="wfm-side-panel-title"></span>
|
||||
<button id="wfm-side-panel-close" class="wfm-side-panel-close">×</button>
|
||||
</div>
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-side-tab-btn active" data-side-tab="thumb">Thumbnail</button>
|
||||
@@ -156,7 +154,6 @@
|
||||
</div>
|
||||
<span id="wfm-nodes-count" class="wfm-nodes-count"></span>
|
||||
<button id="wfm-nodes-refresh-btn" class="wfm-btn wfm-btn-sm">Refresh</button>
|
||||
<div id="wfm-nodes-pagination" class="wfm-pagination-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,10 +163,9 @@
|
||||
<p class="wfm-placeholder" id="wfm-nodes-placeholder">Click to load nodes...</p>
|
||||
</div>
|
||||
<!-- Side Panel -->
|
||||
<div id="wfm-nodes-side-panel" class="wfm-side-panel" style="display:none;">
|
||||
<div id="wfm-nodes-side-panel" class="wfm-side-panel">
|
||||
<div class="wfm-side-panel-header">
|
||||
<span id="wfm-nodes-panel-title" class="wfm-side-panel-title"></span>
|
||||
<button id="wfm-nodes-panel-close" class="wfm-side-panel-close">×</button>
|
||||
</div>
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-nodes-side-tab-btn active" data-side-tab="details">Details</button>
|
||||
@@ -219,6 +215,7 @@
|
||||
<button class="wfm-filter-btn" id="wfm-models-fav-btn" title="Favorites">★</button>
|
||||
<input type="text" id="wfm-models-search" class="wfm-search-input" placeholder="Search models...">
|
||||
<select id="wfm-models-tag-filter" class="wfm-select wfm-toolbar-select"><option value="">All Tags</option></select>
|
||||
<select id="wfm-models-dir-filter" class="wfm-select wfm-toolbar-select"><option value="">All Folders</option></select>
|
||||
<select id="wfm-models-group-filter" class="wfm-select wfm-toolbar-select"><option value="">All Groups</option></select>
|
||||
<div class="wfm-view-btns">
|
||||
<button class="wfm-view-btn active" data-models-view="thumb" title="Thumbnail">⊞</button>
|
||||
@@ -230,7 +227,6 @@
|
||||
<button id="wfm-models-civitai-batch-btn" class="wfm-btn wfm-btn-sm" title="Fetch CivitAI info for all models">CivitAI</button>
|
||||
<span id="wfm-models-civitai-progress" style="font-size:12px;color:var(--wfm-text-secondary);"></span>
|
||||
<button id="wfm-models-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="Badge settings">⚙ Badge</button>
|
||||
<div id="wfm-models-pagination" class="wfm-pagination-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -240,10 +236,9 @@
|
||||
<p class="wfm-placeholder" id="wfm-models-placeholder">Click Refresh to load models...</p>
|
||||
</div>
|
||||
<!-- Side Panel -->
|
||||
<div id="wfm-models-side-panel" class="wfm-side-panel" style="display:none;">
|
||||
<div id="wfm-models-side-panel" class="wfm-side-panel">
|
||||
<div class="wfm-side-panel-header">
|
||||
<span id="wfm-models-panel-title" class="wfm-side-panel-title"></span>
|
||||
<button id="wfm-models-panel-close" class="wfm-side-panel-close">×</button>
|
||||
</div>
|
||||
<div class="wfm-side-tab-nav">
|
||||
<button class="wfm-models-side-tab-btn active" data-side-tab="info">Info</button>
|
||||
@@ -596,7 +591,7 @@
|
||||
<ul>
|
||||
<li id="wfm-help-sidepanel-1">Open from the Workflow Studio Library button in the ComfyUI toolbar</li>
|
||||
<li id="wfm-help-sidepanel-2">Workflows tab: All / ★ Favorites / Groups sub-tabs; ★ shown for favorites in All view</li>
|
||||
<li id="wfm-help-sidepanel-3">Nodes tab: All / ★ Favorites / Groups / Sets sub-tabs; ★ shown for favorites in All view</li>
|
||||
<li id="wfm-help-sidepanel-3">Nodes tab: All / ★ Favorites / Groups / Sets / 📂 Category / 🧩 Package sub-tabs; Category and Package show a dropdown to filter nodes</li>
|
||||
<li id="wfm-help-sidepanel-11">Models tab: All / ★ Favorites / Groups / By Type sub-tabs</li>
|
||||
<li id="wfm-help-sidepanel-4">Prompts tab: browse prompt presets with All/Favorites/Categories sub-tabs</li>
|
||||
<li id="wfm-help-sidepanel-5">Drag & drop workflows onto the canvas to load them</li>
|
||||
@@ -605,6 +600,7 @@
|
||||
<li id="wfm-help-sidepanel-8">Copy individual positive (P) or negative (N) prompts from sidebar items</li>
|
||||
<li id="wfm-help-sidepanel-9">Double-click items to load/place without dragging</li>
|
||||
<li id="wfm-help-sidepanel-10">Search within each sub-tab to quickly find items</li>
|
||||
<li id="wfm-help-sidepanel-12">⚙ Theme button (header): customize panel background, text, border colors — saved to localStorage</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ const state = {
|
||||
topTab: "workflows", // "workflows" | "nodes" | "prompts" | "models"
|
||||
// Node sub-tabs (existing)
|
||||
activeTab: "all", // all | favorites | groups
|
||||
activeTab2: null, // 2nd row: "sets" (or null when 1st row active)
|
||||
activeTab2: null, // 2nd row: "sets" | "category" | "package" (or null when 1st row active)
|
||||
activeNodeCategory: "", // selected category value
|
||||
activeNodePackage: "", // selected package value
|
||||
objectInfo: {}, // raw /object_info data for package lookup
|
||||
favorites: [], // [{name, display_name}]
|
||||
nodeSets: [],
|
||||
groups: {}, // {groupName: [nodeName, ...]}
|
||||
@@ -79,13 +82,28 @@ const fetchGroups = async () => {
|
||||
} catch { return {}; }
|
||||
};
|
||||
|
||||
const fetchObjectInfo = async () => {
|
||||
try {
|
||||
const res = await fetch(`${window.location.origin}/object_info`);
|
||||
return res.ok ? await res.json() : {};
|
||||
} catch { return {}; }
|
||||
};
|
||||
|
||||
const extractPackageName = (pythonModule) => {
|
||||
if (!pythonModule || pythonModule === "nodes") return "ComfyUI (Built-in)";
|
||||
const parts = pythonModule.split(".");
|
||||
if (parts[0] === "custom_nodes" && parts.length > 1) return parts[1];
|
||||
return pythonModule;
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
const [metadata, nodeSets, groups] = await Promise.all([
|
||||
fetchMetadata(), fetchNodeSets(), fetchGroups(),
|
||||
const [metadata, nodeSets, groups, objectInfo] = await Promise.all([
|
||||
fetchMetadata(), fetchNodeSets(), fetchGroups(), fetchObjectInfo(),
|
||||
]);
|
||||
state.metadata = metadata;
|
||||
state.nodeSets = nodeSets;
|
||||
state.groups = groups;
|
||||
state.objectInfo = objectInfo;
|
||||
|
||||
// Extract favorites from metadata
|
||||
state.favorites = [];
|
||||
@@ -149,7 +167,17 @@ const fetchModelList = async (type) => {
|
||||
vae: () => fetch(`${comfyBase}/object_info/VAELoader`).then(r => r.json()).then(d => d?.VAELoader?.input?.required?.vae_name?.[0] || []),
|
||||
controlnet: () => fetch(`${comfyBase}/object_info/ControlNetLoader`).then(r => r.json()).then(d => d?.ControlNetLoader?.input?.required?.control_net_name?.[0] || []),
|
||||
unet: () => fetch(`${comfyBase}/object_info/UNETLoader`).then(r => r.json()).then(d => d?.UNETLoader?.input?.required?.unet_name?.[0] || []),
|
||||
textencoder: () => fetch(`${comfyBase}/object_info/CLIPLoader`).then(r => r.json()).then(d => d?.CLIPLoader?.input?.required?.clip_name1?.[0] || []),
|
||||
textencoder: async () => {
|
||||
const base = window.location.origin;
|
||||
for (const cls of ["DualCLIPLoader", "CLIPLoader"]) {
|
||||
try {
|
||||
const d = await fetch(`${base}/object_info/${cls}`).then(r => r.json());
|
||||
const list = d?.[cls]?.input?.required?.clip_name1?.[0];
|
||||
if (list?.length) return list;
|
||||
} catch {}
|
||||
}
|
||||
return [];
|
||||
},
|
||||
hypernetwork:() => fetch(`${comfyBase}/object_info/HypernetworkLoader`).then(r => r.json()).then(d => d?.HypernetworkLoader?.input?.required?.hypernetwork_name?.[0] || []),
|
||||
embedding: () => fetch(`${comfyBase}/embeddings`).then(r => r.json()).then(d => Array.isArray(d) ? d : []),
|
||||
};
|
||||
@@ -199,7 +227,7 @@ const fetchWorkflowRaw = async (filename) => {
|
||||
|
||||
const loadWfData = async () => {
|
||||
const workflows = await fetchWorkflows();
|
||||
state.wfList = workflows;
|
||||
state.wfList = workflows.filter(w => w.filename !== ".index.json");
|
||||
|
||||
// Extract favorites
|
||||
state.wfFavorites = workflows.filter(w => w.metadata?.favorite);
|
||||
@@ -496,6 +524,76 @@ let panelEl = null;
|
||||
|
||||
const PANEL_ID = "wfm-node-library-panel";
|
||||
|
||||
// ============================================
|
||||
// Theme
|
||||
// ============================================
|
||||
|
||||
const THEME_KEY = "wfm_nlp_theme";
|
||||
|
||||
const THEME_VARS = [
|
||||
{ key: "bg", label: "Background", cssVar: "--comfy-menu-bg", default: "#1e1e1e" },
|
||||
{ key: "input", label: "Sub-header BG", cssVar: "--comfy-input-bg", default: "#2a2a2a" },
|
||||
{ key: "text", label: "Text", cssVar: "--input-text", default: "#dddddd" },
|
||||
{ key: "border", label: "Border", cssVar: "--border-color", default: "#4e4e4e" },
|
||||
{ key: "desc", label: "Secondary text", cssVar: "--descrip-text", default: "#888888" },
|
||||
];
|
||||
|
||||
function loadTheme() {
|
||||
try { return JSON.parse(localStorage.getItem(THEME_KEY)) || {}; } catch { return {}; }
|
||||
}
|
||||
|
||||
function applyTheme(panel, theme) {
|
||||
THEME_VARS.forEach(({ key, cssVar, default: def }) => {
|
||||
panel.style.setProperty(cssVar, theme[key] || def);
|
||||
});
|
||||
}
|
||||
|
||||
function buildThemePanel(panel) {
|
||||
const themePanel = panel.querySelector(".wfm-nlp-theme-panel");
|
||||
if (!themePanel) return;
|
||||
const theme = loadTheme();
|
||||
themePanel.innerHTML = `
|
||||
<div class="wfm-nlp-theme-title">Panel Theme</div>
|
||||
${THEME_VARS.map(({ key, label, default: def }) => `
|
||||
<div class="wfm-nlp-theme-row">
|
||||
<span class="wfm-nlp-theme-label">${label}</span>
|
||||
<input type="color" class="wfm-nlp-theme-color" data-key="${key}"
|
||||
value="${theme[key] || def}">
|
||||
</div>`).join("")}
|
||||
<div class="wfm-nlp-theme-actions">
|
||||
<button class="wfm-nlp-theme-reset-btn">Reset</button>
|
||||
<button class="wfm-nlp-theme-save-btn">Save</button>
|
||||
</div>`;
|
||||
|
||||
themePanel.querySelector(".wfm-nlp-theme-save-btn").addEventListener("click", () => {
|
||||
const saved = {};
|
||||
themePanel.querySelectorAll(".wfm-nlp-theme-color").forEach(input => {
|
||||
saved[input.dataset.key] = input.value;
|
||||
});
|
||||
localStorage.setItem(THEME_KEY, JSON.stringify(saved));
|
||||
applyTheme(panel, saved);
|
||||
themePanel.style.display = "none";
|
||||
});
|
||||
|
||||
themePanel.querySelector(".wfm-nlp-theme-reset-btn").addEventListener("click", () => {
|
||||
localStorage.removeItem(THEME_KEY);
|
||||
THEME_VARS.forEach(({ cssVar, default: def }) => {
|
||||
panel.style.setProperty(cssVar, def);
|
||||
});
|
||||
themePanel.style.display = "none";
|
||||
});
|
||||
|
||||
// Live preview on color change
|
||||
themePanel.querySelectorAll(".wfm-nlp-theme-color").forEach(input => {
|
||||
input.addEventListener("input", () => {
|
||||
panel.style.setProperty(
|
||||
THEME_VARS.find(v => v.key === input.dataset.key).cssVar,
|
||||
input.value
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const createPanel = () => {
|
||||
if (panelEl) return panelEl;
|
||||
|
||||
@@ -504,9 +602,11 @@ const createPanel = () => {
|
||||
panel.innerHTML = `
|
||||
<div class="wfm-nlp-header">
|
||||
<span class="wfm-nlp-title">Workflow Studio Library</span>
|
||||
<button class="wfm-nlp-theme-btn" title="Theme settings">⚙</button>
|
||||
<button class="wfm-nlp-refresh" title="Refresh">↻</button>
|
||||
<button class="wfm-nlp-close" title="Close">×</button>
|
||||
</div>
|
||||
<div class="wfm-nlp-theme-panel" style="display:none;"></div>
|
||||
<div class="wfm-nlp-tabs">
|
||||
<button class="wfm-nlp-tab wfm-nlp-top-tab active" data-toptab="workflows">Workflows</button>
|
||||
<button class="wfm-nlp-tab wfm-nlp-top-tab" data-toptab="nodes">Nodes</button>
|
||||
@@ -584,7 +684,24 @@ const createPanel = () => {
|
||||
showToast("Library refreshed", "success");
|
||||
});
|
||||
|
||||
// Theme button
|
||||
panel.querySelector(".wfm-nlp-theme-btn").addEventListener("click", () => {
|
||||
const themePanel = panel.querySelector(".wfm-nlp-theme-panel");
|
||||
if (!themePanel) return;
|
||||
const isOpen = themePanel.style.display !== "none";
|
||||
if (isOpen) {
|
||||
themePanel.style.display = "none";
|
||||
} else {
|
||||
buildThemePanel(panel);
|
||||
themePanel.style.display = "block";
|
||||
}
|
||||
});
|
||||
|
||||
injectStyles();
|
||||
|
||||
// Apply saved theme
|
||||
applyTheme(panel, loadTheme());
|
||||
|
||||
rebuildSubTabs();
|
||||
return panel;
|
||||
};
|
||||
@@ -715,6 +832,8 @@ const rebuildSubTabs = () => {
|
||||
];
|
||||
const row2Tabs = [
|
||||
{ key: "sets", label: "\u2630 Sets" },
|
||||
{ key: "category", label: "\ud83d\udcc2 Category" },
|
||||
{ key: "package", label: "\ud83e\udde9 Package" },
|
||||
];
|
||||
|
||||
const activeKey = state.activeTab2 || state.activeTab;
|
||||
@@ -757,6 +876,9 @@ const renderContent = () => {
|
||||
const content = panelEl?.querySelector(".wfm-nlp-content");
|
||||
if (!content) return;
|
||||
|
||||
// Remove filter dropdowns from previous category/package views
|
||||
panelEl.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove());
|
||||
|
||||
if (state.topTab === "models") {
|
||||
if (!state.modelsLoaded) {
|
||||
content.innerHTML = `<div class="wfm-nlp-empty">Loading...</div>`;
|
||||
@@ -802,6 +924,8 @@ const renderContent = () => {
|
||||
case "favorites": renderFavorites(content); break;
|
||||
case "sets": renderSets(content); break;
|
||||
case "groups": renderGroups(content); break;
|
||||
case "category": renderNodesByCategory(content); break;
|
||||
case "package": renderNodesByPackage(content); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1583,6 +1707,133 @@ const renderGroups = (container) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Render – Nodes by Category / Package
|
||||
// ============================================
|
||||
|
||||
const getNodeCategory = (nodeType) => {
|
||||
const info = state.objectInfo[nodeType];
|
||||
if (info?.category) return info.category.split("/")[0] || "uncategorized";
|
||||
// Fallback: LiteGraph type string often contains category as prefix
|
||||
const def = typeof LiteGraph !== "undefined"
|
||||
? LiteGraph.registered_node_types[nodeType]
|
||||
: null;
|
||||
const cat = def?.category || "";
|
||||
return cat.split("/")[0] || "uncategorized";
|
||||
};
|
||||
|
||||
const getNodePackage = (nodeType) => {
|
||||
const info = state.objectInfo[nodeType];
|
||||
if (!info) return "ComfyUI (Built-in)";
|
||||
return extractPackageName(info.python_module || "");
|
||||
};
|
||||
|
||||
const renderNodesByCategory = (container) => {
|
||||
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
|
||||
const nodeNames = Object.keys(registered).sort();
|
||||
|
||||
// Build category list
|
||||
const catSet = new Set();
|
||||
nodeNames.forEach(n => catSet.add(getNodeCategory(n)));
|
||||
const categories = [...catSet].sort();
|
||||
|
||||
// Insert dropdown above content (remove old one first)
|
||||
container.parentNode.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove());
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "wfm-nlp-filter-row";
|
||||
wrap.style.cssText = "padding:6px 8px;border-bottom:1px solid var(--border-color,#4e4e4e);flex-shrink:0;";
|
||||
const sel = document.createElement("select");
|
||||
sel.style.cssText = "width:100%;padding:4px 6px;background:var(--comfy-input-bg,#2a2a2a);border:1px solid var(--border-color,#4e4e4e);border-radius:3px;color:var(--input-text,#ddd);font-size:12px;";
|
||||
sel.innerHTML = `<option value="">-- All Categories --</option>` +
|
||||
categories.map(c => `<option value="${esc(c)}"${c === state.activeNodeCategory ? " selected" : ""}>${esc(c)}</option>`).join("");
|
||||
sel.addEventListener("change", () => {
|
||||
state.activeNodeCategory = sel.value;
|
||||
renderNodesByCategoryList(container);
|
||||
});
|
||||
wrap.appendChild(sel);
|
||||
container.parentNode.insertBefore(wrap, container);
|
||||
|
||||
renderNodesByCategoryList(container);
|
||||
};
|
||||
|
||||
const renderNodesByCategoryList = (container) => {
|
||||
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
|
||||
let nodeNames = Object.keys(registered).sort();
|
||||
|
||||
if (state.activeNodeCategory) {
|
||||
nodeNames = nodeNames.filter(n => getNodeCategory(n) === state.activeNodeCategory);
|
||||
}
|
||||
if (state.searchText) {
|
||||
nodeNames = nodeNames.filter(n => n.toLowerCase().includes(state.searchText));
|
||||
}
|
||||
|
||||
container.innerHTML = "";
|
||||
if (nodeNames.length === 0) {
|
||||
container.innerHTML = `<div class="wfm-nlp-empty">No nodes found</div>`;
|
||||
return;
|
||||
}
|
||||
for (const name of nodeNames) {
|
||||
const isFav = state.metadata[name]?.favorite;
|
||||
const label = isFav ? `<span class="wfm-nlp-fav-star">\u2605</span>${esc(name)}` : esc(name);
|
||||
const el = createDraggableItem(label, "single", { classType: name });
|
||||
el.addEventListener("dblclick", () => placeSingleNode(name));
|
||||
container.appendChild(el);
|
||||
}
|
||||
};
|
||||
|
||||
const renderNodesByPackage = (container) => {
|
||||
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
|
||||
const nodeNames = Object.keys(registered).sort();
|
||||
|
||||
// Build package list
|
||||
const pkgSet = new Set();
|
||||
nodeNames.forEach(n => pkgSet.add(getNodePackage(n)));
|
||||
const packages = [...pkgSet].sort();
|
||||
|
||||
// Insert dropdown above content (remove old one first)
|
||||
container.parentNode.querySelectorAll(".wfm-nlp-filter-row").forEach(e => e.remove());
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "wfm-nlp-filter-row";
|
||||
wrap.style.cssText = "padding:6px 8px;border-bottom:1px solid var(--border-color,#4e4e4e);flex-shrink:0;";
|
||||
const sel = document.createElement("select");
|
||||
sel.style.cssText = "width:100%;padding:4px 6px;background:var(--comfy-input-bg,#2a2a2a);border:1px solid var(--border-color,#4e4e4e);border-radius:3px;color:var(--input-text,#ddd);font-size:12px;";
|
||||
sel.innerHTML = `<option value="">-- All Packages --</option>` +
|
||||
packages.map(p => `<option value="${esc(p)}"${p === state.activeNodePackage ? " selected" : ""}>${esc(p)}</option>`).join("");
|
||||
sel.addEventListener("change", () => {
|
||||
state.activeNodePackage = sel.value;
|
||||
renderNodesByPackageList(container);
|
||||
});
|
||||
wrap.appendChild(sel);
|
||||
container.parentNode.insertBefore(wrap, container);
|
||||
|
||||
renderNodesByPackageList(container);
|
||||
};
|
||||
|
||||
const renderNodesByPackageList = (container) => {
|
||||
const registered = typeof LiteGraph !== "undefined" ? LiteGraph.registered_node_types : {};
|
||||
let nodeNames = Object.keys(registered).sort();
|
||||
|
||||
if (state.activeNodePackage) {
|
||||
nodeNames = nodeNames.filter(n => getNodePackage(n) === state.activeNodePackage);
|
||||
}
|
||||
if (state.searchText) {
|
||||
nodeNames = nodeNames.filter(n => n.toLowerCase().includes(state.searchText));
|
||||
}
|
||||
|
||||
container.innerHTML = "";
|
||||
if (nodeNames.length === 0) {
|
||||
container.innerHTML = `<div class="wfm-nlp-empty">No nodes found</div>`;
|
||||
return;
|
||||
}
|
||||
for (const name of nodeNames) {
|
||||
const isFav = state.metadata[name]?.favorite;
|
||||
const label = isFav ? `<span class="wfm-nlp-fav-star">\u2605</span>${esc(name)}` : esc(name);
|
||||
const el = createDraggableItem(label, "single", { classType: name });
|
||||
el.addEventListener("dblclick", () => placeSingleNode(name));
|
||||
container.appendChild(el);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Draggable item factory (nodes)
|
||||
// ============================================
|
||||
@@ -2062,6 +2313,75 @@ const injectStyles = () => {
|
||||
background: rgba(255,255,255,0.12);
|
||||
color: var(--descrip-text, #aaa);
|
||||
}
|
||||
.wfm-nlp-theme-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--input-text, #ddd);
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
padding: 0 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
.wfm-nlp-theme-btn:hover { opacity: 1; }
|
||||
.wfm-nlp-theme-panel {
|
||||
border-bottom: 1px solid var(--border-color, #4e4e4e);
|
||||
background: var(--comfy-input-bg, #2a2a2a);
|
||||
padding: 10px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.wfm-nlp-theme-title {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--input-text, #ddd);
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.wfm-nlp-theme-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.wfm-nlp-theme-label {
|
||||
font-size: 11px;
|
||||
color: var(--descrip-text, #aaa);
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.wfm-nlp-theme-color {
|
||||
width: 32px;
|
||||
height: 22px;
|
||||
border: 1px solid var(--border-color, #555);
|
||||
border-radius: 3px;
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
.wfm-nlp-theme-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.wfm-nlp-theme-save-btn, .wfm-nlp-theme-reset-btn {
|
||||
font-size: 11px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color, #555);
|
||||
}
|
||||
.wfm-nlp-theme-save-btn {
|
||||
background: #4a9eff;
|
||||
color: #fff;
|
||||
border-color: #4a9eff;
|
||||
}
|
||||
.wfm-nlp-theme-save-btn:hover { background: #3a8eef; }
|
||||
.wfm-nlp-theme-reset-btn {
|
||||
background: none;
|
||||
color: var(--descrip-text, #aaa);
|
||||
}
|
||||
.wfm-nlp-theme-reset-btn:hover { color: var(--input-text, #ddd); }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user