Compare commits

...
1 Commits
Author SHA1 Message Date
ketle-manandClaude Sonnet 4.6 40955f2822 Improve UI layout, fix badge persistence, add folder filter (v0.2.6)
- Fix workflow badge not saved/loaded after restart (workflow_service.py)
- Fix workflow badge filter buttons to show palette-defined badges with colors
- Make side panels always visible (remove toggle) in Workflow/Model/Node tabs
- Replace pagination with full scroll display in all three tabs
- Add folder (subdir) filter to Models tab toolbar
- Remove dir/ext badges from Model card view (visible in side panel)
- Resize thumbnail cards to 178x200px, fix grid layout crush with grid-auto-rows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 12:12:31 +09:00
9 changed files with 219 additions and 165 deletions
+97
View File
@@ -1,5 +1,102 @@
# 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 追加修正
### 概要
+2 -1
View File
@@ -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()
+16
View File
@@ -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
View File
@@ -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 */
+3
View File
@@ -255,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",
@@ -721,6 +722,7 @@ const LANGUAGES = {
modelsCopiedPath: "ファイルパスをコピーしました",
modelsNoPreview: "プレビューなし",
modelsTagsHint: "(カンマ区切り)",
modelsAllDirs: "すべてのフォルダ",
modelsAllGroups: "すべてのグループ",
modelsCurrentGroups: "所属グループ",
modelsNoGroup: "グループに未所属",
@@ -1183,6 +1185,7 @@ const LANGUAGES = {
modelsCopiedPath: "文件路径已复制",
modelsNoPreview: "无预览",
modelsTagsHint: "(逗号分隔)",
modelsAllDirs: "所有文件夹",
modelsAllGroups: "所有分组",
modelsCurrentGroups: "当前分组",
modelsNoGroup: "未分配到任何分组",
+41 -45
View File
@@ -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">&laquo;</button>
<span>${state.currentPage + 1} / ${totalPages}</span>
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">&raquo;</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", () => {
+3 -49
View File
@@ -8,7 +8,6 @@ import { t } from "./i18n.js";
// ── State ─────────────────────────────────────────────────
const NODES_PER_PAGE = 50;
const state = {
allNodes: [],
@@ -257,28 +256,16 @@ 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 => grid.appendChild(createNodeCard(node)));
renderPagination(filtered.length, totalPages);
filtered.forEach(node => grid.appendChild(createNodeCard(node)));
}
function createNodeCard(node) {
@@ -313,34 +300,6 @@ function createNodeCard(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">&laquo;</button>
<span>${start}-${end} / ${totalItems}</span>
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">&raquo;</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 = "";
@@ -398,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 => {
@@ -620,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");
});
@@ -1082,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 => {
+10 -37
View File
@@ -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">&#9733;</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) => {
@@ -314,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;
@@ -384,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">&laquo;</button>
<span>${state.currentPage + 1} / ${totalPages}</span>
<button class="wfm-btn wfm-btn-sm" ${state.currentPage >= totalPages - 1 ? "disabled" : ""} data-page="next">&raquo;</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");
@@ -486,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;
@@ -530,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");
@@ -1090,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", () => {
+4 -9
View File
@@ -59,7 +59,6 @@
<button id="wfm-badge-settings-btn" class="wfm-btn wfm-btn-sm" title="View settings">&#9881;</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">&times;</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">&times;</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">&#9733;</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">&#8862;</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">&#9881; 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">&times;</button>
</div>
<div class="wfm-side-tab-nav">
<button class="wfm-models-side-tab-btn active" data-side-tab="info">Info</button>