Compare commits

...
3 Commits
Author SHA1 Message Date
ketle-manandClaude Opus 4.6 6a465ee231 Add theme customization: color overrides, background patterns, font selection (v0.2.1)
- Theme customizer panel with 6 color pickers for live CSS variable overrides
- Background patterns: stripes (H/V/D), polka dot, checkerboard, custom SVG tiling
- SVG pattern controls: color (with style injection for full recoloring), opacity, scale, gap
- Font selector: 16 Google Fonts including Japanese display fonts, dynamically loaded
- Settings tab sections converted to collapsible accordion panels (details/summary)
- Fix customizer reset not clearing colors (localStorage spread merge issue)
- Help tab Settings section updated with new feature descriptions
- i18n: EN/JA/ZH translations for all new UI labels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 09:21:36 +09:00
ketle-manandClaude Opus 4.6 29f1ff9f58 Fix topbar button icons not displaying due to Vue re-render
The three WFS buttons (Workflow Studio, Snapshot, Library) showed blank
icons because Vue re-rendered the DOM after SVG injection, reverting to
unsupported icon-[mdi--*] classes. Use MutationObserver to re-apply SVGs
whenever the container DOM changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 00:22:25 +09:00
ketle-manandClaude Opus 4.6 2be048f13c Add DEVLOG entry for v0.2.0 prompt presets feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 16:25:11 +09:00
8 changed files with 1158 additions and 88 deletions
+160
View File
@@ -1,5 +1,165 @@
# DEVLOG - ComfyUI-Workflow-Studio
## 2026-03-27: テーマカスタマイズ機能・設定タブ改善
### 概要
- テーマのカラーカスタマイズ、背景パターン、フォント選択機能を追加
- 設定タブの各セクションをアコーディオン式折りたたみに変更
- ヘルプタブのSettings Tab説明を更新
### 変更内容
#### `static/js/settings-tab.js` — テーマカスタマイズ機能追加
- **カラーオーバーライド:** 6つのCSS変数(背景、サブ背景、サーフェス、テキスト、プライマリ、アクセント)をカラーピッカーでリアルタイム調整、localStorageに保存
- **背景パターン:** 7種類(なし、横/縦/斜めストライプ、ポルカドット、チェック、SVGファイル)を選択可能
- パターンオプション: 色、不透明度、サイズ、間隔(gap)スライダー
- SVGタイリング: FileReader APIでクライアント側読込、色の置換(fill/stroke属性+`<style>`要素注入)、`::after`疑似要素でopacity制御、gap対応はviewBox拡張ラッパーSVG方式
- **フォント選択:** 16種のGoogle Fonts(日本語デザインフォント含む)を動的ロード、クリックで即時プレビュー
- ゴシック系: Noto Sans JP, Zen丸ゴシック, M PLUS Rounded, 小杉丸ゴシック, さわらびゴシック, BIZ UDPゴシック
- デザイン系: ドットゴシック16, はちまるポップ, デラゴシック, レゲエ One, ロックンロール, ステッキ, トレイン One
- 等幅: Space Mono, Fira Code
- **保存ロジック修正:** `saveLocalSettings()`のスプレッドマージでは`delete`したキーが消えない問題を修正、リセット時は`localStorage.setItem()`で直接上書き
- **SVGリカラー強化:** `<style>`ブロックをSVGに注入し、fill属性がない要素にも色を適用
#### `static/css/main.css` — UIスタイル追加
- **設定アコーディオン:** `<details>`/`<summary>`要素用のスタイル(`.wfm-settings-section`, `.wfm-settings-summary`)— 三角矢印回転アニメーション付き
- **テーマカスタマイザー:** カラーピッカーグリッド、パターン選択グリッド(プレビューSVG付き)、パターンオプション行、フォント選択グリッド
- **パターンプレビュー改善:** SVGプレビューのコントラストを大幅強化(60x60サイズ、太い線、高opacity、青系前景色)
#### `static/js/i18n.js` — 翻訳追加(EN/JA/ZH)
- テーマカスタマイズ関連: カラー調整、背景パターン、パターンオプション(色、不透明度、サイズ、間隔)、フォント
- フォント名: 16種のフォントラベル
- ヘルプSettings項目: helpSettings7, helpSettings8追加
#### `static/js/app.js` — i18nマッピング追加
- `helpSettings7`, `helpSettings8` のHTML id→i18nキーマッピングを追加
#### `templates/index.html` — ヘルプタブ更新
- Settings Tabセクション: テーマカスタマイズ機能の説明を追加(項目2)、Eagle連携を項目7に、アコーディオン折りたたみを項目8に追加
### 技術的な判断
- **SVGカラー変更:** 属性置換だけでは不十分なSVG(CSSクラスやデフォルト黒を使用)に対応するため、`<style>`ブロック注入方式を採用
- **SVG間隔(gap):** viewBoxを拡張するラッパーSVGでタイル間に余白を作成
- **フォント読込:** Google Fonts CDNから動的にlinkタグを挿入、一度読み込んだフォントはSetで管理し重複読込を防止
- **アコーディオン:** `<details>`/`<summary>`ネイティブHTML要素を使用、JSなしで開閉動作
## 2026-03-26: トップバーボタンアイコン修正・Appモードバッジ調査
### 概要
- トップバーの3つのボタン(Workflow Studio / Snapshot / Library)のアイコンが表示されない問題を修正
- ComfyUI Appモード(linearMode)のワークフローJSON構造を調査、バッジ表示対応は保留
### 変更内容
#### `web/comfyui/top_menu_extension.js` — アイコン置換ロジック修正
- **問題:** `setup()` 内の `replaceButtonIcon()` が `requestAnimationFrame` でボタンを見つけてSVGを注入するが、ComfyUIのVueフレームワークが再レンダリングして元の `<i class="icon-[mdi--...]">` に戻してしまう。リトライ条件が `wfmButtons.length === 0`(ボタン未検出時のみ)のため、一度見つかった後は再注入されなかった
- **修正:** `MutationObserver` でボタンの親コンテナ(`.actionbar-container`)を監視し、DOMが変更されるたびにSVGアイコンを再注入するように変更
- `applyButtonIcon()`: ボタンに既にSVGがあればスキップ(無限ループ防止)、なければSVG注入
- `waitAndObserve()`: ボタンがDOMに現れるまで `requestAnimationFrame` で待機し、見つかったらアイコン置換+`MutationObserver`監視開始
### Appモードバッジ表示について(保留)
- ComfyUI Appモード(BETA機能)のワークフローJSON構造を調査
- `extra.linearMode: true` と `extra.linearData` (inputs/outputs配列)でappモード情報が格納される
- appモード削除後も `linearData`/`linearMode` キーは残り中身が空配列になるだけ、拡張子 `.app.json` も維持される
- **結論:** Appモード自体がBETA機能のため、バッジ表示対応は現状維持とする
## 2026-03-25: v0.2.0 プロンプトプリセット機能・Workflow Studio Library
### 概要
- プロンプトプリセット機能を追加:サイドパネルからドラッグ&ドロップでWFS_PromptTextカスタムノードを作成
- Workflow Studio SPAのPromptタブを3カラムレイアウトに刷新(AIアシスタント|プリセット編集|プリセットマネージャー)
- プリセットのバックエンドAPI化(localStorage→サーバーサイド保存)
- サイドパネル名を「WF & Node Library」→「Workflow Studio Library」に変更
### 変更内容
#### `py/nodes/prompt_text.py` — WFS_PromptTextカスタムノード(新規)
- **WFS_PromptText**: ポジティブ/ネガティブの2つのSTRING入力とSTRING出力を持つカスタムノード
- `INPUT_TYPES`: `positive`(multiline), `negative`(multiline)
- `RETURN_TYPES`: `("STRING", "STRING")`, `RETURN_NAMES`: `("positive", "negative")`
- `CATEGORY`: `"Workflow Studio"`
#### `py/nodes/__init__.py` — パッケージ初期化(新規)
- 空ファイル(Pythonパッケージ認識用)
#### `__init__.py` — カスタムノード登録
- `_NODE_MODULES` に `WFS_PromptText` を追加(分離読み込みパターン)
- `NODE_DISPLAY_NAME_MAPPINGS` に `"Prompt Text (WFS)"` を追加
#### `py/services/prompts_service.py` — プリセットCRUDサービス(新規)
- `list_prompts()`: 全プリセット一覧取得
- `create_prompt(data)`: 新規作成(name, text, negText, category, tags, favorite)
- `update_prompt(id, updates)`: 更新(name, text, negText, category, tags, favorite)
- `delete_prompt(id)`: 削除
- `list_categories()`: カテゴリ一覧取得
- データ永続化: `data/prompts.json`
#### `py/routes/prompts_routes.py` — プリセットAPIエンドポイント(新規)
- `GET /api/wfm/prompts` — 一覧取得
- `POST /api/wfm/prompts` — 新規作成
- `POST /api/wfm/prompts/update` — 更新
- `POST /api/wfm/prompts/delete` — 削除
#### `py/config.py` — 設定追加
- `PROMPTS_FILE = DATA_DIR / "prompts.json"` を追加
#### `py/wfm.py` — ルート登録
- `prompts_routes` のインポートと `prompts_routes.setup_routes(app)` を追加
#### `web/comfyui/node_sets_menu.js` — サイドパネルにPromptsタブ追加
- **トップタブ:** Workflows / Nodes / **Prompts** の3タブ構成
- **Promptsサブタブ:** All / ★ Favorites / 📁 Categories
- **State追加:** `promptSubTab`, `promptList`, `promptFavorites`, `promptCategories`, `promptLoaded`
- **API関数:** `fetchPrompts()`, `loadPromptData()`
- **レンダリング:** `renderPromptAll()`, `renderPromptFavorites()`, `renderPromptCategories()`
- **ドラッグ&ドロップ:** `application/x-wfm-prompt` MIMEタイプ、`placePromptNode(posText, negText, promptName, pos)` でWFS_PromptTextノード作成
- **P/Nコピーボタン:** 各プリセットアイテムにポジティブ(P)・ネガティブ(N)の個別コピーボタン(Nはテキスト空の場合非表示)
- **パネル名変更:** ツールチップ・タイトルを「Workflow Studio Library」に更新
- **CSS追加:** `.wfm-nlp-copy-btns`, `.wfm-nlp-copy-pos`(緑ホバー), `.wfm-nlp-copy-neg`(赤ホバー)
#### `templates/index.html` — Promptタブ3カラムレイアウト
- 2カラム(AI Assistant | Presets)→ 3カラム(AI Assistant | Presets | Preset Manager)に変更
- **Presetsパネル:** Deleteボタン削除、Apply→「GenUI Set」、Category入力欄追加、下部にグループ管理行(Select group + Add to Group + + Group + Del Group)
- **Preset Managerパネル:** All / ★ / Groupsタブ、検索、スクロール可能なリスト
- **ヘルプタブ:** helpPrompt5〜7追加、helpSidepanel4・7〜10追加(Promptsタブ・ドラッグ&ドロップ・P/Nコピー)
- サイドパネルタイトルを「Workflow Studio Library」に更新
#### `static/css/main.css` — スタイル追加(約150行)
- `.wfm-prompt-split-3col` — 3カラムFlexboxレイアウト
- `.wfm-prompt-split-col` — 各カラムのスタイル
- `.wfm-preset-manager` — プリセットマネージャーパネル
- `.wfm-pm-tabs`, `.wfm-pm-tab` — マネージャータブ
- `.wfm-pm-list`, `.wfm-pm-item` — アイテムリスト
- `.wfm-pm-item-actions`, `.wfm-pm-action-btn` — お気に入り(★)・削除(✕)ボタン
- `.wfm-pm-group-header` — グループヘッダー
#### `static/js/prompt-tab.js` — 全面リライト
- **API化:** `fetchPresets()`, `apiCreatePreset()`, `apiUpdatePreset()`, `apiDeletePreset()`
- **localStorage移行:** `migrateLocalStoragePresets()` — 一回限りの自動移行
- **プリセット管理:** `renderPresetSelect()`, `renderGroupSelect()`, `selectPresetInEditor(preset)`
- **Preset Manager:** `renderPresetManager()`, `renderPmAll()`, `renderPmFavorites()`, `renderPmGroups()`
- **`createPmItem()`:** お気に入りトグル(★)と削除(✕)ボタン(コピーボタンなし — Presetsパネル側にPP/NP Copyあり)
- **グループ管理:** Presetsパネル下部のnew-group, add-to-group, del-groupボタン
- **Save:** API経由でnegTextフィールドを含むプリセットの作成/更新
#### `static/js/i18n.js` — 翻訳更新
- `applyPreset` → `"GenUI Set"`(英/日/中 全3言語)
- `deletePreset` キー削除(全3言語)
- ヘルプ: `helpPrompt5`〜`helpPrompt7`, `helpSidepanel8`〜`helpSidepanel10` 追加(全3言語)
- サイドパネルタイトル: `helpSidepanelTitle` → `"Workflow Studio Library"` に更新(全3言語)
#### `static/js/app.js` — i18nマッピング更新
- `helpIdMap` に `helpPrompt5`〜`7`, `helpSidepanel8`〜`10` を追加
- `deletePreset` 適用コード削除
#### `README.md` — v0.2.0更新
- バージョンバッジ: `0.1.9` → `0.2.0`
- Prompt Tab: 3カラムレイアウト、Preset Manager、グループ管理、GenUI Set
- Workflow Studio Library: Promptsタブ、ドラッグ&ドロップ、P/Nコピー
- Project Structure: `py/nodes/`, `prompts_routes.py`, `prompts_service.py` 追加
- Changelog: v0.2.0エントリ追加
- スクリーンショット全8枚差し替え
## 2026-03-24: v0.1.9 サイドパネルUI改善・保存ダイアログ・バグ修正
### 概要
+4 -2
View File
@@ -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.
![Workflow Studio](https://img.shields.io/badge/ComfyUI-Custom_Node-blue)
![Version](https://img.shields.io/badge/version-0.2.0-green)
![Version](https://img.shields.io/badge/version-0.2.1-green)
## Screenshots
@@ -66,11 +66,13 @@ Browse, organize, and execute workflows directly from a dedicated studio interfa
- **GenUI Set** — apply preset prompts directly to the GenerateUI interface
### Settings Tab
- **Collapsible sections** — all settings organized in accordion panels for a clean layout
- **Theme selection** — 13 built-in themes with visual swatch preview (Dark, Pop, Minimalist, Cyberpunk, Glassmorphism, Neumorphism, Retro Pixel, Pastel, Brutalism, Earthy, Material, Monotone, Corporate)
- **Theme customization** — override colors (background, surface, text, primary, accent), add background patterns (horizontal/vertical/diagonal stripes, polka dot, checkerboard, custom SVG tiling with color/opacity/scale/gap controls), and select from 16 fonts including Japanese display fonts (Google Fonts)
- **Workflows directory** — configure which folder to scan for workflows
- **Eagle connection** — set Eagle API endpoint for auto-save
- **Ollama connection** — configure Ollama server URL
- **Badge display** — toggle metadata badges on workflow cards
- **Default workflow** — set a workflow to auto-load on startup
- **Language** — English / Japanese / Chinese
### Nodes Tab (v0.1.7)
+261
View File
@@ -2392,6 +2392,267 @@ body {
width: 100%;
}
/* ============================================
Settings Accordion (details/summary)
============================================ */
.wfm-settings-section {
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius);
margin-bottom: 12px;
overflow: hidden;
}
.wfm-settings-summary {
display: flex;
align-items: center;
padding: 12px 16px;
font-size: 15px;
font-weight: 600;
color: var(--wfm-text);
background: var(--wfm-surface);
cursor: pointer;
-webkit-user-select: none;
user-select: none;
list-style: none;
transition: background 0.15s ease;
}
.wfm-settings-summary:hover {
background: var(--wfm-surface-hover);
}
.wfm-settings-summary::-webkit-details-marker {
display: none;
}
.wfm-settings-summary::before {
content: "\25B6";
font-size: 10px;
margin-right: 10px;
transition: transform 0.2s ease;
color: var(--wfm-text-secondary);
}
.wfm-settings-section[open] > .wfm-settings-summary::before {
transform: rotate(90deg);
}
.wfm-settings-section > *:not(summary) {
padding: 0 16px;
}
.wfm-settings-section > *:nth-child(2) {
padding-top: 12px;
}
.wfm-settings-section > *:last-child {
padding-bottom: 16px;
}
/* ============================================
Theme Customizer
============================================ */
.wfm-customizer-toggle {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
margin-top: 12px;
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius-sm);
background: var(--wfm-surface);
color: var(--wfm-text-secondary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.wfm-customizer-toggle:hover {
color: var(--wfm-primary);
border-color: var(--wfm-primary);
}
.wfm-customizer-toggle .arrow {
transition: transform 0.2s ease;
font-size: 10px;
}
.wfm-customizer-toggle.open .arrow {
transform: rotate(90deg);
}
.wfm-customizer-panel {
display: none;
margin-top: 12px;
padding: 16px;
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius);
background: var(--wfm-bg-secondary);
}
.wfm-customizer-panel.open {
display: block;
}
.wfm-customizer-section {
margin-bottom: 16px;
}
.wfm-customizer-section:last-child {
margin-bottom: 0;
}
.wfm-customizer-section h4 {
font-size: 13px;
font-weight: 600;
margin-bottom: 10px;
color: var(--wfm-text);
}
.wfm-color-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.wfm-color-item {
display: flex;
align-items: center;
gap: 8px;
}
.wfm-color-item label {
font-size: 11px;
color: var(--wfm-text-secondary);
min-width: 60px;
flex-shrink: 0;
}
.wfm-color-item input[type="color"] {
width: 32px;
height: 24px;
border: 1px solid var(--wfm-border);
border-radius: var(--wfm-radius-sm);
padding: 1px;
cursor: pointer;
background: transparent;
}
.wfm-color-item input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.wfm-color-item input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 2px;
}
/* Pattern selector */
.wfm-pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
gap: 6px;
}
.wfm-pattern-item {
width: 100%;
aspect-ratio: 1;
border: 2px solid var(--wfm-border);
border-radius: var(--wfm-radius-sm);
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
}
.wfm-pattern-item:hover {
border-color: var(--wfm-primary);
transform: scale(1.05);
}
.wfm-pattern-item.active {
border-color: var(--wfm-primary);
box-shadow: 0 0 0 2px var(--wfm-primary);
}
.wfm-pattern-item-label {
position: absolute;
bottom: 0;
left: 0;
right: 0;
font-size: 9px;
text-align: center;
background: rgba(0,0,0,0.6);
color: #fff;
padding: 1px 2px;
line-height: 1.2;
}
/* Pattern options row */
.wfm-pattern-options {
display: flex;
gap: 12px;
margin-top: 10px;
align-items: center;
flex-wrap: wrap;
}
.wfm-pattern-options label {
font-size: 11px;
color: var(--wfm-text-secondary);
display: flex;
align-items: center;
gap: 4px;
}
.wfm-pattern-options input[type="range"] {
width: 80px;
accent-color: var(--wfm-primary);
}
.wfm-pattern-options input[type="color"] {
width: 24px;
height: 20px;
border: 1px solid var(--wfm-border);
border-radius: 3px;
padding: 1px;
cursor: pointer;
background: transparent;
}
.wfm-pattern-options input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
.wfm-pattern-options input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 2px;
}
/* SVG upload button */
.wfm-svg-upload-btn {
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: var(--wfm-text-secondary);
background: var(--wfm-surface);
}
.wfm-svg-upload-btn:hover {
color: var(--wfm-primary);
}
/* Font selector */
.wfm-font-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 6px;
}
.wfm-font-item {
padding: 8px 10px;
border: 2px solid var(--wfm-border);
border-radius: var(--wfm-radius-sm);
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
overflow: hidden;
}
.wfm-font-item:hover {
border-color: var(--wfm-primary);
}
.wfm-font-item.active {
border-color: var(--wfm-primary);
box-shadow: 0 0 0 2px var(--wfm-primary);
}
.wfm-font-preview {
font-size: 12px;
white-space: nowrap;
color: var(--wfm-text);
}
/* Customizer action buttons */
.wfm-customizer-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--wfm-border);
}
/* ============================================
Nodes Tab
============================================ */
+1
View File
@@ -238,6 +238,7 @@ function applyI18nToHtml() {
"wfm-help-settings-1": "helpSettings1", "wfm-help-settings-2": "helpSettings2",
"wfm-help-settings-3": "helpSettings3", "wfm-help-settings-4": "helpSettings4",
"wfm-help-settings-5": "helpSettings5", "wfm-help-settings-6": "helpSettings6",
"wfm-help-settings-7": "helpSettings7", "wfm-help-settings-8": "helpSettings8",
"wfm-help-nodes-1": "helpNodes1", "wfm-help-nodes-2": "helpNodes2",
"wfm-help-nodes-3": "helpNodes3", "wfm-help-nodes-4": "helpNodes4",
"wfm-help-nodes-5": "helpNodes5", "wfm-help-nodes-6": "helpNodes6",
+144 -15
View File
@@ -234,11 +234,13 @@ const LANGUAGES = {
helpPrompt6: "Group management: create groups, assign/remove presets, delete groups",
helpPrompt7: "GenUI Set: apply preset prompts directly to GenerateUI",
helpSettings1: "UI language and summary language",
helpSettings2: "Workflow data folder path",
helpSettings3: "ComfyUI connection URL and test",
helpSettings4: "Ollama URL, default model, and connection test",
helpSettings5: "Default workflow setting",
helpSettings6: "Eagle integration URL and auto-save toggle",
helpSettings2: "Theme selection (13 presets) with customization: color overrides, background patterns (stripes, polka dot, checkerboard, SVG tiling), and font selection (16 Google Fonts including Japanese display fonts)",
helpSettings3: "Workflow data folder path",
helpSettings4: "ComfyUI connection URL and test",
helpSettings5: "Ollama URL, default model, and connection test",
helpSettings6: "Default workflow setting",
helpSettings7: "Eagle integration URL and auto-save toggle",
helpSettings8: "All sections are collapsible accordion panels",
helpNodes1: "Node Browser: search by name/description/alias/tag, filter by category, package, tag, group, favorites",
helpNodes2: "Card and Table view modes with pagination",
helpNodes3: "Favorite nodes and add custom tags for organization",
@@ -327,6 +329,47 @@ const LANGUAGES = {
themeMaterial: "Material UI",
themeMonotoneAccent: "Monotone + Accent",
themeCorporate: "Corporate Trust",
// -- Theme Customizer --
themeCustomize: "Customize",
themeCustomizeColors: "Color Override",
themeCustomizeBgPattern: "Background Pattern",
themeColorBg: "Background",
themeColorBgSec: "Secondary BG",
themeColorSurface: "Surface",
themeColorText: "Text",
themeColorPrimary: "Primary",
themeColorAccent: "Accent",
themePatternNone: "None",
themePatternStripeH: "Stripe H",
themePatternStripeV: "Stripe V",
themePatternStripeD: "Stripe D",
themePatternDot: "Polka Dot",
themePatternCheck: "Checkerboard",
themePatternSvg: "SVG File",
themePatternColor: "Pattern Color",
themePatternOpacity: "Opacity",
themePatternScale: "Scale",
themePatternGap: "Gap",
themeCustomizerReset: "Reset Customization",
themeCustomizerApply: "Apply & Save",
// -- Font --
themeCustomizeFont: "Font",
themeFontDefault: "System Default",
themeFontNotoSans: "Noto Sans JP",
themeFontZenMaru: "Zen Maru Gothic",
themeFontMPlusRound: "M PLUS Rounded",
themeFontKosugiMaru: "Kosugi Maru",
themeFontSawarabi: "Sawarabi Gothic",
themeFontBizUdp: "BIZ UDPGothic",
themeFontDotGothic: "DotGothic16",
themeFontHachiMaru: "Hachi Maru Pop",
themeFontDelaGothic: "Dela Gothic One",
themeFontReggaeOne: "Reggae One",
themeFontRocknRoll: "RocknRoll One",
themeFontStick: "Stick",
themeFontTrainOne: "Train One",
themeFontSpaceMono: "Space Mono",
themeFontFiraCode: "Fira Code",
},
ja: {
@@ -559,11 +602,13 @@ const LANGUAGES = {
helpPrompt6: "グループ管理:グループ作成、プリセットの割り当て・解除、グループ削除",
helpPrompt7: "GenUI Set:プリセットのプロンプトをGenerateUIに直接適用",
helpSettings1: "UI言語と要約言語の設定",
helpSettings2: "ワークフローデータフォルダのパス変更・リセット",
helpSettings3: "ComfyUI接続URL設定と接続テスト",
helpSettings4: "Ollama URL、デフォルトモデル、接続テスト",
helpSettings5: "デフォルトワークフローの設定・解除",
helpSettings6: "Eagle連携URL、自動保存の有効/無効",
helpSettings2: "テーマ選択(13種プリセット)+カスタマイズ:カラー調整、背景パターン(ストライプ、ポルカドット、チェック、SVGタイリング)、フォント選択(日本語デザインフォント含む16種のGoogle Fonts)",
helpSettings3: "ワークフローデータフォルダのパス変更・リセット",
helpSettings4: "ComfyUI接続URL設定と接続テスト",
helpSettings5: "Ollama URL、デフォルトモデル、接続テスト",
helpSettings6: "デフォルトワークフローの設定・解除",
helpSettings7: "Eagle連携URL、自動保存の有効/無効",
helpSettings8: "各セクションはアコーディオン式で折りたたみ可能",
helpNodes1: "ノードブラウザ:名前/説明/エイリアス/タグで検索、カテゴリ・パッケージ・タグ・グループ・お気に入りフィルタ",
helpNodes2: "カード / テーブル表示モード切り替え(ページネーション付き)",
helpNodes3: "ノードのお気に入り登録、カスタムタグの追加で整理",
@@ -652,6 +697,47 @@ const LANGUAGES = {
themeMaterial: "Material UI (マテリアル)",
themeMonotoneAccent: "Monotone + Accent",
themeCorporate: "Corporate Trust (企業向け)",
// -- Theme Customizer --
themeCustomize: "カスタマイズ",
themeCustomizeColors: "カラー調整",
themeCustomizeBgPattern: "背景パターン",
themeColorBg: "背景",
themeColorBgSec: "サブ背景",
themeColorSurface: "サーフェス",
themeColorText: "テキスト",
themeColorPrimary: "プライマリ",
themeColorAccent: "アクセント",
themePatternNone: "なし",
themePatternStripeH: "横ストライプ",
themePatternStripeV: "縦ストライプ",
themePatternStripeD: "斜めストライプ",
themePatternDot: "ポルカドット",
themePatternCheck: "チェック",
themePatternSvg: "SVGファイル",
themePatternColor: "パターン色",
themePatternOpacity: "不透明度",
themePatternScale: "サイズ",
themePatternGap: "間隔",
themeCustomizerReset: "カスタマイズをリセット",
themeCustomizerApply: "適用して保存",
// -- Font --
themeCustomizeFont: "フォント",
themeFontDefault: "システム標準",
themeFontNotoSans: "Noto Sans JP",
themeFontZenMaru: "Zen丸ゴシック",
themeFontMPlusRound: "M PLUS 丸ゴシック",
themeFontKosugiMaru: "小杉丸ゴシック",
themeFontSawarabi: "さわらびゴシック",
themeFontBizUdp: "BIZ UDPゴシック",
themeFontDotGothic: "ドットゴシック16",
themeFontHachiMaru: "はちまるポップ",
themeFontDelaGothic: "デラゴシック",
themeFontReggaeOne: "レゲエ One",
themeFontRocknRoll: "ロックンロール",
themeFontStick: "ステッキ",
themeFontTrainOne: "トレイン One",
themeFontSpaceMono: "Space Mono",
themeFontFiraCode: "Fira Code",
},
zh: {
@@ -880,11 +966,13 @@ const LANGUAGES = {
helpPrompt6: "组管理:创建组、分配/移除预设、删除组",
helpPrompt7: "GenUI Set:将预设提示词直接应用到生成UI",
helpSettings1: "UI语言和摘要语言设置",
helpSettings2: "工作流数据文件夹路径更改/重置",
helpSettings3: "ComfyUI连接URL设置和测试",
helpSettings4: "Ollama URL、默认模型、连接测试",
helpSettings5: "默认工作流设置/取消",
helpSettings6: "Eagle集成URL、自动保存开关",
helpSettings2: "主题选择(13种预设)+ 自定义:颜色调整、背景图案(条纹、波点、棋盘格、SVG平铺)、字体选择(含日文显示字体在内的16种Google Fonts)",
helpSettings3: "工作流数据文件夹路径更改/重置",
helpSettings4: "ComfyUI连接URL设置和测试",
helpSettings5: "Ollama URL、默认模型、连接测试",
helpSettings6: "默认工作流设置/取消",
helpSettings7: "Eagle集成URL、自动保存开关",
helpSettings8: "各部分可折叠展开",
helpNodes1: "节点浏览器:按名称/描述/别名/标签搜索,按分类、包、标签、组、收藏筛选",
helpNodes2: "卡片和表格视图模式切换(带分页)",
helpNodes3: "收藏节点,添加自定义标签进行整理",
@@ -973,6 +1061,47 @@ const LANGUAGES = {
themeMaterial: "Material UI (材料)",
themeMonotoneAccent: "Monotone + Accent",
themeCorporate: "Corporate Trust (企业)",
// -- Theme Customizer --
themeCustomize: "自定义",
themeCustomizeColors: "颜色调整",
themeCustomizeBgPattern: "背景图案",
themeColorBg: "背景",
themeColorBgSec: "次要背景",
themeColorSurface: "表面",
themeColorText: "文字",
themeColorPrimary: "主色",
themeColorAccent: "强调色",
themePatternNone: "无",
themePatternStripeH: "横条纹",
themePatternStripeV: "竖条纹",
themePatternStripeD: "斜条纹",
themePatternDot: "波点",
themePatternCheck: "棋盘格",
themePatternSvg: "SVG文件",
themePatternColor: "图案颜色",
themePatternOpacity: "不透明度",
themePatternScale: "大小",
themePatternGap: "间距",
themeCustomizerReset: "重置自定义",
themeCustomizerApply: "应用并保存",
// -- Font --
themeCustomizeFont: "字体",
themeFontDefault: "系统默认",
themeFontNotoSans: "Noto Sans JP",
themeFontZenMaru: "Zen丸哥特",
themeFontMPlusRound: "M PLUS 圆体",
themeFontKosugiMaru: "小杉圆体",
themeFontSawarabi: "蕨类哥特",
themeFontBizUdp: "BIZ UDP哥特",
themeFontDotGothic: "点阵哥特16",
themeFontHachiMaru: "Hachi Maru Pop",
themeFontDelaGothic: "Dela Gothic One",
themeFontReggaeOne: "Reggae One",
themeFontRocknRoll: "RocknRoll One",
themeFontStick: "Stick",
themeFontTrainOne: "Train One",
themeFontSpaceMono: "Space Mono",
themeFontFiraCode: "Fira Code",
},
};
+538 -22
View File
@@ -8,6 +8,12 @@ import { t, getLang, getSummaryLang, setLang, setSummaryLang, getLanguageOptions
const SETTINGS_KEY = "wfm_settings";
function rgbToHex(rgb) {
const m = rgb.match(/(\d+)/g);
if (!m || m.length < 3) return "#000000";
return "#" + m.slice(0, 3).map(n => parseInt(n).toString(16).padStart(2, "0")).join("");
}
function loadLocalSettings() {
try {
return JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}");
@@ -67,12 +73,257 @@ const THEMES = [
{ id: "corporate", key: "themeCorporate", colors: ["#F0F4F8", "#FFFFFF", "#2563EB", "#7C3AED"] },
];
// --- Background pattern definitions ---
const BG_PATTERNS = [
{ id: "none", key: "themePatternNone" },
{ id: "stripe-h", key: "themePatternStripeH" },
{ id: "stripe-v", key: "themePatternStripeV" },
{ id: "stripe-d", key: "themePatternStripeD" },
{ id: "dot", key: "themePatternDot" },
{ id: "check", key: "themePatternCheck" },
{ id: "svg", key: "themePatternSvg" },
];
// Font definitions: id, label key, CSS value, Google Fonts family name (if web font)
const FONT_OPTIONS = [
{ id: "", key: "themeFontDefault", css: "", google: "" },
{ id: "noto-sans-jp", key: "themeFontNotoSans", css: "'Noto Sans JP', sans-serif", google: "Noto+Sans+JP:wght@400;700" },
{ id: "zen-maru", key: "themeFontZenMaru", css: "'Zen Maru Gothic', sans-serif", google: "Zen+Maru+Gothic:wght@400;700" },
{ id: "m-plus-rounded", key: "themeFontMPlusRound", css: "'M PLUS Rounded 1c', sans-serif", google: "M+PLUS+Rounded+1c:wght@400;700" },
{ id: "kosugi-maru", key: "themeFontKosugiMaru", css: "'Kosugi Maru', sans-serif", google: "Kosugi+Maru" },
{ id: "sawarabi-gothic",key: "themeFontSawarabi", css: "'Sawarabi Gothic', sans-serif", google: "Sawarabi+Gothic" },
{ id: "biz-udp", key: "themeFontBizUdp", css: "'BIZ UDPGothic', sans-serif", google: "BIZ+UDPGothic:wght@400;700" },
{ id: "dot-gothic", key: "themeFontDotGothic", css: "'DotGothic16', monospace", google: "DotGothic16" },
{ id: "hachi-maru", key: "themeFontHachiMaru", css: "'Hachi Maru Pop', cursive", google: "Hachi+Maru+Pop" },
{ id: "dela-gothic", key: "themeFontDelaGothic", css: "'Dela Gothic One', sans-serif", google: "Dela+Gothic+One" },
{ id: "reggae-one", key: "themeFontReggaeOne", css: "'Reggae One', cursive", google: "Reggae+One" },
{ id: "rocknroll", key: "themeFontRocknRoll", css: "'RocknRoll One', sans-serif", google: "RocknRoll+One" },
{ id: "stick", key: "themeFontStick", css: "'Stick', sans-serif", google: "Stick" },
{ id: "train-one", key: "themeFontTrainOne", css: "'Train One', cursive", google: "Train+One" },
{ id: "space-mono", key: "themeFontSpaceMono", css: "'Space Mono', monospace", google: "Space+Mono:wght@400;700" },
{ id: "fira-code", key: "themeFontFiraCode", css: "'Fira Code', monospace", google: "Fira+Code:wght@400;700" },
];
// Load Google Font dynamically
const _loadedFonts = new Set();
function loadGoogleFont(googleFamily) {
if (!googleFamily || _loadedFonts.has(googleFamily)) return;
_loadedFonts.add(googleFamily);
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `https://fonts.googleapis.com/css2?family=${googleFamily}&display=swap`;
document.head.appendChild(link);
}
function applyCustomFont(fontId) {
const font = FONT_OPTIONS.find(f => f.id === fontId);
let styleEl = document.getElementById("wfm-custom-font-style");
if (!font || !font.css) {
if (styleEl) styleEl.remove();
return;
}
if (font.google) loadGoogleFont(font.google);
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = "wfm-custom-font-style";
document.head.appendChild(styleEl);
}
styleEl.textContent = `body, .wfm-input, .wfm-select, .wfm-textarea, button { font-family: ${font.css} !important; }`;
}
// Color customization targets
const COLOR_TARGETS = [
{ varName: "--wfm-bg", key: "themeColorBg" },
{ varName: "--wfm-bg-secondary", key: "themeColorBgSec" },
{ varName: "--wfm-surface", key: "themeColorSurface" },
{ varName: "--wfm-text", key: "themeColorText" },
{ varName: "--wfm-primary", key: "themeColorPrimary" },
{ varName: "--wfm-accent", key: "themeColorAccent" },
];
function generatePatternCSS(patternId, color, opacity, scale) {
const o = opacity ?? 0.1;
const s = scale ?? 20;
const c = color || "#ffffff";
// Convert hex to rgba
const r = parseInt(c.slice(1,3),16), g = parseInt(c.slice(3,5),16), b = parseInt(c.slice(5,7),16);
const rgba = `rgba(${r},${g},${b},${o})`;
switch (patternId) {
case "stripe-h":
return `repeating-linear-gradient(0deg, ${rgba} 0px, ${rgba} 1px, transparent 1px, transparent ${s}px)`;
case "stripe-v":
return `repeating-linear-gradient(90deg, ${rgba} 0px, ${rgba} 1px, transparent 1px, transparent ${s}px)`;
case "stripe-d":
return `repeating-linear-gradient(45deg, ${rgba} 0px, ${rgba} 1px, transparent 1px, transparent ${s}px)`;
case "dot": {
const half = s / 2;
const dotR = Math.max(1, s * 0.12);
return `radial-gradient(circle ${dotR}px at ${half}px ${half}px, ${rgba} 100%, transparent 100%)`;
}
case "check": {
const half = s / 2;
return `linear-gradient(45deg, ${rgba} 25%, transparent 25%, transparent 75%, ${rgba} 75%), ` +
`linear-gradient(45deg, ${rgba} 25%, transparent 25%, transparent 75%, ${rgba} 75%)`;
}
default:
return "";
}
}
function generatePatternBgSize(patternId, scale) {
const s = scale ?? 20;
if (patternId === "dot") return `${s}px ${s}px`;
if (patternId === "check") {
const half = s / 2;
return `${s}px ${s}px`;
}
return "";
}
function generatePatternBgPosition(patternId, scale) {
const s = scale ?? 20;
if (patternId === "check") {
const half = s / 2;
return `0 0, ${half}px ${half}px`;
}
return "";
}
function generatePatternPreviewSVG(patternId) {
const fg = "#8cb4ff";
const bg = "#1a1a2e";
const encode = (svg) => `data:image/svg+xml,${encodeURIComponent(svg)}`;
switch (patternId) {
case "none":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><line x1="10" y1="10" x2="50" y2="50" stroke="#555" stroke-width="2"/><line x1="50" y1="10" x2="10" y2="50" stroke="#555" stroke-width="2"/></svg>`);
case "stripe-h":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><rect y="4" width="60" height="4" fill="${fg}" opacity="0.7"/><rect y="16" width="60" height="4" fill="${fg}" opacity="0.7"/><rect y="28" width="60" height="4" fill="${fg}" opacity="0.7"/><rect y="40" width="60" height="4" fill="${fg}" opacity="0.7"/><rect y="52" width="60" height="4" fill="${fg}" opacity="0.7"/></svg>`);
case "stripe-v":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><rect x="4" width="4" height="60" fill="${fg}" opacity="0.7"/><rect x="16" width="4" height="60" fill="${fg}" opacity="0.7"/><rect x="28" width="4" height="60" fill="${fg}" opacity="0.7"/><rect x="40" width="4" height="60" fill="${fg}" opacity="0.7"/><rect x="52" width="4" height="60" fill="${fg}" opacity="0.7"/></svg>`);
case "stripe-d":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><line x1="-5" y1="7" x2="53" y2="-5" stroke="${fg}" stroke-width="3" opacity="0.7"/><line x1="7" y1="19" x2="65" y2="7" stroke="${fg}" stroke-width="3" opacity="0.7"/><line x1="-5" y1="31" x2="53" y2="19" stroke="${fg}" stroke-width="3" opacity="0.7"/><line x1="7" y1="43" x2="65" y2="31" stroke="${fg}" stroke-width="3" opacity="0.7"/><line x1="-5" y1="55" x2="53" y2="43" stroke="${fg}" stroke-width="3" opacity="0.7"/><line x1="7" y1="67" x2="65" y2="55" stroke="${fg}" stroke-width="3" opacity="0.7"/></svg>`);
case "dot":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><circle cx="10" cy="10" r="4" fill="${fg}" opacity="0.8"/><circle cx="30" cy="10" r="4" fill="${fg}" opacity="0.8"/><circle cx="50" cy="10" r="4" fill="${fg}" opacity="0.8"/><circle cx="20" cy="25" r="4" fill="${fg}" opacity="0.8"/><circle cx="40" cy="25" r="4" fill="${fg}" opacity="0.8"/><circle cx="10" cy="40" r="4" fill="${fg}" opacity="0.8"/><circle cx="30" cy="40" r="4" fill="${fg}" opacity="0.8"/><circle cx="50" cy="40" r="4" fill="${fg}" opacity="0.8"/><circle cx="20" cy="55" r="4" fill="${fg}" opacity="0.8"/><circle cx="40" cy="55" r="4" fill="${fg}" opacity="0.8"/></svg>`);
case "check":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><rect x="0" y="0" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="15" y="15" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="30" y="0" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="45" y="15" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="0" y="30" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="15" y="45" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="30" y="30" width="15" height="15" fill="${fg}" opacity="0.35"/><rect x="45" y="45" width="15" height="15" fill="${fg}" opacity="0.35"/></svg>`);
case "svg":
return encode(`<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><rect width="60" height="60" fill="${bg}"/><rect x="10" y="10" width="40" height="40" rx="4" fill="none" stroke="${fg}" stroke-width="2" stroke-dasharray="6 3" opacity="0.6"/><text x="30" y="36" text-anchor="middle" fill="${fg}" font-size="20" font-weight="bold" opacity="0.8">+</text></svg>`);
default:
return "";
}
}
// Apply custom overrides (colors + pattern) on top of the current theme
function applyCustomOverrides(custom) {
const root = document.documentElement;
// Remove old custom style element
let styleEl = document.getElementById("wfm-custom-theme-style");
// Color overrides
if (custom && custom.colors) {
for (const [varName, value] of Object.entries(custom.colors)) {
if (value) root.style.setProperty(varName, value);
}
}
// Pattern
if (custom && custom.pattern && custom.pattern.id && custom.pattern.id !== "none") {
if (!styleEl) {
styleEl = document.createElement("style");
styleEl.id = "wfm-custom-theme-style";
document.head.appendChild(styleEl);
}
const p = custom.pattern;
if (p.id === "svg" && p.svgData) {
const s = p.scale ?? 40;
const o = p.opacity ?? 1;
const c = p.color || null;
const gap = p.gap ?? 0;
// Recolor SVG: inject a global style override into the SVG
let svgStr = p.svgData;
if (c) {
// 1. Replace fill/stroke attributes (but not "none")
svgStr = svgStr
.replace(/fill="(?!none)[^"]*"/gi, `fill="${c}"`)
.replace(/stroke="(?!none)[^"]*"/gi, `stroke="${c}"`);
// 2. Replace inline style fill/stroke
svgStr = svgStr
.replace(/fill:\s*(?!none)[^;"]+/gi, `fill:${c}`)
.replace(/stroke:\s*(?!none)[^;"]+/gi, `stroke:${c}`);
// 3. Inject a <style> block for elements with no explicit fill/stroke
const styleBlock = `<style>*:not([fill="none"]){fill:${c}!important}*:not([stroke="none"])[stroke]{stroke:${c}!important}</style>`;
svgStr = svgStr.replace(/<svg([^>]*)>/i, `<svg$1>${styleBlock}`);
}
// Extract original viewBox/width/height, then create wrapper SVG with padding
const tileSize = s + gap;
let tiledSvg;
if (gap > 0) {
// Wrap: set viewBox to include gap as transparent margin
// Replace the root <svg> width/height/viewBox to add padding
tiledSvg = svgStr.replace(
/<svg([^>]*)>/i,
(match, attrs) => {
// Remove existing width/height/viewBox
let clean = attrs
.replace(/\s*width="[^"]*"/gi, '')
.replace(/\s*height="[^"]*"/gi, '')
.replace(/\s*viewBox="[^"]*"/gi, '');
return `<svg${clean} width="${tileSize}" height="${tileSize}" viewBox="0 0 ${tileSize} ${tileSize}">
<svg width="${s}" height="${s}" x="0" y="0">`;
}
) + '</svg>';
} else {
tiledSvg = svgStr;
}
const encoded = `url("data:image/svg+xml,${encodeURIComponent(tiledSvg)}")`;
styleEl.textContent = `body::after { content: ""; position: fixed; inset: 0; z-index: -1; background-image: ${encoded}; background-size: ${tileSize}px ${tileSize}px; background-repeat: repeat; opacity: ${o}; pointer-events: none; }`;
} else if (p.id !== "svg") {
const bg = generatePatternCSS(p.id, p.color, p.opacity, p.scale);
const bgSize = generatePatternBgSize(p.id, p.scale);
const bgPos = generatePatternBgPosition(p.id, p.scale);
let css = `body { background-image: ${bg} !important;`;
if (bgSize) css += ` background-size: ${bgSize} !important;`;
if (bgPos) css += ` background-position: ${bgPos} !important;`;
css += ` }`;
styleEl.textContent = css;
}
} else {
if (styleEl) styleEl.remove();
}
// Font
if (custom && custom.fontId) {
applyCustomFont(custom.fontId);
} else {
const fontEl = document.getElementById("wfm-custom-font-style");
if (fontEl) fontEl.remove();
}
}
function clearCustomOverrides() {
const root = document.documentElement;
for (const ct of COLOR_TARGETS) {
root.style.removeProperty(ct.varName);
}
const styleEl = document.getElementById("wfm-custom-theme-style");
if (styleEl) styleEl.remove();
const fontEl = document.getElementById("wfm-custom-font-style");
if (fontEl) fontEl.remove();
}
export function applyTheme(themeId) {
if (themeId) {
document.documentElement.setAttribute("data-theme", themeId);
} else {
document.documentElement.removeAttribute("data-theme");
}
// Apply saved custom overrides
const settings = loadLocalSettings();
clearCustomOverrides();
if (settings.themeCustom) {
applyCustomOverrides(settings.themeCustom);
}
}
export function getSavedTheme() {
@@ -101,8 +352,8 @@ export async function initSettingsTab() {
<h2 style="font-size:18px;margin-bottom:20px;">${t("settingsTitle")}</h2>
<!-- Language Settings -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("langLabel")} / ${t("summaryLangLabel")}</h3>
<details class="wfm-settings-section" open>
<summary class="wfm-settings-summary">${t("langLabel")} / ${t("summaryLangLabel")}</summary>
<div class="wfm-form-group">
<label>${t("langLabel")}</label>
<select class="wfm-select" id="wfm-settings-ui-lang">
@@ -118,11 +369,11 @@ export async function initSettingsTab() {
⚠ ${t("summaryLangNote")}
</small>
</div>
</div>
</details>
<!-- Theme -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("themeLabel")}</h3>
<details class="wfm-settings-section" open>
<summary class="wfm-settings-summary">${t("themeLabel")}</summary>
<div class="wfm-theme-grid" id="wfm-theme-grid">
${THEMES.map(th => `
<div class="wfm-theme-card ${(settings.theme || "") === th.id ? "active" : ""}" data-theme-id="${th.id}">
@@ -133,11 +384,94 @@ export async function initSettingsTab() {
</div>
`).join("")}
</div>
</div>
<!-- Theme Customizer -->
<button class="wfm-customizer-toggle" id="wfm-customizer-toggle">
<span class="arrow">&#9654;</span> ${t("themeCustomize")}
</button>
<div class="wfm-customizer-panel" id="wfm-customizer-panel">
<!-- Color overrides -->
<div class="wfm-customizer-section">
<h4>${t("themeCustomizeColors")}</h4>
<div class="wfm-color-grid" id="wfm-color-grid">
${COLOR_TARGETS.map(ct => {
const saved = settings.themeCustom?.colors?.[ct.varName] || "";
return `<div class="wfm-color-item">
<label>${t(ct.key)}</label>
<input type="color" data-var="${ct.varName}" value="${saved || getComputedStyle(document.documentElement).getPropertyValue(ct.varName).trim()}" title="${ct.varName}">
</div>`;
}).join("")}
</div>
</div>
<!-- Background pattern -->
<div class="wfm-customizer-section">
<h4>${t("themeCustomizeBgPattern")}</h4>
<div class="wfm-pattern-grid" id="wfm-pattern-grid">
${BG_PATTERNS.map(p => {
const active = (settings.themeCustom?.pattern?.id || "none") === p.id ? "active" : "";
const preview = generatePatternPreviewSVG(p.id);
const bgStyle = preview
? `background:url('${preview}') no-repeat center/cover;`
: "background:#333;";
return `<div class="wfm-pattern-item ${active} ${p.id === 'svg' ? 'wfm-svg-upload-btn' : ''}" data-pattern-id="${p.id}" style="${bgStyle}">
<span class="wfm-pattern-item-label">${t(p.key)}</span>
</div>`;
}).join("")}
</div>
<input type="file" id="wfm-svg-upload" accept=".svg" style="display:none;">
<!-- Pattern options -->
<div class="wfm-pattern-options" id="wfm-pattern-options" style="${(!settings.themeCustom?.pattern?.id || settings.themeCustom?.pattern?.id === 'none') ? 'display:none;' : ''}">
<label>
${t("themePatternColor")}
<input type="color" id="wfm-pattern-color" value="${settings.themeCustom?.pattern?.color || '#ffffff'}">
</label>
<label>
${t("themePatternOpacity")}
<input type="range" id="wfm-pattern-opacity" min="0.02" max="0.5" step="0.02" value="${settings.themeCustom?.pattern?.opacity ?? 0.1}">
<span id="wfm-pattern-opacity-val">${settings.themeCustom?.pattern?.opacity ?? 0.1}</span>
</label>
<label>
${t("themePatternScale")}
<input type="range" id="wfm-pattern-scale" min="8" max="80" step="2" value="${settings.themeCustom?.pattern?.scale ?? 20}">
<span id="wfm-pattern-scale-val">${settings.themeCustom?.pattern?.scale ?? 20}px</span>
</label>
<label>
${t("themePatternGap")}
<input type="range" id="wfm-pattern-gap" min="0" max="60" step="2" value="${settings.themeCustom?.pattern?.gap ?? 0}">
<span id="wfm-pattern-gap-val">${settings.themeCustom?.pattern?.gap ?? 0}px</span>
</label>
</div>
<div id="wfm-svg-name" style="font-size:11px;color:var(--wfm-text-secondary);margin-top:6px;${settings.themeCustom?.pattern?.id === 'svg' ? '' : 'display:none;'}">
${settings.themeCustom?.pattern?.svgName ? `SVG: ${settings.themeCustom.pattern.svgName}` : ''}
</div>
</div>
<!-- Font -->
<div class="wfm-customizer-section">
<h4>${t("themeCustomizeFont")}</h4>
<div class="wfm-font-grid" id="wfm-font-grid">
${FONT_OPTIONS.map(f => {
const active = (settings.themeCustom?.fontId || "") === f.id ? "active" : "";
return `<div class="wfm-font-item ${active}" data-font-id="${f.id}" title="${f.css || 'System Default'}">
<span class="wfm-font-preview" style="${f.css ? `font-family:${f.css};` : ''}">${t(f.key)}</span>
</div>`;
}).join("")}
</div>
</div>
<!-- Actions -->
<div class="wfm-customizer-actions">
<button class="wfm-btn wfm-btn-primary wfm-btn-sm" id="wfm-customizer-apply">${t("themeCustomizerApply")}</button>
<button class="wfm-btn wfm-btn-sm" id="wfm-customizer-reset">${t("themeCustomizerReset")}</button>
</div>
</div>
</details>
<!-- Workflow Data Folder -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("workflowsDir")}</h3>
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("workflowsDir")}</summary>
<div class="wfm-form-group">
<label>${t("workflowsDirLabel")}</label>
<div style="display:flex;gap:8px;">
@@ -154,11 +488,11 @@ export async function initSettingsTab() {
${t("workflowsDirCurrent")}: <code id="wfm-settings-workflows-dir-current" style="color:var(--wfm-primary);word-break:break-all;">${workflowsDirInfo.current}</code>
</div>
</div>
</div>
</details>
<!-- ComfyUI Settings -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("comfyuiConnection")}</h3>
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("comfyuiConnection")}</summary>
<div class="wfm-form-group">
<label>${t("comfyuiUrl")}</label>
<div style="display:flex;gap:8px;">
@@ -172,11 +506,11 @@ export async function initSettingsTab() {
</small>
</div>
<div id="wfm-settings-url-status" style="font-size:12px;margin-top:4px;"></div>
</div>
</details>
<!-- Ollama Settings -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("ollamaSettings")}</h3>
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("ollamaSettings")}</summary>
<div class="wfm-form-group">
<label>${t("ollamaUrl")}</label>
<input type="text" class="wfm-input" id="wfm-settings-ollama-url"
@@ -197,11 +531,11 @@ export async function initSettingsTab() {
<button class="wfm-btn wfm-btn-sm" id="wfm-settings-ollama-test">${t("testConnection")}</button>
<span id="wfm-settings-ollama-status" style="font-size:12px;line-height:28px;"></span>
</div>
</div>
</details>
<!-- Default Workflow -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("defaultWorkflow")}</h3>
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("defaultWorkflow")}</summary>
<div class="wfm-form-group">
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:13px;" id="wfm-settings-default-wf-name">${settings.defaultWorkflow || t("defaultWorkflowNone")}</span>
@@ -211,11 +545,11 @@ export async function initSettingsTab() {
${t("defaultWorkflowHint")}
</small>
</div>
</div>
</details>
<!-- Eagle Integration (optional) -->
<div style="border:1px solid var(--wfm-border);border-radius:var(--wfm-radius);padding:16px;margin-bottom:20px;">
<h3 style="font-size:15px;margin-bottom:12px;">${t("eagleIntegration")}</h3>
<details class="wfm-settings-section">
<summary class="wfm-settings-summary">${t("eagleIntegration")}</summary>
<div class="wfm-form-group">
<label>${t("eagleUrl")}</label>
<div style="display:flex;gap:8px;">
@@ -233,7 +567,7 @@ export async function initSettingsTab() {
${t("eagleAutoSave")}
</label>
</div>
</div>
</details>
<!-- Save Button -->
<button class="wfm-btn wfm-btn-primary" id="wfm-settings-save" style="min-width:120px;">${t("saveSettings")}</button>
@@ -244,11 +578,193 @@ export async function initSettingsTab() {
const card = e.target.closest(".wfm-theme-card");
if (!card) return;
const themeId = card.dataset.themeId;
// Clear custom overrides when switching theme base
clearCustomOverrides();
const cur = loadLocalSettings();
delete cur.themeCustom;
cur.theme = themeId;
localStorage.setItem(SETTINGS_KEY, JSON.stringify(cur));
applyTheme(themeId);
saveLocalSettings({ theme: themeId });
// Update active state
document.querySelectorAll(".wfm-theme-card").forEach(c => c.classList.remove("active"));
card.classList.add("active");
// Update color pickers to reflect new theme's computed values
requestAnimationFrame(() => {
document.querySelectorAll("#wfm-color-grid input[type=color]").forEach(inp => {
const v = getComputedStyle(document.documentElement).getPropertyValue(inp.dataset.var).trim();
if (v) inp.value = v.startsWith("#") ? v : rgbToHex(v);
});
});
showToast(t("settingsSaved"), "success");
});
// --- Theme Customizer handlers ---
// State for live editing
let customizerState = {
colors: { ...(settings.themeCustom?.colors || {}) },
pattern: { ...(settings.themeCustom?.pattern || { id: "none", color: "#ffffff", opacity: 0.1, scale: 20 }) },
fontId: settings.themeCustom?.fontId || "",
};
// Toggle panel
document.getElementById("wfm-customizer-toggle")?.addEventListener("click", () => {
const toggle = document.getElementById("wfm-customizer-toggle");
const panel = document.getElementById("wfm-customizer-panel");
toggle.classList.toggle("open");
panel.classList.toggle("open");
});
// Live color change
document.getElementById("wfm-color-grid")?.addEventListener("input", (e) => {
if (e.target.type !== "color") return;
const varName = e.target.dataset.var;
const value = e.target.value;
customizerState.colors[varName] = value;
document.documentElement.style.setProperty(varName, value);
});
// Pattern selection
document.getElementById("wfm-pattern-grid")?.addEventListener("click", (e) => {
const item = e.target.closest(".wfm-pattern-item");
if (!item) return;
const patternId = item.dataset.patternId;
// SVG file upload
if (patternId === "svg") {
document.getElementById("wfm-svg-upload")?.click();
return;
}
customizerState.pattern.id = patternId;
// Update active state
document.querySelectorAll(".wfm-pattern-item").forEach(i => i.classList.remove("active"));
item.classList.add("active");
// Show/hide options
const optionsEl = document.getElementById("wfm-pattern-options");
const svgNameEl = document.getElementById("wfm-svg-name");
if (patternId === "none") {
optionsEl.style.display = "none";
svgNameEl.style.display = "none";
} else {
optionsEl.style.display = "";
svgNameEl.style.display = "none";
}
// Live preview
applyCustomOverrides(customizerState);
});
// SVG file upload handler
document.getElementById("wfm-svg-upload")?.addEventListener("change", (e) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const svgData = ev.target.result;
customizerState.pattern.id = "svg";
customizerState.pattern.svgData = svgData;
customizerState.pattern.svgName = file.name;
// Update active state
document.querySelectorAll(".wfm-pattern-item").forEach(i => i.classList.remove("active"));
document.querySelector('.wfm-pattern-item[data-pattern-id="svg"]')?.classList.add("active");
// Show options + name
document.getElementById("wfm-pattern-options").style.display = "";
const svgNameEl = document.getElementById("wfm-svg-name");
svgNameEl.textContent = `SVG: ${file.name}`;
svgNameEl.style.display = "";
applyCustomOverrides(customizerState);
};
reader.readAsText(file);
e.target.value = "";
});
// Pattern option changes (live)
document.getElementById("wfm-pattern-color")?.addEventListener("input", (e) => {
customizerState.pattern.color = e.target.value;
applyCustomOverrides(customizerState);
});
document.getElementById("wfm-pattern-opacity")?.addEventListener("input", (e) => {
const v = parseFloat(e.target.value);
customizerState.pattern.opacity = v;
document.getElementById("wfm-pattern-opacity-val").textContent = v;
applyCustomOverrides(customizerState);
});
document.getElementById("wfm-pattern-scale")?.addEventListener("input", (e) => {
const v = parseInt(e.target.value);
customizerState.pattern.scale = v;
document.getElementById("wfm-pattern-scale-val").textContent = v + "px";
applyCustomOverrides(customizerState);
});
document.getElementById("wfm-pattern-gap")?.addEventListener("input", (e) => {
const v = parseInt(e.target.value);
customizerState.pattern.gap = v;
document.getElementById("wfm-pattern-gap-val").textContent = v + "px";
applyCustomOverrides(customizerState);
});
// Font selection
document.getElementById("wfm-font-grid")?.addEventListener("click", (e) => {
const item = e.target.closest(".wfm-font-item");
if (!item) return;
const fontId = item.dataset.fontId;
customizerState.fontId = fontId;
// Load web font for preview
const font = FONT_OPTIONS.find(f => f.id === fontId);
if (font?.google) loadGoogleFont(font.google);
applyCustomFont(fontId);
// Update active state
document.querySelectorAll(".wfm-font-item").forEach(i => i.classList.remove("active"));
item.classList.add("active");
});
// Apply & Save
document.getElementById("wfm-customizer-apply")?.addEventListener("click", () => {
// Clean empty color overrides
const cleanColors = {};
for (const [k, v] of Object.entries(customizerState.colors)) {
if (v) cleanColors[k] = v;
}
const custom = {
colors: Object.keys(cleanColors).length ? cleanColors : undefined,
pattern: customizerState.pattern.id !== "none" ? { ...customizerState.pattern } : undefined,
fontId: customizerState.fontId || undefined,
};
saveLocalSettings({ themeCustom: (custom.colors || custom.pattern || custom.fontId) ? custom : undefined });
showToast(t("settingsSaved"), "success");
});
// Reset
document.getElementById("wfm-customizer-reset")?.addEventListener("click", () => {
clearCustomOverrides();
customizerState = {
colors: {},
pattern: { id: "none", color: "#ffffff", opacity: 0.1, scale: 20, gap: 0 },
fontId: "",
};
// Remove themeCustom from localStorage directly (spread merge won't delete keys)
const cur = loadLocalSettings();
delete cur.themeCustom;
localStorage.setItem(SETTINGS_KEY, JSON.stringify(cur));
applyTheme(cur.theme || "");
// Reset UI
requestAnimationFrame(() => {
document.querySelectorAll("#wfm-color-grid input[type=color]").forEach(inp => {
const v = getComputedStyle(document.documentElement).getPropertyValue(inp.dataset.var).trim();
if (v) inp.value = v.startsWith("#") ? v : rgbToHex(v);
});
});
document.querySelectorAll(".wfm-pattern-item").forEach(i => i.classList.remove("active"));
document.querySelector('.wfm-pattern-item[data-pattern-id="none"]')?.classList.add("active");
document.getElementById("wfm-pattern-options").style.display = "none";
document.getElementById("wfm-svg-name").style.display = "none";
// Reset font UI
document.querySelectorAll(".wfm-font-item").forEach(i => i.classList.remove("active"));
document.querySelector('.wfm-font-item[data-font-id=""]')?.classList.add("active");
showToast(t("settingsSaved"), "success");
});
+7 -5
View File
@@ -466,11 +466,13 @@
<h3>Settings Tab</h3>
<ul>
<li id="wfm-help-settings-1">UI language and summary language</li>
<li id="wfm-help-settings-2">Workflow data folder path</li>
<li id="wfm-help-settings-3">ComfyUI connection URL and test</li>
<li id="wfm-help-settings-4">Ollama URL, default model, and connection test</li>
<li id="wfm-help-settings-5">Default workflow setting</li>
<li id="wfm-help-settings-6">Eagle integration URL and auto-save toggle</li>
<li id="wfm-help-settings-2">Theme selection (13 presets) with customization: color overrides, background patterns (stripes, polka dot, checkerboard, SVG tiling), and font selection (16 Google Fonts including Japanese display fonts)</li>
<li id="wfm-help-settings-3">Workflow data folder path</li>
<li id="wfm-help-settings-4">ComfyUI connection URL and test</li>
<li id="wfm-help-settings-5">Ollama URL, default model, and connection test</li>
<li id="wfm-help-settings-6">Default workflow setting</li>
<li id="wfm-help-settings-7">Eagle integration URL and auto-save toggle</li>
<li id="wfm-help-settings-8">All sections are collapsible accordion panels</li>
</ul>
</div>
+43 -44
View File
@@ -605,51 +605,50 @@ app.registerExtension({
};
injectStyles();
const replaceButtonIcon = () => {
const wfmButtons = document.querySelectorAll(`button[aria-label="${BUTTON_TOOLTIP}"]`);
wfmButtons.forEach((button) => {
button.classList.add("wfm-top-menu-button");
button.innerHTML = getWfmIcon();
button.style.borderRadius = "4px";
button.style.padding = "6px";
button.style.backgroundColor = "var(--primary-bg)";
const svg = button.querySelector("svg");
if (svg) {
svg.style.width = "20px";
svg.style.height = "20px";
}
});
const snapButtons = document.querySelectorAll(`button[aria-label="${SNAPSHOT_TOOLTIP}"]`);
snapButtons.forEach((button) => {
button.classList.add("wfm-snapshot-button");
button.innerHTML = getSnapshotIcon();
button.style.borderRadius = "4px";
button.style.padding = "6px";
const svg = button.querySelector("svg");
if (svg) {
svg.style.width = "20px";
svg.style.height = "20px";
}
});
const nsButtons = document.querySelectorAll(`button[aria-label="${NODE_SETS_TOOLTIP}"]`);
nsButtons.forEach((button) => {
button.classList.add("wfm-node-sets-btn");
button.innerHTML = getNodeSetsIcon();
button.style.borderRadius = "4px";
button.style.padding = "6px";
const svg = button.querySelector("svg");
if (svg) {
svg.style.width = "20px";
svg.style.height = "20px";
}
});
if (wfmButtons.length === 0) {
requestAnimationFrame(replaceButtonIcon);
const applyButtonIcon = (button, className, getIconFn, extraStyles) => {
if (button.querySelector("svg")) return;
button.classList.add(className);
button.innerHTML = getIconFn();
button.style.borderRadius = "4px";
button.style.padding = "6px";
if (extraStyles) Object.assign(button.style, extraStyles);
const svg = button.querySelector("svg");
if (svg) {
svg.style.width = "20px";
svg.style.height = "20px";
}
};
requestAnimationFrame(replaceButtonIcon);
const buttonConfigs = [
{ tooltip: BUTTON_TOOLTIP, className: "wfm-top-menu-button", getIcon: getWfmIcon, styles: { backgroundColor: "var(--primary-bg)" } },
{ tooltip: SNAPSHOT_TOOLTIP, className: "wfm-snapshot-button", getIcon: getSnapshotIcon, styles: null },
{ tooltip: NODE_SETS_TOOLTIP, className: "wfm-node-sets-btn", getIcon: getNodeSetsIcon, styles: null },
];
const replaceAllIcons = () => {
for (const cfg of buttonConfigs) {
document.querySelectorAll(`button[aria-label="${cfg.tooltip}"]`).forEach((btn) => {
applyButtonIcon(btn, cfg.className, cfg.getIcon, cfg.styles);
});
}
};
const waitAndObserve = () => {
const firstBtn = document.querySelector(`button[aria-label="${BUTTON_TOOLTIP}"]`);
if (!firstBtn) {
requestAnimationFrame(waitAndObserve);
return;
}
replaceAllIcons();
const container = firstBtn.closest(".actionbar-container") || firstBtn.parentElement;
if (container) {
new MutationObserver(() => replaceAllIcons()).observe(container, {
childList: true,
subtree: true,
});
}
};
requestAnimationFrame(waitAndObserve);
},
});