Compare commits
1 Commits
feat/wire-
...
pr/subgrap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fcf5c8348 |
@@ -502,94 +502,11 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
## Files Generated
|
||||
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
|
||||
- \`post-release-checklist.md\` - Follow-up tasks
|
||||
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
|
||||
EOF
|
||||
```
|
||||
|
||||
4. **RELEASE COMPLETION**: All post-release setup completed?
|
||||
|
||||
### Step 16: Generate GTM Feature Summary
|
||||
|
||||
1. **Extract and analyze PR data:**
|
||||
```bash
|
||||
echo "📊 Checking for marketing-worthy features..."
|
||||
|
||||
# Extract all PR data inline
|
||||
PR_DATA=$(
|
||||
PR_LIST=$(git log ${BASE_TAG}..HEAD --grep="Merge pull request" --pretty=format:"%s" | grep -oE "#[0-9]+" | tr -d '#' | sort -u)
|
||||
|
||||
echo "["
|
||||
first=true
|
||||
for PR in $PR_LIST; do
|
||||
[[ "$first" == true ]] && first=false || echo ","
|
||||
gh pr view $PR --json number,title,author,body,files,labels,closedAt 2>/dev/null || continue
|
||||
done
|
||||
echo "]"
|
||||
)
|
||||
|
||||
# Save for analysis
|
||||
echo "$PR_DATA" > prs-${NEW_VERSION}.json
|
||||
```
|
||||
|
||||
2. **Analyze for GTM-worthy features:**
|
||||
```
|
||||
<task>
|
||||
Review these PRs to identify if ANY would interest a marketing/growth team.
|
||||
|
||||
Consider if a PR:
|
||||
- Changes something users directly interact with or experience
|
||||
- Makes something noticeably better, faster, or easier
|
||||
- Introduces capabilities users have been asking for
|
||||
- Has visual assets (screenshots, GIFs, videos) that could be shared
|
||||
- Tells a compelling story about improvement or innovation
|
||||
- Would make users excited if they heard about it
|
||||
|
||||
Many releases contain only technical improvements, bug fixes, or internal changes -
|
||||
that's perfectly normal. Only flag PRs that would genuinely interest end users.
|
||||
|
||||
If you find marketing-worthy PRs, note:
|
||||
- PR number, title, and author
|
||||
- Any media links from the description
|
||||
- One sentence on why it's worth showcasing
|
||||
|
||||
If nothing is marketing-worthy, just say "No marketing-worthy features in this release."
|
||||
</task>
|
||||
|
||||
PR data: [contents of prs-${NEW_VERSION}.json]
|
||||
```
|
||||
|
||||
3. **Generate GTM notification (only if needed):**
|
||||
```
|
||||
If there are marketing-worthy features, create a message for #gtm with:
|
||||
|
||||
🚀 Frontend Release v${NEW_VERSION}
|
||||
|
||||
Timeline: Available now in nightly, ~2-3 weeks for core
|
||||
|
||||
Features worth showcasing:
|
||||
[List the selected PRs with media links and authors]
|
||||
|
||||
Testing: --front-end-version ${NEW_VERSION}
|
||||
|
||||
If there are NO marketing-worthy features, generate:
|
||||
"No marketing-worthy features in v${NEW_VERSION} - mostly internal improvements and bug fixes."
|
||||
```
|
||||
|
||||
4. **Save the output:**
|
||||
```bash
|
||||
# Claude generates the GTM summary and saves it
|
||||
# Save to gtm-summary-${NEW_VERSION}.md
|
||||
|
||||
# Check if notification is needed
|
||||
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
|
||||
echo "✅ No GTM notification needed for this release"
|
||||
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
else
|
||||
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
|
||||
echo "📤 Share this file in #gtm channel to notify the team"
|
||||
fi
|
||||
```
|
||||
|
||||
## Advanced Safety Features
|
||||
|
||||
### Rollback Procedures
|
||||
|
||||
11
.gitignore
vendored
@@ -7,12 +7,6 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Package manager lockfiles (allow users to use different package managers)
|
||||
bun.lock
|
||||
bun.lockb
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
|
||||
# ESLint cache
|
||||
.eslintcache
|
||||
|
||||
@@ -67,8 +61,5 @@ dist.zip
|
||||
# Temporary repository directory
|
||||
templates_repo/
|
||||
|
||||
# Vite's timestamped config modules
|
||||
# Vite’s timestamped config modules
|
||||
vite.config.mts.timestamp-*.mjs
|
||||
|
||||
# Linux core dumps
|
||||
./core
|
||||
|
||||
@@ -50,7 +50,7 @@ export class Topbar {
|
||||
workflowName: string,
|
||||
command: 'Save' | 'Save As' | 'Export'
|
||||
) {
|
||||
await this.triggerTopbarCommand(['File', command])
|
||||
await this.triggerTopbarCommand(['Workflow', command])
|
||||
await this.getSaveDialog().fill(workflowName)
|
||||
await this.page.keyboard.press('Enter')
|
||||
|
||||
@@ -72,8 +72,8 @@ export class Topbar {
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
if (path.length < 1) {
|
||||
throw new Error('Path cannot be empty')
|
||||
if (path.length < 2) {
|
||||
throw new Error('Path is too short')
|
||||
}
|
||||
|
||||
const menu = await this.openTopbarMenu()
|
||||
@@ -85,13 +85,6 @@ export class Topbar {
|
||||
.locator('.p-tieredmenu-item')
|
||||
.filter({ has: topLevelMenuItem })
|
||||
await topLevelMenu.waitFor({ state: 'visible' })
|
||||
|
||||
// Handle top-level commands (like "New")
|
||||
if (path.length === 1) {
|
||||
await topLevelMenuItem.click()
|
||||
return
|
||||
}
|
||||
|
||||
await topLevelMenu.hover()
|
||||
|
||||
let currentMenu = topLevelMenu
|
||||
|
||||
@@ -17,11 +17,11 @@ test.describe('Group Node', () => {
|
||||
await libraryTab.open()
|
||||
})
|
||||
|
||||
test('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||
test.skip('Is added to node library sidebar', async ({ comfyPage }) => {
|
||||
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
|
||||
})
|
||||
|
||||
test('Can be added to canvas using node library sidebar', async ({
|
||||
test.skip('Can be added to canvas using node library sidebar', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const initialNodeCount = await comfyPage.getGraphNodesCount()
|
||||
@@ -34,7 +34,7 @@ test.describe('Group Node', () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
|
||||
})
|
||||
|
||||
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
|
||||
test.skip('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
|
||||
await libraryTab.getFolder(groupNodeCategory).click()
|
||||
await libraryTab
|
||||
.getNode(groupNodeName)
|
||||
@@ -61,7 +61,7 @@ test.describe('Group Node', () => {
|
||||
).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
|
||||
test.skip('Displays preview on bookmark hover', async ({ comfyPage }) => {
|
||||
await libraryTab.getFolder(groupNodeCategory).click()
|
||||
await libraryTab
|
||||
.getNode(groupNodeName)
|
||||
@@ -95,7 +95,7 @@ test.describe('Group Node', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
test.skip('Displays tooltip on title hover', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.convertAllNodesToGroupNode('Group Node')
|
||||
await comfyPage.page.mouse.move(47, 173)
|
||||
@@ -104,7 +104,7 @@ test.describe('Group Node', () => {
|
||||
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Manage group opens with the correct group selected', async ({
|
||||
test.skip('Manage group opens with the correct group selected', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const makeGroup = async (name, type1, type2) => {
|
||||
@@ -165,7 +165,7 @@ test.describe('Group Node', () => {
|
||||
expect(visibleInputCount).toBe(2)
|
||||
})
|
||||
|
||||
test('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
test.skip('Reconnects inputs after configuration changed via manage dialog save', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const expectSingleNode = async (type: string) => {
|
||||
@@ -268,7 +268,10 @@ test.describe('Group Node', () => {
|
||||
await comfyPage.setSetting('Comfy.ConfirmClear', false)
|
||||
|
||||
// Clear workflow
|
||||
await comfyPage.executeCommand('Comfy.ClearWorkflow')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand([
|
||||
'Edit',
|
||||
'Clear Workflow'
|
||||
])
|
||||
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
@@ -277,7 +280,7 @@ test.describe('Group Node', () => {
|
||||
test('Copies and pastes group node into a newly created blank workflow', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.ctrlV()
|
||||
await verifyNodeLoaded(comfyPage, 1)
|
||||
})
|
||||
@@ -293,7 +296,7 @@ test.describe('Group Node', () => {
|
||||
test('Serializes group node after copy and paste across workflows', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.ctrlV()
|
||||
const currentGraphState = await comfyPage.page.evaluate(() =>
|
||||
window['app'].graph.serialize()
|
||||
|
||||
@@ -684,7 +684,7 @@ test.describe('Load workflow', () => {
|
||||
workflowA = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
||||
workflowB = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
||||
|
||||
// Wait for localStorage to persist the workflow paths before reloading
|
||||
|
||||
@@ -75,7 +75,7 @@ test.describe('Menu', () => {
|
||||
|
||||
test('Displays keybinding next to item', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.openTopbarMenu()
|
||||
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('File')
|
||||
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('Workflow')
|
||||
await workflowMenuItem.hover()
|
||||
const exportTag = comfyPage.page.locator('.keybinding-tag', {
|
||||
hasText: 'Ctrl + s'
|
||||
|
||||
@@ -18,7 +18,7 @@ test.describe('Reroute Node', () => {
|
||||
[workflowName]: workflowName
|
||||
})
|
||||
await comfyPage.setup()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
|
||||
// Insert the workflow
|
||||
const workflowsTab = comfyPage.menu.workflowsTab
|
||||
|
||||
@@ -24,11 +24,11 @@ test.describe('Canvas Right Click Menu', () => {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
|
||||
})
|
||||
|
||||
test('Can convert to group node', async ({ comfyPage }) => {
|
||||
test.skip('Can convert to group node', async ({ comfyPage }) => {
|
||||
await comfyPage.select2Nodes()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
|
||||
await comfyPage.rightClickCanvas()
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
|
||||
await comfyPage.clickContextMenuItem('Convert to Group Node')
|
||||
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
|
||||
await comfyPage.page.keyboard.press('Enter')
|
||||
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 100 KiB |
@@ -63,7 +63,7 @@ test.describe('Workflow Tab Thumbnails', () => {
|
||||
test('Should show thumbnail when hovering over a non-active tab', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
const thumbnailImg = await getTabThumbnailImage(
|
||||
comfyPage,
|
||||
0,
|
||||
@@ -73,7 +73,7 @@ test.describe('Workflow Tab Thumbnails', () => {
|
||||
})
|
||||
|
||||
test('Should not show thumbnail for active tab', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
const thumbnailImg = await getTabThumbnailImage(
|
||||
comfyPage,
|
||||
1,
|
||||
@@ -105,7 +105,7 @@ test.describe('Workflow Tab Thumbnails', () => {
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Create a new workflow (tab 1) which will be empty
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Now we have two tabs: tab 0 (default workflow with nodes) and tab 1 (empty)
|
||||
|
||||
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.91396 12.7428L5.41396 10.7428C5.57175 10.1116 5.09439 9.50024 4.44382 9.50024H2.50538C2.04651 9.50024 1.64652 9.81253 1.53523 10.2577L1.03523 12.2577C0.877446 12.8888 1.3548 13.5002 2.00538 13.5002H3.94382C4.40269 13.5002 4.80267 13.1879 4.91396 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5.91396 6.74277L6.41396 4.74277C6.57175 4.11163 6.09439 3.50024 5.44382 3.50024H3.50538C3.04651 3.50024 2.64652 3.81253 2.53523 4.2577L2.03523 6.2577C1.87745 6.88885 2.3548 7.50024 3.00538 7.50024H4.94382C5.40269 7.50024 5.80267 7.18794 5.91396 6.74277Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M10.914 12.7428L11.414 10.7428C11.5718 10.1116 11.0944 9.50024 10.4438 9.50024H8.50538C8.04651 9.50024 7.64652 9.81253 7.53523 10.2577L7.03523 12.2577C6.87745 12.8888 7.3548 13.5002 8.00538 13.5002H9.94382C10.4027 13.5002 10.8027 13.1879 10.914 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M12.2342 5.46739L11.5287 7.11354C11.4248 7.35597 11.0811 7.35597 10.9772 7.11354L10.2717 5.46739C10.2414 5.39659 10.185 5.34017 10.1141 5.30983L8.468 4.60433C8.22557 4.50044 8.22557 4.15675 8.468 4.05285L10.1141 3.34736C10.185 3.31701 10.2414 3.26059 10.2717 3.18979L10.9772 1.54364C11.0811 1.30121 11.4248 1.30121 11.5287 1.54364L12.2342 3.18979C12.2645 3.26059 12.3209 3.31701 12.3918 3.34736L14.0379 4.05285C14.2803 4.15675 14.2803 4.50044 14.0379 4.60433L12.3918 5.30983C12.3209 5.34017 12.2645 5.39659 12.2342 5.46739Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6667 10L10.598 10.2577C10.4812 10.6954 10.0848 11 9.63172 11H5.30161C4.64458 11 4.16608 10.3772 4.33538 9.74234L5.40204 5.74234C5.51878 5.30458 5.91523 5 6.36828 5H10.8286C11.4199 5 11.8505 5.56051 11.6982 6.13185L11.6736 6.22389M14 8H10M4.5 8H2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 405 B |
@@ -1,5 +0,0 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.1894 6.24254L13.6894 4.24254C13.8471 3.61139 13.3698 3 12.7192 3H3.78077C3.3219 3 2.92192 3.3123 2.81062 3.75746L2.31062 5.75746C2.15284 6.38861 2.63019 7 3.28077 7H12.2192C12.6781 7 13.0781 6.6877 13.1894 6.24254Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M13.1894 12.2425L13.6894 10.2425C13.8471 9.61139 13.3698 9 12.7192 9H8.78077C8.3219 9 7.92192 9.3123 7.81062 9.75746L7.31062 11.7575C7.15284 12.3886 7.6302 13 8.28077 13H12.2192C12.6781 13 13.0781 12.6877 13.1894 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5.18936 12.2425L5.68936 10.2425C5.84714 9.61139 5.36978 9 4.71921 9H3.78077C3.3219 9 2.92192 9.3123 2.81062 9.75746L2.31062 11.7575C2.15284 12.3886 2.6302 13 3.28077 13H4.21921C4.67808 13 5.07806 12.6877 5.18936 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 970 B |
@@ -34,7 +34,7 @@ import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphGroup, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
@@ -48,7 +48,25 @@ const isUnpackVisible = computed(() => {
|
||||
canvasStore.selectedItems[0] instanceof SubgraphNode
|
||||
)
|
||||
})
|
||||
|
||||
const isConvertVisible = computed(() => {
|
||||
const items = canvasStore.selectedItems
|
||||
if (!items || items.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Unpack button takes precedence for single subgraph node
|
||||
if (items.length === 1 && items[0] instanceof SubgraphNode) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Hide if ALL selected items are groups
|
||||
const allAreGroups = items.every((item) => item instanceof LGraphGroup)
|
||||
if (allAreGroups) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Otherwise, show it, assuming there's some selection
|
||||
return (
|
||||
canvasStore.groupSelected ||
|
||||
canvasStore.rerouteSelected ||
|
||||
|
||||
@@ -8,13 +8,10 @@
|
||||
:icon-badge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip-suffix="getTabTooltipSuffix(tab)"
|
||||
:label="tab.label || tab.title"
|
||||
:is-small="isSmall"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<SidebarTemplatesButton />
|
||||
<div class="side-tool-bar-end">
|
||||
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
||||
<SidebarHelpCenterIcon />
|
||||
@@ -46,7 +43,6 @@ import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
import SidebarLogoutIcon from './SidebarLogoutIcon.vue'
|
||||
import SidebarTemplatesButton from './SidebarTemplatesButton.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -90,7 +86,7 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
box-shadow: var(--bar-shadow);
|
||||
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
--sidebar-icon-size: 1.5rem;
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
|
||||
@@ -58,12 +58,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import ReleaseNotificationToast from '@/components/helpcenter/ReleaseNotificationToast.vue'
|
||||
import WhatsNewPopup from '@/components/helpcenter/WhatsNewPopup.vue'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useReleaseStore } from '@/stores/releaseStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
@@ -71,9 +70,8 @@ import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const releaseStore = useReleaseStore()
|
||||
const helpCenterStore = useHelpCenterStore()
|
||||
const { shouldShowRedDot } = storeToRefs(releaseStore)
|
||||
const { isVisible: isHelpCenterVisible } = storeToRefs(helpCenterStore)
|
||||
const isHelpCenterVisible = ref(false)
|
||||
|
||||
const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
@@ -82,11 +80,11 @@ const sidebarLocation = computed(() =>
|
||||
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
|
||||
|
||||
const toggleHelpCenter = () => {
|
||||
helpCenterStore.toggle()
|
||||
isHelpCenterVisible.value = !isHelpCenterVisible.value
|
||||
}
|
||||
|
||||
const closeHelpCenter = () => {
|
||||
helpCenterStore.hide()
|
||||
isHelpCenterVisible.value = false
|
||||
}
|
||||
|
||||
// Initialize release store on mount
|
||||
@@ -132,7 +130,6 @@ onMounted(async () => {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
|
||||
@@ -19,29 +19,12 @@
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="side-bar-button-content">
|
||||
<slot name="icon">
|
||||
<OverlayBadge v-if="shouldShowBadge" :value="overlayValue">
|
||||
<i
|
||||
v-if="typeof icon === 'string'"
|
||||
:class="icon + ' side-bar-button-icon'"
|
||||
/>
|
||||
<component :is="icon" v-else class="side-bar-button-icon" />
|
||||
</OverlayBadge>
|
||||
<i
|
||||
v-else-if="typeof icon === 'string'"
|
||||
:class="icon + ' side-bar-button-icon'"
|
||||
/>
|
||||
<component
|
||||
:is="icon"
|
||||
v-else-if="typeof icon === 'object'"
|
||||
class="side-bar-button-icon"
|
||||
/>
|
||||
</slot>
|
||||
<span v-if="label && !isSmall" class="side-bar-button-label">{{
|
||||
t(label)
|
||||
}}</span>
|
||||
</div>
|
||||
<slot name="icon">
|
||||
<OverlayBadge v-if="shouldShowBadge" :value="overlayValue">
|
||||
<i :class="icon + ' side-bar-button-icon'" />
|
||||
</OverlayBadge>
|
||||
<i v-else :class="icon + ' side-bar-button-icon'" />
|
||||
</slot>
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
@@ -50,7 +33,6 @@
|
||||
import Button from 'primevue/button'
|
||||
import OverlayBadge from 'primevue/overlaybadge'
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -59,17 +41,13 @@ const {
|
||||
selected = false,
|
||||
tooltip = '',
|
||||
tooltipSuffix = '',
|
||||
iconBadge = '',
|
||||
label = '',
|
||||
isSmall = false
|
||||
iconBadge = ''
|
||||
} = defineProps<{
|
||||
icon?: string | Component
|
||||
icon?: string
|
||||
selected?: boolean
|
||||
tooltip?: string
|
||||
tooltipSuffix?: string
|
||||
iconBadge?: string | (() => string | null)
|
||||
label?: string
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -96,21 +74,8 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
|
||||
<style scoped>
|
||||
.side-bar-button {
|
||||
width: var(--sidebar-width);
|
||||
height: calc(var(--sidebar-width) + 0.5rem);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.side-tool-bar-end .side-bar-button {
|
||||
height: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.side-bar-button-content {
|
||||
@apply flex flex-col items-center gap-2;
|
||||
}
|
||||
|
||||
.side-bar-button-label {
|
||||
@apply text-[10px] text-center whitespace-nowrap;
|
||||
line-height: 1;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.comfyui-body-left .side-bar-button.side-bar-button-selected,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
:icon="TemplateIcon"
|
||||
:tooltip="$t('sideToolbar.templates')"
|
||||
:label="$t('sideToolbar.labels.templates')"
|
||||
:is-small="isSmall"
|
||||
class="templates-tab-button"
|
||||
@click="openTemplates"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
|
||||
// Import the custom template icon
|
||||
const TemplateIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/template'))
|
||||
)
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const isSmall = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
|
||||
)
|
||||
|
||||
const openTemplates = () => {
|
||||
void commandStore.execute('Comfy.BrowseTemplates')
|
||||
}
|
||||
</script>
|
||||
@@ -30,17 +30,10 @@
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.resetView')"
|
||||
icon="pi pi-filter-slash"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="resetOrganization"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.refresh')"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="() => commandStore.execute('Comfy.RefreshNodeDefinitions')"
|
||||
@click="resetOrganization"
|
||||
/>
|
||||
<Popover ref="groupingPopover">
|
||||
<div class="flex flex-col gap-1 p-2">
|
||||
@@ -146,7 +139,6 @@ import {
|
||||
DEFAULT_SORTING_ID,
|
||||
nodeOrganizationService
|
||||
} from '@/services/nodeOrganizationService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
|
||||
import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
@@ -163,7 +155,6 @@ import NodeBookmarkTreeExplorer from './nodeLibrary/NodeBookmarkTreeExplorer.vue
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeBookmarkStore = useNodeBookmarkStore()
|
||||
const nodeHelpStore = useNodeHelpStore()
|
||||
const commandStore = useCommandStore()
|
||||
const expandedKeys = ref<Record<string, boolean>>({})
|
||||
const { expandNode, toggleNodeOnEvent } = useTreeExpansion(expandedKeys)
|
||||
|
||||
|
||||
@@ -55,30 +55,9 @@
|
||||
v-bind="props.action"
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
:class="typeof item.class === 'function' ? item.class() : item.class"
|
||||
@mousedown="
|
||||
isZoomCommand(item) ? handleZoomMouseDown(item, $event) : undefined
|
||||
"
|
||||
@click="isZoomCommand(item) ? handleZoomClick($event) : undefined"
|
||||
>
|
||||
<i
|
||||
v-if="hasActiveStateSiblings(item)"
|
||||
class="p-menubar-item-icon pi pi-check text-sm"
|
||||
:class="{ invisible: !item.comfyCommand?.active?.() }"
|
||||
/>
|
||||
<span
|
||||
v-else-if="
|
||||
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
|
||||
"
|
||||
class="p-menubar-item-icon"
|
||||
:class="item.icon"
|
||||
/>
|
||||
<span v-if="item.icon" class="p-menubar-item-icon" :class="item.icon" />
|
||||
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
|
||||
<i
|
||||
v-if="item.comfyCommand?.id === 'Comfy.NewBlankWorkflow'"
|
||||
class="ml-auto"
|
||||
:class="item.icon"
|
||||
/>
|
||||
<span
|
||||
v-if="item?.comfyCommand?.keybinding"
|
||||
class="ml-auto border border-surface rounded text-muted text-xs text-nowrap p-1 keybinding-tag"
|
||||
@@ -115,7 +94,6 @@ import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
@@ -185,22 +163,16 @@ const extraMenuItems: MenuItem[] = [
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
key: 'browse-templates',
|
||||
label: t('menuLabels.Browse Templates'),
|
||||
icon: 'pi pi-folder-open',
|
||||
command: () => commandStore.execute('Comfy.BrowseTemplates')
|
||||
key: 'manage-extensions',
|
||||
label: t('menu.manageExtensions'),
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
command: showManageExtensions
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: t('g.settings'),
|
||||
icon: 'mdi mdi-cog-outline',
|
||||
command: () => showSettings()
|
||||
},
|
||||
{
|
||||
key: 'manage-extensions',
|
||||
label: t('menu.manageExtensions'),
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
command: showManageExtensions
|
||||
}
|
||||
]
|
||||
|
||||
@@ -265,36 +237,6 @@ const onMenuShow = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isZoomCommand = (item: MenuItem) => {
|
||||
return (
|
||||
item.comfyCommand?.id === 'Comfy.Canvas.ZoomIn' ||
|
||||
item.comfyCommand?.id === 'Comfy.Canvas.ZoomOut'
|
||||
)
|
||||
}
|
||||
|
||||
const handleZoomMouseDown = (item: MenuItem, event: MouseEvent) => {
|
||||
if (item.comfyCommand) {
|
||||
whileMouseDown(
|
||||
event,
|
||||
async () => {
|
||||
await commandStore.execute(item.comfyCommand!.id)
|
||||
},
|
||||
50
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleZoomClick = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// Prevent the menu from closing for zoom commands
|
||||
return false
|
||||
}
|
||||
|
||||
const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
return menuItemsStore.menuItemHasActiveStateChildren[item.parentPath]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -261,10 +261,7 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
return '$0.14-2.80/Run (varies with model, mode & duration)'
|
||||
|
||||
const modelValue = String(modelWidget.value)
|
||||
if (
|
||||
modelValue.includes('v2-1-master') ||
|
||||
modelValue.includes('v2-master')
|
||||
) {
|
||||
if (modelValue.includes('v2-master')) {
|
||||
return '$1.40/Run'
|
||||
} else if (
|
||||
modelValue.includes('v1-6') ||
|
||||
@@ -283,19 +280,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
console.log('durationValue', durationValue)
|
||||
|
||||
// Same pricing matrix as KlingTextToVideoNode
|
||||
if (
|
||||
modelValue.includes('v2-1-master') ||
|
||||
modelValue.includes('v2-master')
|
||||
) {
|
||||
if (modelValue.includes('v2-master')) {
|
||||
if (durationValue.includes('10')) {
|
||||
return '$2.80/Run'
|
||||
}
|
||||
return '$1.40/Run' // 5s default
|
||||
} else if (
|
||||
modelValue.includes('v2-1') ||
|
||||
modelValue.includes('v1-6') ||
|
||||
modelValue.includes('v1-5')
|
||||
) {
|
||||
} else if (modelValue.includes('v1-6') || modelValue.includes('v1-5')) {
|
||||
if (modeValue.includes('pro')) {
|
||||
return durationValue.includes('10') ? '$0.98/Run' : '$0.49/Run'
|
||||
} else {
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
|
||||
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const AiModelIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/ai-model'))
|
||||
)
|
||||
|
||||
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'model-library',
|
||||
icon: AiModelIcon,
|
||||
icon: 'pi pi-box',
|
||||
title: 'sideToolbar.modelLibrary',
|
||||
tooltip: 'sideToolbar.modelLibrary',
|
||||
label: 'sideToolbar.labels.models',
|
||||
component: markRaw(ModelLibrarySidebarTab),
|
||||
type: 'vue',
|
||||
iconBadge: () => {
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
const NodeIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/node'))
|
||||
)
|
||||
|
||||
export const useNodeLibrarySidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'node-library',
|
||||
icon: NodeIcon,
|
||||
icon: 'pi pi-book',
|
||||
title: 'sideToolbar.nodeLibrary',
|
||||
tooltip: 'sideToolbar.nodeLibrary',
|
||||
label: 'sideToolbar.labels.nodes',
|
||||
component: markRaw(NodeLibrarySidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export const useQueueSidebarTab = (): SidebarTabExtension => {
|
||||
},
|
||||
title: 'sideToolbar.queue',
|
||||
tooltip: 'sideToolbar.queue',
|
||||
label: 'sideToolbar.labels.queue',
|
||||
component: markRaw(QueueSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
const WorkflowIcon = markRaw(
|
||||
defineAsyncComponent(() => import('virtual:icons/comfy/workflow'))
|
||||
)
|
||||
|
||||
export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
const settingStore = useSettingStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
return {
|
||||
id: 'workflows',
|
||||
icon: WorkflowIcon,
|
||||
icon: 'pi pi-folder-open',
|
||||
iconBadge: () => {
|
||||
if (
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'
|
||||
@@ -26,7 +22,6 @@ export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
|
||||
},
|
||||
title: 'sideToolbar.workflows',
|
||||
tooltip: 'sideToolbar.workflows',
|
||||
label: 'sideToolbar.labels.workflows',
|
||||
component: markRaw(WorkflowsSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useWorkflowService } from '@/services/workflowService'
|
||||
import type { ComfyCommand } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
|
||||
import { useHelpCenterStore } from '@/stores/helpCenterStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -279,7 +278,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Comfy.Canvas.FitView',
|
||||
icon: 'pi pi-expand',
|
||||
label: 'Fit view to selected nodes',
|
||||
menubarLabel: 'Zoom to fit',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
if (app.canvas.empty) {
|
||||
@@ -305,7 +303,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Comfy.Canvas.ToggleLinkVisibility',
|
||||
icon: 'pi pi-eye',
|
||||
label: 'Canvas Toggle Link Visibility',
|
||||
menubarLabel: 'Node Links',
|
||||
versionAdded: '1.3.6',
|
||||
|
||||
function: (() => {
|
||||
@@ -327,15 +324,12 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
)
|
||||
}
|
||||
}
|
||||
})(),
|
||||
active: () =>
|
||||
useSettingStore().get('Comfy.LinkRenderMode') !== LiteGraph.HIDDEN_LINK
|
||||
})()
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleMinimap',
|
||||
icon: 'pi pi-map',
|
||||
label: 'Canvas Toggle Minimap',
|
||||
menubarLabel: 'Minimap',
|
||||
versionAdded: '1.24.1',
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
@@ -343,8 +337,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
'Comfy.Minimap.Visible',
|
||||
!settingStore.get('Comfy.Minimap.Visible')
|
||||
)
|
||||
},
|
||||
active: () => useSettingStore().get('Comfy.Minimap.Visible')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.QueuePrompt',
|
||||
@@ -548,25 +541,21 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
id: 'Workspace.ToggleBottomPanel',
|
||||
icon: 'pi pi-list',
|
||||
label: 'Toggle Bottom Panel',
|
||||
menubarLabel: 'Bottom Panel',
|
||||
versionAdded: '1.3.22',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
bottomPanelStore.toggleBottomPanel()
|
||||
},
|
||||
active: () => bottomPanelStore.bottomPanelVisible
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Workspace.ToggleFocusMode',
|
||||
icon: 'pi pi-eye',
|
||||
label: 'Toggle Focus Mode',
|
||||
menubarLabel: 'Focus Mode',
|
||||
versionAdded: '1.3.27',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
useWorkspaceStore().toggleFocusMode()
|
||||
},
|
||||
active: () => useWorkspaceStore().focusMode
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Graph.FitGroupToContents',
|
||||
@@ -807,7 +796,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -826,34 +814,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.OpenManagerDialog',
|
||||
icon: 'mdi mdi-puzzle-outline',
|
||||
label: 'Manager',
|
||||
function: () => {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleHelpCenter',
|
||||
icon: 'pi pi-question-circle',
|
||||
label: 'Help Center',
|
||||
function: () => {
|
||||
useHelpCenterStore().toggle()
|
||||
},
|
||||
active: () => useHelpCenterStore().isVisible
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleCanvasInfo',
|
||||
icon: 'pi pi-info-circle',
|
||||
label: 'Canvas Performance',
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
const currentValue = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
await settingStore.set('Comfy.Graph.CanvasInfo', !currentValue)
|
||||
},
|
||||
active: () => useSettingStore().get('Comfy.Graph.CanvasInfo')
|
||||
},
|
||||
{
|
||||
id: 'Workspace.ToggleBottomPanel.Shortcuts',
|
||||
icon: 'pi pi-key',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { whenever } from '@vueuse/core'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { useCivitaiModel } from '@/composables/useCivitaiModel'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { downloadUrlToHfRepoUrl, isCivitaiModelUrl } from '@/utils/formatUtil'
|
||||
|
||||
export function useDownload(url: string, fileName?: string) {
|
||||
@@ -15,7 +14,7 @@ export function useDownload(url: string, fileName?: string) {
|
||||
|
||||
const fetchFileSize = async () => {
|
||||
try {
|
||||
const response = await fetchWithHeaders(url, { method: 'HEAD' })
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
if (!response.ok) throw new Error('Failed to fetch file size')
|
||||
|
||||
const size = response.headers.get('content-length')
|
||||
|
||||
@@ -19,7 +19,6 @@ export const useLitegraphSettings = () => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
if (canvasStore.canvas) {
|
||||
canvasStore.canvas.show_info = canvasInfoEnabled
|
||||
canvasStore.canvas.draw(false, true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -102,9 +102,6 @@ export function useMinimap() {
|
||||
const groupColor = computed(() =>
|
||||
isLightTheme.value ? '#A2D3EC' : '#1F547A'
|
||||
)
|
||||
const groupColorDefault = computed(
|
||||
() => (isLightTheme.value ? '#283640' : '#B3C1CB') // this is the default group color when using nodeColors setting
|
||||
)
|
||||
const bypassColor = computed(() =>
|
||||
isLightTheme.value ? '#DBDBDB' : '#4B184B'
|
||||
)
|
||||
@@ -252,17 +249,7 @@ export function useMinimap() {
|
||||
const w = group.size[0] * scale.value
|
||||
const h = group.size[1] * scale.value
|
||||
|
||||
let color = groupColor.value
|
||||
|
||||
if (nodeColors.value) {
|
||||
color = group.color ?? groupColorDefault.value
|
||||
|
||||
if (isLightTheme.value) {
|
||||
color = adjustColor(color, { opacity: 0.5 })
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = color
|
||||
ctx.fillStyle = groupColor.value
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
|
||||
import type {
|
||||
@@ -161,17 +160,12 @@ export function useTemplateWorkflows() {
|
||||
*/
|
||||
const fetchTemplateJson = async (id: string, sourceModule: string) => {
|
||||
if (sourceModule === 'default') {
|
||||
// Default templates provided by frontend are served as static files
|
||||
const response = await fetchWithHeaders(
|
||||
api.fileURL(`/templates/${id}.json`)
|
||||
)
|
||||
return await response.json()
|
||||
// Default templates provided by frontend are served on this separate endpoint
|
||||
return fetch(api.fileURL(`/templates/${id}.json`)).then((r) => r.json())
|
||||
} else {
|
||||
// Custom node templates served via API
|
||||
const response = await api.fetchApi(
|
||||
`/workflow_templates/${sourceModule}/${id}.json`
|
||||
)
|
||||
return await response.json()
|
||||
return fetch(
|
||||
api.apiURL(`/workflow_templates/${sourceModule}/${id}.json`)
|
||||
).then((r) => r.json())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { IWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { createAxiosWithHeaders } from '@/services/networkClientAdapter'
|
||||
|
||||
const MAX_RETRIES = 5
|
||||
const TIMEOUT = 4096
|
||||
|
||||
// Create axios client with header injection
|
||||
const axiosClient = createAxiosWithHeaders()
|
||||
|
||||
export interface CacheEntry<T> {
|
||||
data: T
|
||||
timestamp?: number
|
||||
@@ -60,7 +58,7 @@ const fetchData = async (
|
||||
controller: AbortController
|
||||
) => {
|
||||
const { route, response_key, query_params, timeout = TIMEOUT } = config
|
||||
const res = await axiosClient.get(route, {
|
||||
const res = await axios.get(route, {
|
||||
params: query_params,
|
||||
signal: controller.signal,
|
||||
timeout
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
export const CORE_MENU_COMMANDS = [
|
||||
[[], ['Comfy.NewBlankWorkflow']],
|
||||
[[], []], // Separator after New
|
||||
[['File'], ['Comfy.OpenWorkflow']],
|
||||
[['Workflow'], ['Comfy.NewBlankWorkflow']],
|
||||
[['Workflow'], ['Comfy.OpenWorkflow', 'Comfy.BrowseTemplates']],
|
||||
[
|
||||
['File'],
|
||||
['Workflow'],
|
||||
[
|
||||
'Comfy.SaveWorkflow',
|
||||
'Comfy.SaveWorkflowAs',
|
||||
@@ -12,6 +11,8 @@ export const CORE_MENU_COMMANDS = [
|
||||
]
|
||||
],
|
||||
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
|
||||
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
|
||||
[['Edit'], ['Comfy.ClearWorkflow']],
|
||||
[['Edit'], ['Comfy.OpenClipspace']],
|
||||
[
|
||||
['Help'],
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* Example showing how authentication headers are automatically injected
|
||||
* with the new header registration system.
|
||||
*
|
||||
* Before: Services had to manually retrieve and add auth headers
|
||||
* After: Headers are automatically injected via the network adapters
|
||||
*/
|
||||
import {
|
||||
createAxiosWithHeaders,
|
||||
fetchWithHeaders
|
||||
} from '@/services/networkClientAdapter'
|
||||
|
||||
// ============================================
|
||||
// BEFORE: Manual header management
|
||||
// ============================================
|
||||
|
||||
// This is how services used to handle auth headers:
|
||||
/*
|
||||
import axios from 'axios'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
export async function oldWayToMakeRequest() {
|
||||
// Had to manually get auth headers
|
||||
const authHeaders = await useFirebaseAuthStore().getAuthHeader()
|
||||
|
||||
if (!authHeaders) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
|
||||
// Had to manually add headers to each request
|
||||
const response = await axios.get('/api/data', {
|
||||
headers: {
|
||||
...authHeaders,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// AFTER: Automatic header injection
|
||||
// ============================================
|
||||
|
||||
// With the new system, auth headers are automatically injected:
|
||||
|
||||
/**
|
||||
* Example 1: Using fetchWithHeaders
|
||||
* Headers are automatically injected - no manual auth handling needed
|
||||
*/
|
||||
export async function modernFetchExample() {
|
||||
// Just make the request - auth headers are added automatically!
|
||||
const response = await fetchWithHeaders('/api/data', {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
// Auth headers are automatically added by the AuthHeaderProvider
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 2: Using createAxiosWithHeaders
|
||||
* Create an axios client that automatically injects headers
|
||||
*/
|
||||
export function createModernApiClient() {
|
||||
// Create a client with automatic header injection
|
||||
const client = createAxiosWithHeaders({
|
||||
baseURL: '/api',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
return {
|
||||
async getData() {
|
||||
// No need to manually add auth headers!
|
||||
const response = await client.get('/data')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async postData(data: any) {
|
||||
// Auth headers are automatically included
|
||||
const response = await client.post('/data', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async updateData(id: string, data: any) {
|
||||
// Works with all HTTP methods
|
||||
const response = await client.put(`/data/${id}`, data)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 3: Real-world service refactoring
|
||||
* Shows how to update an existing service to use automatic headers
|
||||
*/
|
||||
|
||||
// Before: CustomerEventsService with manual auth
|
||||
/*
|
||||
class OldCustomerEventsService {
|
||||
private async makeRequest(url: string) {
|
||||
const authHeaders = await useFirebaseAuthStore().getAuthHeader()
|
||||
if (!authHeaders) {
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
|
||||
return axios.get(url, { headers: authHeaders })
|
||||
}
|
||||
|
||||
async getEvents() {
|
||||
return this.makeRequest('/customers/events')
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// After: CustomerEventsService with automatic auth
|
||||
class ModernCustomerEventsService {
|
||||
private client = createAxiosWithHeaders({
|
||||
baseURL: '/api'
|
||||
})
|
||||
|
||||
async getEvents() {
|
||||
// Auth headers are automatically included!
|
||||
const response = await this.client.get('/customers/events')
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getEventDetails(eventId: string) {
|
||||
// No manual auth handling needed
|
||||
const response = await this.client.get(`/customers/events/${eventId}`)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Benefits of the new system:
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 1. Cleaner code - no auth header boilerplate
|
||||
* 2. Consistent auth handling across all services
|
||||
* 3. Automatic token refresh (handled by Firebase SDK)
|
||||
* 4. Fallback to API key when Firebase auth unavailable
|
||||
* 5. Easy to add new header providers (debug headers, etc.)
|
||||
* 6. Headers can be conditionally applied based on URL/method
|
||||
* 7. Priority system allows overriding headers when needed
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// How it works behind the scenes:
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 1. During app initialization (preInit hook), the AuthHeadersExtension
|
||||
* registers the AuthHeaderProvider with the header registry
|
||||
*
|
||||
* 2. When you use fetchWithHeaders or createAxiosWithHeaders, they
|
||||
* automatically query the header registry for all registered providers
|
||||
*
|
||||
* 3. The AuthHeaderProvider checks for Firebase token first, then
|
||||
* falls back to API key if needed
|
||||
*
|
||||
* 4. Headers are merged and added to the request automatically
|
||||
*
|
||||
* 5. If authentication fails, the request proceeds without auth headers
|
||||
* (the backend will handle the 401/403 response)
|
||||
*/
|
||||
|
||||
export const examples = {
|
||||
modernFetchExample,
|
||||
createModernApiClient,
|
||||
ModernCustomerEventsService
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/**
|
||||
* Example of how extensions can register header providers
|
||||
* This file demonstrates the header registration API for extension developers
|
||||
*/
|
||||
import { headerRegistry } from '@/services/headerRegistry'
|
||||
import type {
|
||||
HeaderMap,
|
||||
HeaderProviderContext,
|
||||
IHeaderProvider
|
||||
} from '@/types/headerTypes'
|
||||
|
||||
/**
|
||||
* Example 1: Simple static header provider
|
||||
*/
|
||||
class StaticHeaderProvider implements IHeaderProvider {
|
||||
provideHeaders(_context: HeaderProviderContext): HeaderMap {
|
||||
return {
|
||||
'X-Extension-Name': 'my-extension',
|
||||
'X-Extension-Version': '1.0.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 2: Dynamic header provider that adds headers based on the request
|
||||
*/
|
||||
class DynamicHeaderProvider implements IHeaderProvider {
|
||||
async provideHeaders(context: HeaderProviderContext): Promise<HeaderMap> {
|
||||
const headers: HeaderMap = {}
|
||||
|
||||
// Add different headers based on the URL
|
||||
if (context.url.includes('/api/')) {
|
||||
headers['X-API-Version'] = 'v2'
|
||||
}
|
||||
|
||||
// Add headers based on request method
|
||||
if (context.method === 'POST' || context.method === 'PUT') {
|
||||
headers['X-Request-ID'] = () => crypto.randomUUID()
|
||||
}
|
||||
|
||||
// Add timestamp header
|
||||
headers['X-Timestamp'] = () => new Date().toISOString()
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 3: Auth token provider
|
||||
*/
|
||||
class AuthTokenProvider implements IHeaderProvider {
|
||||
private getToken(): string | null {
|
||||
// This could retrieve a token from storage, state, etc.
|
||||
return localStorage.getItem('auth-token')
|
||||
}
|
||||
|
||||
provideHeaders(_context: HeaderProviderContext): HeaderMap {
|
||||
const token = this.getToken()
|
||||
|
||||
if (token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of how to register providers in an extension
|
||||
*/
|
||||
export function setupHeaderProviders() {
|
||||
// Register a simple static provider
|
||||
const staticRegistration = headerRegistry.registerHeaderProvider(
|
||||
new StaticHeaderProvider()
|
||||
)
|
||||
|
||||
// Register a dynamic provider with higher priority
|
||||
const dynamicRegistration = headerRegistry.registerHeaderProvider(
|
||||
new DynamicHeaderProvider(),
|
||||
{ priority: 10 }
|
||||
)
|
||||
|
||||
// Register an auth provider that only applies to API endpoints
|
||||
const authRegistration = headerRegistry.registerHeaderProvider(
|
||||
new AuthTokenProvider(),
|
||||
{
|
||||
priority: 20, // Higher priority to override other auth headers
|
||||
filter: (context) => context.url.includes('/api/')
|
||||
}
|
||||
)
|
||||
|
||||
// Return cleanup function for when extension is unloaded
|
||||
return () => {
|
||||
staticRegistration.dispose()
|
||||
dynamicRegistration.dispose()
|
||||
authRegistration.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example of a provider that integrates with a cloud service
|
||||
*/
|
||||
export class CloudServiceHeaderProvider implements IHeaderProvider {
|
||||
constructor(
|
||||
private apiKey: string,
|
||||
private workspaceId: string
|
||||
) {}
|
||||
|
||||
async provideHeaders(context: HeaderProviderContext): Promise<HeaderMap> {
|
||||
// Only add headers for requests to the cloud service
|
||||
if (!context.url.includes('cloud.comfyui.com')) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
'X-API-Key': this.apiKey,
|
||||
'X-Workspace-ID': this.workspaceId,
|
||||
'X-Client-Version': '1.0.0',
|
||||
'X-Session-ID': async () => {
|
||||
// Could fetch or generate session ID asynchronously
|
||||
const sessionId = await this.getOrCreateSessionId()
|
||||
return sessionId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getOrCreateSessionId(): Promise<string> {
|
||||
// Simulate async session creation
|
||||
return 'session-' + Date.now()
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
import { headerRegistry } from '@/services/headerRegistry'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import type {
|
||||
HeaderMap,
|
||||
HeaderProviderContext,
|
||||
IHeaderProvider
|
||||
} from '@/types/headerTypes'
|
||||
|
||||
/**
|
||||
* Example extension showing how to register header providers
|
||||
* during the pre-init lifecycle hook.
|
||||
*
|
||||
* The pre-init hook is the earliest extension lifecycle hook,
|
||||
* called before the canvas is created. This makes it perfect
|
||||
* for registering cross-cutting concerns like header providers.
|
||||
*/
|
||||
|
||||
// Example: Authentication token provider
|
||||
class AuthTokenProvider implements IHeaderProvider {
|
||||
async provideHeaders(_context: HeaderProviderContext): Promise<HeaderMap> {
|
||||
// This could fetch tokens from a secure store, refresh them, etc.
|
||||
const token = await this.getAuthToken()
|
||||
|
||||
if (token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
private async getAuthToken(): Promise<string | null> {
|
||||
// Example: Get token from localStorage or a secure store
|
||||
// In a real implementation, this might refresh tokens, handle expiration, etc.
|
||||
return localStorage.getItem('auth_token')
|
||||
}
|
||||
}
|
||||
|
||||
// Example: API key provider for specific domains
|
||||
class ApiKeyProvider implements IHeaderProvider {
|
||||
private apiKeys: Record<string, string> = {
|
||||
'api.example.com': 'example-api-key',
|
||||
'api.another.com': 'another-api-key'
|
||||
}
|
||||
|
||||
provideHeaders(context: HeaderProviderContext): HeaderMap {
|
||||
const url = new URL(context.url)
|
||||
const apiKey = this.apiKeys[url.hostname]
|
||||
|
||||
if (apiKey) {
|
||||
return {
|
||||
'X-API-Key': apiKey
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Custom header provider for debugging
|
||||
class DebugHeaderProvider implements IHeaderProvider {
|
||||
provideHeaders(_context: HeaderProviderContext): HeaderMap {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return {
|
||||
'X-Debug-Mode': 'true',
|
||||
'X-Request-ID': crypto.randomUUID()
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export const headerRegistrationExtension: ComfyExtension = {
|
||||
name: 'HeaderRegistration',
|
||||
|
||||
/**
|
||||
* Pre-init hook - called before canvas creation.
|
||||
* This is the perfect place to register header providers.
|
||||
*/
|
||||
async preInit(_app) {
|
||||
console.log(
|
||||
'[HeaderRegistration] Registering header providers in pre-init hook'
|
||||
)
|
||||
|
||||
// Register auth token provider with high priority
|
||||
const authRegistration = headerRegistry.registerHeaderProvider(
|
||||
new AuthTokenProvider(),
|
||||
{
|
||||
priority: 100
|
||||
}
|
||||
)
|
||||
|
||||
// Register API key provider
|
||||
const apiKeyRegistration = headerRegistry.registerHeaderProvider(
|
||||
new ApiKeyProvider(),
|
||||
{
|
||||
priority: 90
|
||||
}
|
||||
)
|
||||
|
||||
// Register debug header provider with lower priority
|
||||
const debugRegistration = headerRegistry.registerHeaderProvider(
|
||||
new DebugHeaderProvider(),
|
||||
{
|
||||
priority: 10
|
||||
}
|
||||
)
|
||||
|
||||
// Store registrations for potential cleanup later
|
||||
// Extensions can store their data on the app instance
|
||||
const extensionData = {
|
||||
headerRegistrations: [
|
||||
authRegistration,
|
||||
apiKeyRegistration,
|
||||
debugRegistration
|
||||
]
|
||||
}
|
||||
|
||||
// Store a reference on the extension itself for potential cleanup
|
||||
;(headerRegistrationExtension as any).registrations =
|
||||
extensionData.headerRegistrations
|
||||
},
|
||||
|
||||
/**
|
||||
* Standard init hook - called after canvas creation.
|
||||
* At this point, header providers are already active.
|
||||
*/
|
||||
async init(_app) {
|
||||
console.log(
|
||||
'[HeaderRegistration] Headers are now being injected into all HTTP requests'
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup hook - called after app is fully initialized.
|
||||
* We could add UI elements here to manage headers.
|
||||
*/
|
||||
async setup(_app) {
|
||||
// Example: Add a command to test header injection
|
||||
const { useCommandStore } = await import('@/stores/commandStore')
|
||||
|
||||
useCommandStore().registerCommand({
|
||||
id: 'header-registration.test',
|
||||
icon: 'pi pi-globe',
|
||||
label: 'Test Header Injection',
|
||||
function: async () => {
|
||||
try {
|
||||
// Make a test request to see headers in action
|
||||
const response = await fetch('/api/test')
|
||||
console.log('[HeaderRegistration] Test request completed', {
|
||||
status: response.status,
|
||||
headers: response.headers
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[HeaderRegistration] Test request failed', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Extension usage:
|
||||
// 1. Import this extension in your extension index
|
||||
// 2. Register it with app.registerExtension(headerRegistrationExtension)
|
||||
// 3. Header providers will be automatically registered before any network activity
|
||||
@@ -1,29 +0,0 @@
|
||||
import { AuthHeaderProvider } from '@/providers/authHeaderProvider'
|
||||
import { app } from '@/scripts/app'
|
||||
import { headerRegistry } from '@/services/headerRegistry'
|
||||
|
||||
/**
|
||||
* Core extension that registers authentication header providers.
|
||||
* This ensures all HTTP requests automatically include authentication headers.
|
||||
*/
|
||||
app.registerExtension({
|
||||
name: 'Comfy.AuthHeaders',
|
||||
|
||||
/**
|
||||
* Register authentication header provider in the pre-init phase.
|
||||
* This ensures headers are available before any network activity.
|
||||
*/
|
||||
async preInit(_app) {
|
||||
console.log('[AuthHeaders] Registering authentication header provider')
|
||||
|
||||
// Register the auth header provider with high priority
|
||||
// This ensures auth headers are added to all requests
|
||||
headerRegistry.registerHeaderProvider(new AuthHeaderProvider(), {
|
||||
priority: 1000 // High priority to ensure auth headers are applied
|
||||
})
|
||||
|
||||
console.log(
|
||||
'[AuthHeaders] Authentication headers will be automatically injected'
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -3,7 +3,6 @@ import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
type ExecutionId,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode
|
||||
@@ -1173,7 +1172,8 @@ export class GroupNodeHandler {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getExtraMenuOptions?.apply(this, arguments)
|
||||
|
||||
let optionIndex = options.findIndex((o) => o?.content === 'Outputs')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let optionIndex = options.findIndex((o) => o.content === 'Outputs')
|
||||
if (optionIndex === -1) optionIndex = options.length
|
||||
else optionIndex++
|
||||
options.splice(
|
||||
@@ -1634,57 +1634,6 @@ export class GroupNodeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
function addConvertToGroupOptions() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function addConvertOption(options, index) {
|
||||
const selected = Object.values(app.canvas.selected_nodes ?? {})
|
||||
const disabled =
|
||||
selected.length < 2 ||
|
||||
selected.find((n) => GroupNodeHandler.isGroupNode(n))
|
||||
options.splice(index, null, {
|
||||
content: `Convert to Group Node (Deprecated)`,
|
||||
disabled,
|
||||
callback: convertSelectedNodesToGroupNode
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function addManageOption(options, index) {
|
||||
const groups = app.graph.extra?.groupNodes
|
||||
const disabled = !groups || !Object.keys(groups).length
|
||||
options.splice(index, null, {
|
||||
content: `Manage Group Nodes`,
|
||||
disabled,
|
||||
callback: () => manageGroupNodes()
|
||||
})
|
||||
}
|
||||
|
||||
// Add to canvas
|
||||
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const options = getCanvasMenuOptions.apply(this, arguments)
|
||||
const index = options.findIndex((o) => o?.content === 'Add Group')
|
||||
const insertAt = index === -1 ? options.length - 1 : index + 2
|
||||
addConvertOption(options, insertAt)
|
||||
addManageOption(options, insertAt + 1)
|
||||
return options
|
||||
}
|
||||
|
||||
// Add to nodes
|
||||
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions
|
||||
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const options = getNodeMenuOptions.apply(this, arguments)
|
||||
if (!GroupNodeHandler.isGroupNode(node)) {
|
||||
const index = options.findIndex((o) => o?.content === 'Properties')
|
||||
const insertAt = index === -1 ? options.length - 1 : index
|
||||
addConvertOption(options, insertAt)
|
||||
}
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
|
||||
for (const node of nodes) {
|
||||
if (typeof node.type === 'string' && node.type.startsWith('workflow/')) {
|
||||
@@ -1780,9 +1729,6 @@ const ext: ComfyExtension = {
|
||||
}
|
||||
}
|
||||
],
|
||||
setup() {
|
||||
addConvertToGroupOptions()
|
||||
},
|
||||
async beforeConfigureGraph(
|
||||
graphData: ComfyWorkflowJSON,
|
||||
missingNodeTypes: string[]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import './authHeaders'
|
||||
import './clipspace'
|
||||
import './contextMenuFilter'
|
||||
import './dynamicPrompts'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
class Load3dUtils {
|
||||
@@ -10,7 +9,7 @@ class Load3dUtils {
|
||||
prefix: string,
|
||||
fileType: string = 'png'
|
||||
) {
|
||||
const blob = await fetchWithHeaders(imageData).then((r) => r.blob())
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const name = `${prefix}_${Date.now()}.${fileType}`
|
||||
const file = new File([blob], name, {
|
||||
type: fileType === 'mp4' ? 'video/mp4' : 'image/png'
|
||||
|
||||
@@ -4,8 +4,6 @@ import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
export class ModelExporter {
|
||||
@@ -38,18 +36,7 @@ export class ModelExporter {
|
||||
desiredFilename: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Check if this is a ComfyUI relative URL
|
||||
const isComfyUrl = url.startsWith('/') || url.includes('/view?')
|
||||
|
||||
let response: Response
|
||||
if (isComfyUrl) {
|
||||
// Use ComfyUI API client for internal URLs
|
||||
response = await fetchWithHeaders(api.apiURL(url))
|
||||
} else {
|
||||
// Use direct fetch for external URLs
|
||||
response = await fetchWithHeaders(url)
|
||||
}
|
||||
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
|
||||
const link = document.createElement('a')
|
||||
|
||||
@@ -20,7 +20,6 @@ import type { SubgraphEventMap } from './infrastructure/SubgraphEventMap'
|
||||
import type {
|
||||
DefaultConnectionColors,
|
||||
Dictionary,
|
||||
HasBoundingRect,
|
||||
IContextMenuValue,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
@@ -29,8 +28,7 @@ import type {
|
||||
MethodNames,
|
||||
OptionalProps,
|
||||
Point,
|
||||
Positionable,
|
||||
Size
|
||||
Positionable
|
||||
} from './interfaces'
|
||||
import { LiteGraph, SubgraphNode } from './litegraph'
|
||||
import {
|
||||
@@ -1571,9 +1569,6 @@ export class LGraph
|
||||
boundingRect
|
||||
)
|
||||
|
||||
//Correct for title height. It's included in bounding box, but not _posSize
|
||||
subgraphNode.pos[1] += LiteGraph.NODE_TITLE_HEIGHT / 2
|
||||
|
||||
// Add the subgraph node to the graph
|
||||
this.add(subgraphNode)
|
||||
|
||||
@@ -1673,21 +1668,14 @@ export class LGraph
|
||||
if (!(subgraphNode instanceof SubgraphNode))
|
||||
throw new Error('Can only unpack Subgraph Nodes')
|
||||
this.beforeChange()
|
||||
//NOTE: Create bounds can not be called on positionables directly as the subgraph is not being displayed and boundingRect is not initialized.
|
||||
//NOTE: NODE_TITLE_HEIGHT is explicitly excluded here
|
||||
const positionables = [
|
||||
...subgraphNode.subgraph.nodes,
|
||||
...subgraphNode.subgraph.reroutes.values(),
|
||||
...subgraphNode.subgraph.groups
|
||||
].map((p: { pos: Point; size?: Size }): HasBoundingRect => {
|
||||
return {
|
||||
boundingRect: [p.pos[0], p.pos[1], p.size?.[0] ?? 0, p.size?.[1] ?? 0]
|
||||
}
|
||||
})
|
||||
const bounds = createBounds(positionables) ?? [0, 0, 0, 0]
|
||||
const center = [bounds[0] + bounds[2] / 2, bounds[1] + bounds[3] / 2]
|
||||
const center = [0, 0]
|
||||
for (const node of subgraphNode.subgraph.nodes) {
|
||||
center[0] += node.pos[0] + node.size[0] / 2
|
||||
center[1] += node.pos[1] + node.size[1] / 2
|
||||
}
|
||||
center[0] /= subgraphNode.subgraph.nodes.length
|
||||
center[1] /= subgraphNode.subgraph.nodes.length
|
||||
|
||||
const toSelect: Positionable[] = []
|
||||
const offsetX = subgraphNode.pos[0] - center[0] + subgraphNode.size[0] / 2
|
||||
const offsetY = subgraphNode.pos[1] - center[1] + subgraphNode.size[1] / 2
|
||||
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
|
||||
@@ -1709,21 +1697,6 @@ export class LGraph
|
||||
for (const input of node.inputs) {
|
||||
input.link = null
|
||||
}
|
||||
for (const output of node.outputs) {
|
||||
output.links = []
|
||||
}
|
||||
toSelect.push(node)
|
||||
}
|
||||
const groups = structuredClone(
|
||||
[...subgraphNode.subgraph.groups].map((g) => g.serialize())
|
||||
)
|
||||
for (const g_info of groups) {
|
||||
const group = new LGraphGroup(g_info.title, g_info.id)
|
||||
this.add(group, true)
|
||||
group.configure(g_info)
|
||||
group.pos[0] += offsetX
|
||||
group.pos[1] += offsetY
|
||||
toSelect.push(group)
|
||||
}
|
||||
//cleanup reoute.linkIds now, but leave link.parentIds dangling
|
||||
for (const islot of subgraphNode.inputs) {
|
||||
@@ -1749,16 +1722,17 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
}
|
||||
const newLinks: {
|
||||
oid: NodeId
|
||||
oslot: number
|
||||
tid: NodeId
|
||||
tslot: number
|
||||
id: LinkId
|
||||
iparent?: RerouteId
|
||||
eparent?: RerouteId
|
||||
externalFirst: boolean
|
||||
}[] = []
|
||||
|
||||
const newLinks: [
|
||||
NodeId,
|
||||
number,
|
||||
NodeId,
|
||||
number,
|
||||
LinkId,
|
||||
RerouteId | undefined,
|
||||
RerouteId | undefined,
|
||||
boolean
|
||||
][] = []
|
||||
for (const [, link] of subgraphNode.subgraph._links) {
|
||||
let externalParentId: RerouteId | undefined
|
||||
if (link.origin_id === SUBGRAPH_INPUT_ID) {
|
||||
@@ -1783,16 +1757,16 @@ export class LGraph
|
||||
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
|
||||
[]) {
|
||||
const sublink = this.links[linkId]
|
||||
newLinks.push({
|
||||
oid: link.origin_id,
|
||||
oslot: link.origin_slot,
|
||||
tid: sublink.target_id,
|
||||
tslot: sublink.target_slot,
|
||||
id: link.id,
|
||||
iparent: link.parentId,
|
||||
eparent: sublink.parentId,
|
||||
externalFirst: true
|
||||
})
|
||||
newLinks.push([
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
sublink.target_id,
|
||||
sublink.target_slot,
|
||||
link.id,
|
||||
link.parentId,
|
||||
sublink.parentId,
|
||||
true
|
||||
])
|
||||
sublink.parentId = undefined
|
||||
}
|
||||
continue
|
||||
@@ -1804,47 +1778,47 @@ export class LGraph
|
||||
}
|
||||
link.target_id = target_id
|
||||
}
|
||||
newLinks.push({
|
||||
oid: link.origin_id,
|
||||
oslot: link.origin_slot,
|
||||
tid: link.target_id,
|
||||
tslot: link.target_slot,
|
||||
id: link.id,
|
||||
iparent: link.parentId,
|
||||
eparent: externalParentId,
|
||||
externalFirst: false
|
||||
})
|
||||
newLinks.push([
|
||||
link.origin_id,
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
link.target_slot,
|
||||
link.id,
|
||||
link.parentId,
|
||||
externalParentId,
|
||||
false
|
||||
])
|
||||
}
|
||||
this.remove(subgraphNode)
|
||||
this.subgraphs.delete(subgraphNode.subgraph.id)
|
||||
const linkIdMap = new Map<LinkId, LinkId[]>()
|
||||
for (const newLink of newLinks) {
|
||||
let created: LLink | null | undefined
|
||||
if (newLink.oid == SUBGRAPH_INPUT_ID) {
|
||||
if (newLink[0] == SUBGRAPH_INPUT_ID) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink.tid]
|
||||
created = this.inputNode.slots[newLink.oslot].connect(
|
||||
tnode.inputs[newLink.tslot],
|
||||
const tnode = this._nodes_by_id[newLink[2]]
|
||||
created = this.inputNode.slots[newLink[1]].connect(
|
||||
tnode.inputs[newLink[3]],
|
||||
tnode
|
||||
)
|
||||
} else if (newLink.tid == SUBGRAPH_OUTPUT_ID) {
|
||||
} else if (newLink[2] == SUBGRAPH_OUTPUT_ID) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink.oid]
|
||||
created = this.outputNode.slots[newLink.tslot].connect(
|
||||
tnode.outputs[newLink.oslot],
|
||||
const tnode = this._nodes_by_id[newLink[0]]
|
||||
created = this.outputNode.slots[newLink[3]].connect(
|
||||
tnode.outputs[newLink[1]],
|
||||
tnode
|
||||
)
|
||||
} else {
|
||||
created = this._nodes_by_id[newLink.oid].connect(
|
||||
newLink.oslot,
|
||||
this._nodes_by_id[newLink.tid],
|
||||
newLink.tslot
|
||||
created = this._nodes_by_id[newLink[0]].connect(
|
||||
newLink[1],
|
||||
this._nodes_by_id[newLink[2]],
|
||||
newLink[3]
|
||||
)
|
||||
}
|
||||
if (!created) {
|
||||
@@ -1852,12 +1826,12 @@ export class LGraph
|
||||
continue
|
||||
}
|
||||
//This is a little unwieldy since Map.has isn't a type guard
|
||||
const linkIds = linkIdMap.get(newLink.id) ?? []
|
||||
const linkIds = linkIdMap.get(newLink[4]) ?? []
|
||||
linkIds.push(created.id)
|
||||
if (!linkIdMap.has(newLink.id)) {
|
||||
linkIdMap.set(newLink.id, linkIds)
|
||||
if (!linkIdMap.has(newLink[4])) {
|
||||
linkIdMap.set(newLink[4], linkIds)
|
||||
}
|
||||
newLink.id = created.id
|
||||
newLink[4] = created.id
|
||||
}
|
||||
const rerouteIdMap = new Map<RerouteId, RerouteId>()
|
||||
for (const reroute of subgraphNode.subgraph.reroutes.values()) {
|
||||
@@ -1873,18 +1847,17 @@ export class LGraph
|
||||
])
|
||||
rerouteIdMap.set(reroute.id, migratedReroute.id)
|
||||
this.reroutes.set(migratedReroute.id, migratedReroute)
|
||||
toSelect.push(migratedReroute)
|
||||
}
|
||||
//iterate over newly created links to update reroute parentIds
|
||||
for (const newLink of newLinks) {
|
||||
const linkInstance = this.links.get(newLink.id)
|
||||
const linkInstance = this.links.get(newLink[4])
|
||||
if (!linkInstance) {
|
||||
continue
|
||||
}
|
||||
let instance: Reroute | LLink | undefined = linkInstance
|
||||
let parentId: RerouteId | undefined = undefined
|
||||
if (newLink.externalFirst) {
|
||||
parentId = newLink.eparent
|
||||
let parentId: RerouteId | undefined = newLink[6]
|
||||
if (newLink[7]) {
|
||||
parentId = newLink[6]
|
||||
//TODO: recursion check/helper method? Probably exists, but wouldn't mesh with the reference tracking used by this implementation
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
@@ -1896,7 +1869,7 @@ export class LGraph
|
||||
parentId = instance.parentId
|
||||
}
|
||||
}
|
||||
parentId = newLink.iparent
|
||||
parentId = newLink[5]
|
||||
while (parentId) {
|
||||
const migratedId = rerouteIdMap.get(parentId)
|
||||
if (!migratedId) throw new Error('Broken Id link when unpacking')
|
||||
@@ -1910,8 +1883,8 @@ export class LGraph
|
||||
if (!oldReroute) throw new Error('Broken Id link when unpacking')
|
||||
parentId = oldReroute.parentId
|
||||
}
|
||||
if (!newLink.externalFirst) {
|
||||
parentId = newLink.eparent
|
||||
if (!newLink[7]) {
|
||||
parentId = newLink[6]
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
@@ -1924,13 +1897,18 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: LGraphNode[] = []
|
||||
for (const nodeId of nodeIdMap.values()) {
|
||||
const node = this._nodes_by_id[nodeId]
|
||||
nodes.push(node)
|
||||
node._setConcreteSlots()
|
||||
node.arrange()
|
||||
}
|
||||
const reroutes = [...rerouteIdMap.values()]
|
||||
.map((i) => this.reroutes.get(i))
|
||||
.filter((x): x is Reroute => !!x)
|
||||
|
||||
this.canvasAction((c) => c.selectItems(toSelect))
|
||||
this.canvasAction((c) => c.selectItems([...nodes, ...reroutes]))
|
||||
this.afterChange()
|
||||
}
|
||||
|
||||
|
||||
@@ -135,10 +135,6 @@ export class FloatingRenderLink implements RenderLink {
|
||||
return true
|
||||
}
|
||||
|
||||
canConnectToSubgraphInput(input: SubgraphInput): boolean {
|
||||
return this.toType === 'output' && input.isValidTarget(this.fromSlot)
|
||||
}
|
||||
|
||||
connectToInput(
|
||||
node: LGraphNode,
|
||||
input: INodeInputSlot,
|
||||
|
||||
@@ -681,20 +681,6 @@ export class LinkConnector {
|
||||
let targetSlot = input
|
||||
|
||||
for (const link of renderLinks) {
|
||||
// Validate the connection type before proceeding
|
||||
if (
|
||||
'canConnectToSubgraphInput' in link &&
|
||||
!link.canConnectToSubgraphInput(targetSlot)
|
||||
) {
|
||||
console.warn(
|
||||
'Invalid connection type',
|
||||
link.fromSlot.type,
|
||||
'->',
|
||||
targetSlot.type
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
link.connectToSubgraphInput(targetSlot, this.events)
|
||||
|
||||
// If we just connected to an EmptySubgraphInput, check if we should reuse the slot
|
||||
@@ -955,14 +941,6 @@ export class LinkConnector {
|
||||
)
|
||||
}
|
||||
|
||||
isSubgraphInputValidDrop(input: SubgraphInput): boolean {
|
||||
return this.renderLinks.some(
|
||||
(link) =>
|
||||
'canConnectToSubgraphInput' in link &&
|
||||
link.canConnectToSubgraphInput(input)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a reroute is a valid drop target for any of the links being connected.
|
||||
* @param reroute The reroute that would be dropped on.
|
||||
|
||||
@@ -55,10 +55,6 @@ export class MovingOutputLink extends MovingLinkBase {
|
||||
return reroute.origin_id !== this.outputNode.id
|
||||
}
|
||||
|
||||
canConnectToSubgraphInput(input: SubgraphInput): boolean {
|
||||
return input.isValidTarget(this.fromSlot)
|
||||
}
|
||||
|
||||
connectToInput(): never {
|
||||
throw new Error('MovingOutputLink cannot connect to an input.')
|
||||
}
|
||||
|
||||
@@ -58,10 +58,6 @@ export class ToOutputRenderLink implements RenderLink {
|
||||
return true
|
||||
}
|
||||
|
||||
canConnectToSubgraphInput(input: SubgraphInput): boolean {
|
||||
return input.isValidTarget(this.fromSlot)
|
||||
}
|
||||
|
||||
connectToOutput(
|
||||
node: LGraphNode,
|
||||
output: INodeOutputSlot,
|
||||
|
||||
@@ -171,12 +171,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
subgraphInput: SubgraphInput,
|
||||
input: INodeInputSlot & Partial<ISubgraphInput>
|
||||
) {
|
||||
if (
|
||||
input._listenerController &&
|
||||
typeof input._listenerController.abort === 'function'
|
||||
) {
|
||||
input._listenerController.abort()
|
||||
}
|
||||
input._listenerController?.abort()
|
||||
input._listenerController = new AbortController()
|
||||
const { signal } = input._listenerController
|
||||
|
||||
@@ -212,12 +207,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
override configure(info: ExportedSubgraphInstance): void {
|
||||
for (const input of this.inputs) {
|
||||
if (
|
||||
input._listenerController &&
|
||||
typeof input._listenerController.abort === 'function'
|
||||
) {
|
||||
input._listenerController.abort()
|
||||
}
|
||||
input._listenerController?.abort()
|
||||
}
|
||||
|
||||
this.inputs.length = 0
|
||||
@@ -266,14 +256,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const subgraphInput = this.subgraph.inputNode.slots.find(
|
||||
(slot) => slot.name === input.name
|
||||
)
|
||||
if (!subgraphInput) {
|
||||
// Skip inputs that don't exist in the subgraph definition
|
||||
// This can happen when loading workflows with dynamically added inputs
|
||||
console.warn(
|
||||
`[SubgraphNode.configure] No subgraph input found for input ${input.name}, skipping`
|
||||
if (!subgraphInput)
|
||||
throw new Error(
|
||||
`[SubgraphNode.configure] No subgraph input found for input ${input.name}`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
this.#addSubgraphInputListeners(subgraphInput, input)
|
||||
|
||||
@@ -532,12 +518,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
|
||||
for (const input of this.inputs) {
|
||||
if (
|
||||
input._listenerController &&
|
||||
typeof input._listenerController.abort === 'function'
|
||||
) {
|
||||
input._listenerController.abort()
|
||||
}
|
||||
input._listenerController?.abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
import { MovingOutputLink } from '@/lib/litegraph/src/canvas/MovingOutputLink'
|
||||
import { ToOutputRenderLink } from '@/lib/litegraph/src/canvas/ToOutputRenderLink'
|
||||
import { LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
|
||||
|
||||
import { createTestSubgraph } from '../subgraph/fixtures/subgraphHelpers'
|
||||
|
||||
describe('LinkConnector SubgraphInput connection validation', () => {
|
||||
let connector: LinkConnector
|
||||
const mockSetConnectingLinks = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new LinkConnector(mockSetConnectingLinks)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('MovingOutputLink validation', () => {
|
||||
it('should implement canConnectToSubgraphInput method', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const sourceNode = new LGraphNode('SourceNode')
|
||||
sourceNode.addOutput('number_out', 'number')
|
||||
subgraph.add(sourceNode)
|
||||
|
||||
const targetNode = new LGraphNode('TargetNode')
|
||||
targetNode.addInput('number_in', 'number')
|
||||
subgraph.add(targetNode)
|
||||
|
||||
const link = new LLink(1, 'number', sourceNode.id, 0, targetNode.id, 0)
|
||||
subgraph._links.set(link.id, link)
|
||||
|
||||
const movingLink = new MovingOutputLink(subgraph, link)
|
||||
|
||||
// Verify the method exists
|
||||
expect(typeof movingLink.canConnectToSubgraphInput).toBe('function')
|
||||
})
|
||||
|
||||
it('should validate type compatibility correctly', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const sourceNode = new LGraphNode('SourceNode')
|
||||
sourceNode.addOutput('number_out', 'number')
|
||||
sourceNode.addOutput('string_out', 'string')
|
||||
subgraph.add(sourceNode)
|
||||
|
||||
const targetNode = new LGraphNode('TargetNode')
|
||||
targetNode.addInput('number_in', 'number')
|
||||
targetNode.addInput('string_in', 'string')
|
||||
subgraph.add(targetNode)
|
||||
|
||||
// Create valid link (number -> number)
|
||||
const validLink = new LLink(
|
||||
1,
|
||||
'number',
|
||||
sourceNode.id,
|
||||
0,
|
||||
targetNode.id,
|
||||
0
|
||||
)
|
||||
subgraph._links.set(validLink.id, validLink)
|
||||
const validMovingLink = new MovingOutputLink(subgraph, validLink)
|
||||
|
||||
// Create invalid link (string -> number)
|
||||
const invalidLink = new LLink(
|
||||
2,
|
||||
'string',
|
||||
sourceNode.id,
|
||||
1,
|
||||
targetNode.id,
|
||||
1
|
||||
)
|
||||
subgraph._links.set(invalidLink.id, invalidLink)
|
||||
const invalidMovingLink = new MovingOutputLink(subgraph, invalidLink)
|
||||
|
||||
const numberInput = subgraph.inputs[0]
|
||||
|
||||
// Test validation
|
||||
expect(validMovingLink.canConnectToSubgraphInput(numberInput)).toBe(true)
|
||||
expect(invalidMovingLink.canConnectToSubgraphInput(numberInput)).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle wildcard types', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'wildcard_input', type: '*' }]
|
||||
})
|
||||
|
||||
const sourceNode = new LGraphNode('SourceNode')
|
||||
sourceNode.addOutput('number_out', 'number')
|
||||
subgraph.add(sourceNode)
|
||||
|
||||
const targetNode = new LGraphNode('TargetNode')
|
||||
targetNode.addInput('number_in', 'number')
|
||||
subgraph.add(targetNode)
|
||||
|
||||
const link = new LLink(1, 'number', sourceNode.id, 0, targetNode.id, 0)
|
||||
subgraph._links.set(link.id, link)
|
||||
const movingLink = new MovingOutputLink(subgraph, link)
|
||||
|
||||
const wildcardInput = subgraph.inputs[0]
|
||||
|
||||
// Wildcard should accept any type
|
||||
expect(movingLink.canConnectToSubgraphInput(wildcardInput)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ToOutputRenderLink validation', () => {
|
||||
it('should implement canConnectToSubgraphInput method', () => {
|
||||
// Create a minimal valid setup
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.id = 1
|
||||
node.addInput('test_in', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
const slot = node.inputs[0] as NodeInputSlot
|
||||
const renderLink = new ToOutputRenderLink(subgraph, node, slot)
|
||||
|
||||
// Verify the method exists
|
||||
expect(typeof renderLink.canConnectToSubgraphInput).toBe('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dropOnIoNode validation', () => {
|
||||
it('should prevent invalid connections when dropping on SubgraphInputNode', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const sourceNode = new LGraphNode('SourceNode')
|
||||
sourceNode.addOutput('string_out', 'string')
|
||||
subgraph.add(sourceNode)
|
||||
|
||||
const targetNode = new LGraphNode('TargetNode')
|
||||
targetNode.addInput('string_in', 'string')
|
||||
subgraph.add(targetNode)
|
||||
|
||||
// Create an invalid link (string output -> string input, but subgraph expects number)
|
||||
const link = new LLink(1, 'string', sourceNode.id, 0, targetNode.id, 0)
|
||||
subgraph._links.set(link.id, link)
|
||||
const movingLink = new MovingOutputLink(subgraph, link)
|
||||
|
||||
// Mock console.warn to verify it's called
|
||||
const consoleWarnSpy = vi
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {})
|
||||
|
||||
// Add the link to the connector
|
||||
connector.renderLinks.push(movingLink)
|
||||
connector.state.connectingTo = 'output'
|
||||
|
||||
// Create mock event
|
||||
const mockEvent = {
|
||||
canvasX: 100,
|
||||
canvasY: 100
|
||||
} as any
|
||||
|
||||
// Mock the getSlotInPosition to return the subgraph input
|
||||
const mockGetSlotInPosition = vi.fn().mockReturnValue(subgraph.inputs[0])
|
||||
subgraph.inputNode.getSlotInPosition = mockGetSlotInPosition
|
||||
|
||||
// Spy on connectToSubgraphInput to ensure it's NOT called
|
||||
const connectSpy = vi.spyOn(movingLink, 'connectToSubgraphInput')
|
||||
|
||||
// Drop on the SubgraphInputNode
|
||||
connector.dropOnIoNode(subgraph.inputNode, mockEvent)
|
||||
|
||||
// Verify that the invalid connection was skipped
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'Invalid connection type',
|
||||
'string',
|
||||
'->',
|
||||
'number'
|
||||
)
|
||||
expect(connectSpy).not.toHaveBeenCalled()
|
||||
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should allow valid connections when dropping on SubgraphInputNode', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const sourceNode = new LGraphNode('SourceNode')
|
||||
sourceNode.addOutput('number_out', 'number')
|
||||
subgraph.add(sourceNode)
|
||||
|
||||
const targetNode = new LGraphNode('TargetNode')
|
||||
targetNode.addInput('number_in', 'number')
|
||||
subgraph.add(targetNode)
|
||||
|
||||
// Create a valid link (number -> number)
|
||||
const link = new LLink(1, 'number', sourceNode.id, 0, targetNode.id, 0)
|
||||
subgraph._links.set(link.id, link)
|
||||
const movingLink = new MovingOutputLink(subgraph, link)
|
||||
|
||||
// Add the link to the connector
|
||||
connector.renderLinks.push(movingLink)
|
||||
connector.state.connectingTo = 'output'
|
||||
|
||||
// Create mock event
|
||||
const mockEvent = {
|
||||
canvasX: 100,
|
||||
canvasY: 100
|
||||
} as any
|
||||
|
||||
// Mock the getSlotInPosition to return the subgraph input
|
||||
const mockGetSlotInPosition = vi.fn().mockReturnValue(subgraph.inputs[0])
|
||||
subgraph.inputNode.getSlotInPosition = mockGetSlotInPosition
|
||||
|
||||
// Spy on connectToSubgraphInput to ensure it IS called
|
||||
const connectSpy = vi.spyOn(movingLink, 'connectToSubgraphInput')
|
||||
|
||||
// Drop on the SubgraphInputNode
|
||||
connector.dropOnIoNode(subgraph.inputNode, mockEvent)
|
||||
|
||||
// Verify that the valid connection was made
|
||||
expect(connectSpy).toHaveBeenCalledWith(
|
||||
subgraph.inputs[0],
|
||||
connector.events
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSubgraphInputValidDrop', () => {
|
||||
it('should check if render links can connect to SubgraphInput', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
const sourceNode = new LGraphNode('SourceNode')
|
||||
sourceNode.addOutput('number_out', 'number')
|
||||
sourceNode.addOutput('string_out', 'string')
|
||||
subgraph.add(sourceNode)
|
||||
|
||||
const targetNode = new LGraphNode('TargetNode')
|
||||
targetNode.addInput('number_in', 'number')
|
||||
targetNode.addInput('string_in', 'string')
|
||||
subgraph.add(targetNode)
|
||||
|
||||
// Create valid and invalid links
|
||||
const validLink = new LLink(
|
||||
1,
|
||||
'number',
|
||||
sourceNode.id,
|
||||
0,
|
||||
targetNode.id,
|
||||
0
|
||||
)
|
||||
const invalidLink = new LLink(
|
||||
2,
|
||||
'string',
|
||||
sourceNode.id,
|
||||
1,
|
||||
targetNode.id,
|
||||
1
|
||||
)
|
||||
subgraph._links.set(validLink.id, validLink)
|
||||
subgraph._links.set(invalidLink.id, invalidLink)
|
||||
|
||||
const validMovingLink = new MovingOutputLink(subgraph, validLink)
|
||||
const invalidMovingLink = new MovingOutputLink(subgraph, invalidLink)
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
// Test with only invalid link
|
||||
connector.renderLinks.length = 0
|
||||
connector.renderLinks.push(invalidMovingLink)
|
||||
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(false)
|
||||
|
||||
// Test with valid link
|
||||
connector.renderLinks.length = 0
|
||||
connector.renderLinks.push(validMovingLink)
|
||||
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(true)
|
||||
|
||||
// Test with mixed links
|
||||
connector.renderLinks.length = 0
|
||||
connector.renderLinks.push(invalidMovingLink, validMovingLink)
|
||||
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle render links without canConnectToSubgraphInput method', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'number_input', type: 'number' }]
|
||||
})
|
||||
|
||||
// Create a mock render link without the method
|
||||
const mockLink = {
|
||||
fromSlot: { type: 'number' }
|
||||
// No canConnectToSubgraphInput method
|
||||
} as any
|
||||
|
||||
connector.renderLinks.push(mockLink)
|
||||
|
||||
const subgraphInput = subgraph.inputs[0]
|
||||
|
||||
// Should return false as the link doesn't have the method
|
||||
expect(connector.isSubgraphInputValidDrop(subgraphInput)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,6 @@ import { assert, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
ISlotType,
|
||||
LGraph,
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
LiteGraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
@@ -81,7 +80,7 @@ describe('SubgraphConversion', () => {
|
||||
expect(graph.nodes.length).toBe(4)
|
||||
expect(graph.links.size).toBe(2)
|
||||
})
|
||||
it('Should keep reroutes and groups', () => {
|
||||
it('Should keep reroutes', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
outputs: [{ name: 'value', type: 'number' }]
|
||||
})
|
||||
@@ -99,7 +98,6 @@ describe('SubgraphConversion', () => {
|
||||
const outer = createNode(graph, ['number'])
|
||||
const outerLink = subgraphNode.connect(0, outer, 0)
|
||||
assert(outerLink)
|
||||
subgraph.add(new LGraphGroup())
|
||||
|
||||
subgraph.createReroute([10, 10], innerLink)
|
||||
graph.createReroute([10, 10], outerLink)
|
||||
@@ -107,7 +105,6 @@ describe('SubgraphConversion', () => {
|
||||
graph.unpackSubgraph(subgraphNode)
|
||||
|
||||
expect(graph.reroutes.size).toBe(2)
|
||||
expect(graph.groups.length).toBe(1)
|
||||
})
|
||||
it('Should map reroutes onto split outputs', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "تجميع العقد المحددة"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "فك التفرع الفرعي المحدد"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "تحويل العقد المحددة إلى عقدة مجموعة"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "مدير"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "فتح سير عمل"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "عرض نافذة الإعدادات"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "أداء اللوحة"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "مركز المساعدة"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "تبديل النمط (فاتح/داكن)"
|
||||
},
|
||||
|
||||
@@ -749,7 +749,6 @@
|
||||
"manageExtensions": "إدارة الإضافات",
|
||||
"onChange": "عند التغيير",
|
||||
"onChangeTooltip": "سيتم وضع سير العمل في قائمة الانتظار عند إجراء تغيير",
|
||||
"queue": "لوحة الانتظار",
|
||||
"refresh": "تحديث تعريفات العقد",
|
||||
"resetView": "إعادة تعيين عرض اللوحة",
|
||||
"run": "تشغيل",
|
||||
@@ -763,11 +762,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "حول ComfyUI",
|
||||
"Add Edit Model Step": "إضافة خطوة تعديل النموذج",
|
||||
"Bottom Panel": "لوحة سفلية",
|
||||
"Browse Templates": "تصفح القوالب",
|
||||
"Bypass/Unbypass Selected Nodes": "تجاوز/إلغاء تجاوز العقد المحددة",
|
||||
"Canvas Performance": "أداء اللوحة",
|
||||
"Canvas Toggle Link Visibility": "تبديل ظهور الروابط على اللوحة",
|
||||
"Canvas Toggle Lock": "تبديل قفل اللوحة",
|
||||
"Canvas Toggle Minimap": "تبديل الخريطة المصغرة على اللوحة",
|
||||
"Check for Updates": "التحقق من التحديثات",
|
||||
"Clear Pending Tasks": "مسح المهام المعلقة",
|
||||
"Clear Workflow": "مسح سير العمل",
|
||||
@@ -789,20 +788,15 @@
|
||||
"Exit Subgraph": "الخروج من الرسم الفرعي",
|
||||
"Export": "تصدير",
|
||||
"Export (API)": "تصدير (API)",
|
||||
"File": "ملف",
|
||||
"Fit Group To Contents": "ملائمة المجموعة للمحتويات",
|
||||
"Focus Mode": "وضع التركيز",
|
||||
"Fit view to selected nodes": "تعديل العرض للعقد المحددة",
|
||||
"Give Feedback": "تقديم ملاحظات",
|
||||
"Group Selected Nodes": "تجميع العقد المحددة",
|
||||
"Help": "مساعدة",
|
||||
"Help Center": "مركز المساعدة",
|
||||
"Increase Brush Size in MaskEditor": "زيادة حجم الفرشاة في محرر القناع",
|
||||
"Interrupt": "إيقاف مؤقت",
|
||||
"Load Default Workflow": "تحميل سير العمل الافتراضي",
|
||||
"Manage group nodes": "إدارة عقد المجموعة",
|
||||
"Manager": "المدير",
|
||||
"Minimap": "خريطة مصغرة",
|
||||
"Model Library": "مكتبة النماذج",
|
||||
"Move Selected Nodes Down": "تحريك العقد المحددة للأسفل",
|
||||
"Move Selected Nodes Left": "تحريك العقد المحددة لليسار",
|
||||
"Move Selected Nodes Right": "تحريك العقد المحددة لليمين",
|
||||
@@ -810,8 +804,6 @@
|
||||
"Mute/Unmute Selected Nodes": "كتم/إلغاء كتم العقد المحددة",
|
||||
"New": "جديد",
|
||||
"Next Opened Workflow": "سير العمل التالي المفتوح",
|
||||
"Node Library": "مكتبة العقد",
|
||||
"Node Links": "روابط العقد",
|
||||
"Open": "فتح",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة",
|
||||
"Open Custom Nodes Folder": "فتح مجلد العقد المخصصة",
|
||||
@@ -826,7 +818,6 @@
|
||||
"Pin/Unpin Selected Items": "تثبيت/إلغاء تثبيت العناصر المحددة",
|
||||
"Pin/Unpin Selected Nodes": "تثبيت/إلغاء تثبيت العقد المحددة",
|
||||
"Previous Opened Workflow": "سير العمل السابق المفتوح",
|
||||
"Queue Panel": "لوحة الانتظار",
|
||||
"Queue Prompt": "قائمة انتظار التعليمات",
|
||||
"Queue Prompt (Front)": "قائمة انتظار التعليمات (أمامي)",
|
||||
"Queue Selected Output Nodes": "قائمة انتظار عقد المخرجات المحددة",
|
||||
@@ -842,21 +833,25 @@
|
||||
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
|
||||
"Show Settings Dialog": "عرض نافذة الإعدادات",
|
||||
"Sign Out": "تسجيل خروج",
|
||||
"Toggle Bottom Panel": "تبديل اللوحة السفلية",
|
||||
"Toggle Essential Bottom Panel": "تبديل اللوحة السفلية الأساسية",
|
||||
"Toggle Focus Mode": "تبديل وضع التركيز",
|
||||
"Toggle Logs Bottom Panel": "تبديل لوحة السجلات السفلية",
|
||||
"Toggle Model Library Sidebar": "تبديل الشريط الجانبي لمكتبة النماذج",
|
||||
"Toggle Node Library Sidebar": "تبديل الشريط الجانبي لمكتبة العقد",
|
||||
"Toggle Queue Sidebar": "تبديل الشريط الجانبي لقائمة الانتظار",
|
||||
"Toggle Search Box": "تبديل مربع البحث",
|
||||
"Toggle Terminal Bottom Panel": "تبديل لوحة الطرفية السفلية",
|
||||
"Toggle Theme (Dark/Light)": "تبديل السمة (داكن/فاتح)",
|
||||
"Toggle View Controls Bottom Panel": "تبديل لوحة التحكم في العرض السفلية",
|
||||
"Toggle Workflows Sidebar": "تبديل الشريط الجانبي لسير العمل",
|
||||
"Toggle the Custom Nodes Manager": "تبديل مدير العقد المخصصة",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "تبديل شريط تقدم مدير العقد المخصصة",
|
||||
"Undo": "تراجع",
|
||||
"Ungroup selected group nodes": "فك تجميع عقد المجموعة المحددة",
|
||||
"Unpack the selected Subgraph": "فك تجميع الرسم البياني الفرعي المحدد",
|
||||
"Workflows": "سير العمل",
|
||||
"Workflow": "سير العمل",
|
||||
"Zoom In": "تكبير",
|
||||
"Zoom Out": "تصغير",
|
||||
"Zoom to fit": "تكبير لتناسب"
|
||||
"Zoom Out": "تصغير"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ألوان العقد",
|
||||
@@ -1201,13 +1196,6 @@
|
||||
"browseTemplates": "تصفح القوالب المثال",
|
||||
"downloads": "التنزيلات",
|
||||
"helpCenter": "مركز المساعدة",
|
||||
"labels": {
|
||||
"models": "النماذج",
|
||||
"nodes": "العُقَد",
|
||||
"queue": "قائمة الانتظار",
|
||||
"templates": "القوالب",
|
||||
"workflows": "سير العمل"
|
||||
},
|
||||
"logout": "تسجيل الخروج",
|
||||
"modelLibrary": "مكتبة النماذج",
|
||||
"newBlankWorkflow": "إنشاء سير عمل جديد فارغ",
|
||||
@@ -1245,7 +1233,6 @@
|
||||
},
|
||||
"showFlatList": "عرض القائمة المسطحة"
|
||||
},
|
||||
"templates": "القوالب",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "هل أنت متأكد من رغبتك في حذف هذا السير؟",
|
||||
"confirmDeleteTitle": "حذف سير العمل؟",
|
||||
|
||||
@@ -125,15 +125,15 @@
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "Exit Subgraph"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Unpack the selected Subgraph"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Group Selected Nodes"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Unpack the selected Subgraph"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Convert selected nodes to group node"
|
||||
},
|
||||
@@ -185,9 +185,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "Manager"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Open Workflow"
|
||||
},
|
||||
@@ -215,12 +212,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Show Settings Dialog"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Canvas Performance"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Help Center"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Toggle Theme (Dark/Light)"
|
||||
},
|
||||
|
||||
@@ -437,14 +437,6 @@
|
||||
"queue": "Queue",
|
||||
"nodeLibrary": "Node Library",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates",
|
||||
"labels": {
|
||||
"queue": "Queue",
|
||||
"nodes": "Nodes",
|
||||
"models": "Models",
|
||||
"workflows": "Workflows",
|
||||
"templates": "Templates"
|
||||
},
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
@@ -546,8 +538,7 @@
|
||||
"light": "Light",
|
||||
"manageExtensions": "Manage Extensions",
|
||||
"settings": "Settings",
|
||||
"help": "Help",
|
||||
"queue": "Queue Panel"
|
||||
"help": "Help"
|
||||
},
|
||||
"tabMenu": {
|
||||
"duplicateTab": "Duplicate Tab",
|
||||
@@ -941,7 +932,7 @@
|
||||
"Image Layer": "Image Layer"
|
||||
},
|
||||
"menuLabels": {
|
||||
"File": "File",
|
||||
"Workflow": "Workflow",
|
||||
"Edit": "Edit",
|
||||
"Help": "Help",
|
||||
"Check for Updates": "Check for Updates",
|
||||
@@ -960,16 +951,16 @@
|
||||
"Browse Templates": "Browse Templates",
|
||||
"Add Edit Model Step": "Add Edit Model Step",
|
||||
"Delete Selected Items": "Delete Selected Items",
|
||||
"Zoom to fit": "Zoom to fit",
|
||||
"Fit view to selected nodes": "Fit view to selected nodes",
|
||||
"Move Selected Nodes Down": "Move Selected Nodes Down",
|
||||
"Move Selected Nodes Left": "Move Selected Nodes Left",
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Node Links": "Node Links",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
||||
"Minimap": "Minimap",
|
||||
"Canvas Toggle Minimap": "Canvas Toggle Minimap",
|
||||
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
||||
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
||||
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
||||
@@ -988,7 +979,6 @@
|
||||
"Exit Subgraph": "Exit Subgraph",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Unpack the selected Subgraph": "Unpack the selected Subgraph",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
"Manage group nodes": "Manage group nodes",
|
||||
"Ungroup selected group nodes": "Ungroup selected group nodes",
|
||||
@@ -1006,7 +996,6 @@
|
||||
"Open Mask Editor for Selected Node": "Open Mask Editor for Selected Node",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Manager": "Manager",
|
||||
"Open": "Open",
|
||||
"Queue Prompt": "Queue Prompt",
|
||||
"Queue Prompt (Front)": "Queue Prompt (Front)",
|
||||
@@ -1016,8 +1005,6 @@
|
||||
"Save": "Save",
|
||||
"Save As": "Save As",
|
||||
"Show Settings Dialog": "Show Settings Dialog",
|
||||
"Canvas Performance": "Canvas Performance",
|
||||
"Help Center": "Help Center",
|
||||
"Toggle Theme (Dark/Light)": "Toggle Theme (Dark/Light)",
|
||||
"Undo": "Undo",
|
||||
"Open Sign In Dialog": "Open Sign In Dialog",
|
||||
@@ -1026,17 +1013,17 @@
|
||||
"Next Opened Workflow": "Next Opened Workflow",
|
||||
"Previous Opened Workflow": "Previous Opened Workflow",
|
||||
"Toggle Search Box": "Toggle Search Box",
|
||||
"Bottom Panel": "Bottom Panel",
|
||||
"Toggle Bottom Panel": "Toggle Bottom Panel",
|
||||
"Show Keybindings Dialog": "Show Keybindings Dialog",
|
||||
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
|
||||
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
|
||||
"Toggle Essential Bottom Panel": "Toggle Essential Bottom Panel",
|
||||
"Toggle View Controls Bottom Panel": "Toggle View Controls Bottom Panel",
|
||||
"Focus Mode": "Focus Mode",
|
||||
"Model Library": "Model Library",
|
||||
"Node Library": "Node Library",
|
||||
"Queue Panel": "Queue Panel",
|
||||
"Workflows": "Workflows"
|
||||
"Toggle Focus Mode": "Toggle Focus Mode",
|
||||
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
|
||||
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
|
||||
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
|
||||
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Agrupar nodos seleccionados"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Desempaquetar el subgrafo seleccionado"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Convertir nodos seleccionados en nodo de grupo"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Abrir espacio de clips"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "Administrador"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Abrir Flujo de Trabajo"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Mostrar Diálogo de Configuraciones"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Rendimiento del lienzo"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Centro de ayuda"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Cambiar Tema (Oscuro/Claro)"
|
||||
},
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
"versionMismatchWarning": "Advertencia de compatibilidad de versión",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} Visita https://docs.comfy.org/installation/update_comfyui#common-update-issues para obtener instrucciones de actualización.",
|
||||
"videoFailedToLoad": "Falló la carga del video",
|
||||
"workflow": "Flujo de trabajo"
|
||||
"workflow": "Flujo de trabajo",
|
||||
"duplicate": "Duplicar"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Ajustar vista",
|
||||
@@ -749,7 +750,6 @@
|
||||
"manageExtensions": "Gestionar extensiones",
|
||||
"onChange": "Al cambiar",
|
||||
"onChangeTooltip": "El flujo de trabajo se encolará una vez que se haga un cambio",
|
||||
"queue": "Panel de cola",
|
||||
"refresh": "Actualizar definiciones de nodos",
|
||||
"resetView": "Restablecer vista del lienzo",
|
||||
"run": "Ejecutar",
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "Acerca de ComfyUI",
|
||||
"Add Edit Model Step": "Agregar paso de edición de modelo",
|
||||
"Bottom Panel": "Panel inferior",
|
||||
"Browse Templates": "Explorar plantillas",
|
||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||
"Canvas Performance": "Rendimiento del lienzo",
|
||||
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
||||
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
||||
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
|
||||
"Check for Updates": "Buscar actualizaciones",
|
||||
"Clear Pending Tasks": "Borrar tareas pendientes",
|
||||
"Clear Workflow": "Borrar flujo de trabajo",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "Salir de subgrafo",
|
||||
"Export": "Exportar",
|
||||
"Export (API)": "Exportar (API)",
|
||||
"File": "Archivo",
|
||||
"Fit Group To Contents": "Ajustar grupo a contenidos",
|
||||
"Focus Mode": "Modo de enfoque",
|
||||
"Fit view to selected nodes": "Ajustar vista a los nodos seleccionados",
|
||||
"Give Feedback": "Dar retroalimentación",
|
||||
"Group Selected Nodes": "Agrupar nodos seleccionados",
|
||||
"Help": "Ayuda",
|
||||
"Help Center": "Centro de ayuda",
|
||||
"Increase Brush Size in MaskEditor": "Aumentar tamaño del pincel en MaskEditor",
|
||||
"Interrupt": "Interrumpir",
|
||||
"Load Default Workflow": "Cargar flujo de trabajo predeterminado",
|
||||
"Manage group nodes": "Gestionar nodos de grupo",
|
||||
"Manager": "Administrador",
|
||||
"Minimap": "Minimapa",
|
||||
"Model Library": "Biblioteca de modelos",
|
||||
"Move Selected Nodes Down": "Mover nodos seleccionados hacia abajo",
|
||||
"Move Selected Nodes Left": "Mover nodos seleccionados hacia la izquierda",
|
||||
"Move Selected Nodes Right": "Mover nodos seleccionados hacia la derecha",
|
||||
@@ -810,8 +805,6 @@
|
||||
"Mute/Unmute Selected Nodes": "Silenciar/Activar sonido de nodos seleccionados",
|
||||
"New": "Nuevo",
|
||||
"Next Opened Workflow": "Siguiente flujo de trabajo abierto",
|
||||
"Node Library": "Biblioteca de nodos",
|
||||
"Node Links": "Enlaces de nodos",
|
||||
"Open": "Abrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Abrir visor 3D (Beta) para el nodo seleccionado",
|
||||
"Open Custom Nodes Folder": "Abrir carpeta de nodos personalizados",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "Anclar/Desanclar elementos seleccionados",
|
||||
"Pin/Unpin Selected Nodes": "Anclar/Desanclar nodos seleccionados",
|
||||
"Previous Opened Workflow": "Flujo de trabajo abierto anterior",
|
||||
"Queue Panel": "Panel de cola",
|
||||
"Queue Prompt": "Indicador de cola",
|
||||
"Queue Prompt (Front)": "Indicador de cola (Frente)",
|
||||
"Queue Selected Output Nodes": "Encolar nodos de salida seleccionados",
|
||||
@@ -842,21 +834,25 @@
|
||||
"Show Keybindings Dialog": "Mostrar diálogo de combinaciones de teclas",
|
||||
"Show Settings Dialog": "Mostrar diálogo de configuración",
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Toggle Bottom Panel": "Alternar panel inferior",
|
||||
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",
|
||||
"Toggle Focus Mode": "Alternar modo de enfoque",
|
||||
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
|
||||
"Toggle Model Library Sidebar": "Alternar barra lateral de la biblioteca de modelos",
|
||||
"Toggle Node Library Sidebar": "Alternar barra lateral de la biblioteca de nodos",
|
||||
"Toggle Queue Sidebar": "Alternar barra lateral de la cola",
|
||||
"Toggle Search Box": "Alternar caja de búsqueda",
|
||||
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
|
||||
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
|
||||
"Toggle View Controls Bottom Panel": "Alternar panel inferior de controles de vista",
|
||||
"Toggle Workflows Sidebar": "Alternar barra lateral de los flujos de trabajo",
|
||||
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
|
||||
"Undo": "Deshacer",
|
||||
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
|
||||
"Unpack the selected Subgraph": "Desempaquetar el Subgrafo seleccionado",
|
||||
"Workflows": "Flujos de trabajo",
|
||||
"Workflow": "Flujo de trabajo",
|
||||
"Zoom In": "Acercar",
|
||||
"Zoom Out": "Alejar",
|
||||
"Zoom to fit": "Ajustar al tamaño"
|
||||
"Zoom Out": "Alejar"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Colores de nodos",
|
||||
@@ -1201,13 +1197,6 @@
|
||||
"browseTemplates": "Explorar plantillas de ejemplo",
|
||||
"downloads": "Descargas",
|
||||
"helpCenter": "Centro de ayuda",
|
||||
"labels": {
|
||||
"models": "Modelos",
|
||||
"nodes": "Nodos",
|
||||
"queue": "Cola",
|
||||
"templates": "Plantillas",
|
||||
"workflows": "Flujos de trabajo"
|
||||
},
|
||||
"logout": "Cerrar sesión",
|
||||
"modelLibrary": "Biblioteca de modelos",
|
||||
"newBlankWorkflow": "Crear un nuevo flujo de trabajo en blanco",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "Mostrar lista plana"
|
||||
},
|
||||
"templates": "Plantillas",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "¿Estás seguro de que quieres eliminar este flujo de trabajo?",
|
||||
"confirmDeleteTitle": "¿Eliminar flujo de trabajo?",
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Grouper les nœuds sélectionnés"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Décompresser le sous-graphe sélectionné"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Convertir les nœuds sélectionnés en nœud de groupe"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Espace de clip"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "Gestionnaire"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Ouvrir le flux de travail"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Afficher la boîte de dialogue des paramètres"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Performance du canvas"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Centre d'aide"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Changer de thème (Sombre/Clair)"
|
||||
},
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
"versionMismatchWarning": "Avertissement de compatibilité de version",
|
||||
"versionMismatchWarningMessage": "{warning} : {detail} Consultez https://docs.comfy.org/installation/update_comfyui#common-update-issues pour les instructions de mise à jour.",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
"workflow": "Flux de travail",
|
||||
"duplicate": "Dupliquer"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Adapter la vue",
|
||||
@@ -749,7 +750,6 @@
|
||||
"manageExtensions": "Gérer les extensions",
|
||||
"onChange": "Sur modification",
|
||||
"onChangeTooltip": "Le flux de travail sera mis en file d'attente une fois une modification effectuée",
|
||||
"queue": "Panneau de file d’attente",
|
||||
"refresh": "Actualiser les définitions des nœuds",
|
||||
"resetView": "Réinitialiser la vue du canevas",
|
||||
"run": "Exécuter",
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "À propos de ComfyUI",
|
||||
"Add Edit Model Step": "Ajouter une étape d’édition de modèle",
|
||||
"Bottom Panel": "Panneau inférieur",
|
||||
"Browse Templates": "Parcourir les modèles",
|
||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||
"Canvas Performance": "Performance du canevas",
|
||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
"Clear Workflow": "Effacer le flux de travail",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "Quitter le sous-graphe",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"File": "Fichier",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
"Focus Mode": "Mode focus",
|
||||
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Help Center": "Centre d’aide",
|
||||
"Increase Brush Size in MaskEditor": "Augmenter la taille du pinceau dans MaskEditor",
|
||||
"Interrupt": "Interrompre",
|
||||
"Load Default Workflow": "Charger le flux de travail par défaut",
|
||||
"Manage group nodes": "Gérer les nœuds de groupe",
|
||||
"Manager": "Gestionnaire",
|
||||
"Minimap": "Minicarte",
|
||||
"Model Library": "Bibliothèque de modèles",
|
||||
"Move Selected Nodes Down": "Déplacer les nœuds sélectionnés vers le bas",
|
||||
"Move Selected Nodes Left": "Déplacer les nœuds sélectionnés vers la gauche",
|
||||
"Move Selected Nodes Right": "Déplacer les nœuds sélectionnés vers la droite",
|
||||
@@ -810,8 +805,6 @@
|
||||
"Mute/Unmute Selected Nodes": "Mettre en sourdine/Activer le son des nœuds sélectionnés",
|
||||
"New": "Nouveau",
|
||||
"Next Opened Workflow": "Prochain flux de travail ouvert",
|
||||
"Node Library": "Bibliothèque de nœuds",
|
||||
"Node Links": "Liens de nœuds",
|
||||
"Open": "Ouvrir",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné",
|
||||
"Open Custom Nodes Folder": "Ouvrir le dossier des nœuds personnalisés",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "Épingler/Désépingler les éléments sélectionnés",
|
||||
"Pin/Unpin Selected Nodes": "Épingler/Désépingler les nœuds sélectionnés",
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Queue Panel": "Panneau de file d’attente",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Queue Selected Output Nodes": "Mettre en file d’attente les nœuds de sortie sélectionnés",
|
||||
@@ -842,21 +834,25 @@
|
||||
"Show Keybindings Dialog": "Afficher la boîte de dialogue des raccourcis clavier",
|
||||
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Toggle Bottom Panel": "Basculer le panneau inférieur",
|
||||
"Toggle Essential Bottom Panel": "Afficher/Masquer le panneau inférieur essentiel",
|
||||
"Toggle Focus Mode": "Basculer le mode focus",
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Afficher/Masquer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Queue Sidebar": "Afficher/Masquer la barre latérale de la file d’attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
"Toggle Theme (Dark/Light)": "Basculer le thème (Sombre/Clair)",
|
||||
"Toggle View Controls Bottom Panel": "Afficher/Masquer le panneau inférieur des contrôles de vue",
|
||||
"Toggle Workflows Sidebar": "Afficher/Masquer la barre latérale des workflows",
|
||||
"Toggle the Custom Nodes Manager": "Basculer le gestionnaire de nœuds personnalisés",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Basculer la barre de progression du gestionnaire de nœuds personnalisés",
|
||||
"Undo": "Annuler",
|
||||
"Ungroup selected group nodes": "Dégrouper les nœuds de groupe sélectionnés",
|
||||
"Unpack the selected Subgraph": "Décompresser le Subgraph sélectionné",
|
||||
"Workflows": "Flux de travail",
|
||||
"Workflow": "Flux de travail",
|
||||
"Zoom In": "Zoom avant",
|
||||
"Zoom Out": "Zoom arrière",
|
||||
"Zoom to fit": "Ajuster à l’écran"
|
||||
"Zoom Out": "Zoom arrière"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Couleurs des nœuds",
|
||||
@@ -1201,13 +1197,6 @@
|
||||
"browseTemplates": "Parcourir les modèles d'exemple",
|
||||
"downloads": "Téléchargements",
|
||||
"helpCenter": "Centre d'aide",
|
||||
"labels": {
|
||||
"models": "Modèles",
|
||||
"nodes": "Nœuds",
|
||||
"queue": "File d’attente",
|
||||
"templates": "Modèles",
|
||||
"workflows": "Flux de travail"
|
||||
},
|
||||
"logout": "Déconnexion",
|
||||
"modelLibrary": "Bibliothèque de modèles",
|
||||
"newBlankWorkflow": "Créer un nouveau flux de travail vierge",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "Afficher la liste plate"
|
||||
},
|
||||
"templates": "Modèles",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Êtes-vous sûr de vouloir supprimer ce flux de travail ?",
|
||||
"confirmDeleteTitle": "Supprimer le flux de travail ?",
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "選択したノードをグループ化"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "選択したサブグラフを展開"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "選択したノードをグループノードに変換"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "クリップスペース"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "マネージャー"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "ワークフローを開く"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "設定ダイアログを表示"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "キャンバスパフォーマンス"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "ヘルプセンター"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "テーマの切り替え(ダーク/ライト)"
|
||||
},
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
"versionMismatchWarning": "バージョン互換性の警告",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} 更新手順については https://docs.comfy.org/installation/update_comfyui#common-update-issues をご覧ください。",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
"workflow": "ワークフロー",
|
||||
"duplicate": "複製"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "ビューに合わせる",
|
||||
@@ -749,7 +750,6 @@
|
||||
"manageExtensions": "拡張機能の管理",
|
||||
"onChange": "変更時",
|
||||
"onChangeTooltip": "変更が行われるとワークフローがキューに追加されます",
|
||||
"queue": "キューパネル",
|
||||
"refresh": "ノードを更新",
|
||||
"resetView": "ビューをリセット",
|
||||
"run": "実行する",
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "ComfyUIについて",
|
||||
"Add Edit Model Step": "モデル編集ステップを追加",
|
||||
"Bottom Panel": "下部パネル",
|
||||
"Browse Templates": "テンプレートを参照",
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Performance": "キャンバスパフォーマンス",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "サブグラフを終了",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"File": "ファイル",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
"Focus Mode": "フォーカスモード",
|
||||
"Fit view to selected nodes": "選択したノードにビューを合わせる",
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Help Center": "ヘルプセンター",
|
||||
"Increase Brush Size in MaskEditor": "マスクエディタでブラシサイズを大きくする",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "デフォルトワークフローを読み込む",
|
||||
"Manage group nodes": "グループノードを管理",
|
||||
"Manager": "マネージャー",
|
||||
"Minimap": "ミニマップ",
|
||||
"Model Library": "モデルライブラリ",
|
||||
"Move Selected Nodes Down": "選択したノードを下へ移動",
|
||||
"Move Selected Nodes Left": "選択したノードを左へ移動",
|
||||
"Move Selected Nodes Right": "選択したノードを右へ移動",
|
||||
@@ -810,8 +805,6 @@
|
||||
"Mute/Unmute Selected Nodes": "選択したノードのミュート/ミュート解除",
|
||||
"New": "新規",
|
||||
"Next Opened Workflow": "次に開いたワークフロー",
|
||||
"Node Library": "ノードライブラリ",
|
||||
"Node Links": "ノードリンク",
|
||||
"Open": "開く",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "選択したノードの3Dビューアー(ベータ)を開く",
|
||||
"Open Custom Nodes Folder": "カスタムノードフォルダを開く",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "選択したアイテムのピン留め/ピン留め解除",
|
||||
"Pin/Unpin Selected Nodes": "選択したノードのピン留め/ピン留め解除",
|
||||
"Previous Opened Workflow": "前に開いたワークフロー",
|
||||
"Queue Panel": "キューパネル",
|
||||
"Queue Prompt": "キューのプロンプト",
|
||||
"Queue Prompt (Front)": "キューのプロンプト (前面)",
|
||||
"Queue Selected Output Nodes": "選択した出力ノードをキューに追加",
|
||||
@@ -842,21 +834,25 @@
|
||||
"Show Keybindings Dialog": "キーバインドダイアログを表示",
|
||||
"Show Settings Dialog": "設定ダイアログを表示",
|
||||
"Sign Out": "サインアウト",
|
||||
"Toggle Bottom Panel": "下部パネルの切り替え",
|
||||
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",
|
||||
"Toggle Focus Mode": "フォーカスモードの切り替え",
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
|
||||
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
|
||||
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
|
||||
"Toggle Search Box": "検索ボックスの切り替え",
|
||||
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
|
||||
"Toggle Theme (Dark/Light)": "テーマを切り替え(ダーク/ライト)",
|
||||
"Toggle View Controls Bottom Panel": "ビューコントロール下部パネルの切り替え",
|
||||
"Toggle Workflows Sidebar": "ワークフローサイドバーを切り替え",
|
||||
"Toggle the Custom Nodes Manager": "カスタムノードマネージャーを切り替え",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "カスタムノードマネージャーの進行状況バーを切り替え",
|
||||
"Undo": "元に戻す",
|
||||
"Ungroup selected group nodes": "選択したグループノードのグループ解除",
|
||||
"Unpack the selected Subgraph": "選択したサブグラフを展開",
|
||||
"Workflows": "ワークフロー",
|
||||
"Workflow": "ワークフロー",
|
||||
"Zoom In": "ズームイン",
|
||||
"Zoom Out": "ズームアウト",
|
||||
"Zoom to fit": "全体表示にズーム"
|
||||
"Zoom Out": "ズームアウト"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "ノードの色",
|
||||
@@ -1201,13 +1197,6 @@
|
||||
"browseTemplates": "サンプルテンプレートを表示",
|
||||
"downloads": "ダウンロード",
|
||||
"helpCenter": "ヘルプセンター",
|
||||
"labels": {
|
||||
"models": "モデル",
|
||||
"nodes": "ノード",
|
||||
"queue": "キュー",
|
||||
"templates": "テンプレート",
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"logout": "ログアウト",
|
||||
"modelLibrary": "モデルライブラリ",
|
||||
"newBlankWorkflow": "新しい空のワークフローを作成",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "フラットリストを表示"
|
||||
},
|
||||
"templates": "テンプレート",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "このワークフローを削除してもよろしいですか?",
|
||||
"confirmDeleteTitle": "ワークフローを削除しますか?",
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "선택한 노드 그룹화"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "선택한 서브그래프 풀기"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "선택한 노드를 그룹 노드로 변환"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "클립스페이스"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "매니저"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "워크플로 열기"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "설정 대화상자 보기"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "캔버스 성능"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "도움말 센터"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "밝기 테마 전환 (어두운/밝은)"
|
||||
},
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
"versionMismatchWarning": "버전 호환성 경고",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} 업데이트 지침은 https://docs.comfy.org/installation/update_comfyui#common-update-issues 를 방문하세요.",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로"
|
||||
"workflow": "워크플로",
|
||||
"duplicate": "복제"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "보기 맞춤",
|
||||
@@ -749,7 +750,6 @@
|
||||
"manageExtensions": "확장 프로그램 관리",
|
||||
"onChange": "변경 시",
|
||||
"onChangeTooltip": "변경이 있는 경우에만 워크플로를 실행 대기열에 추가합니다.",
|
||||
"queue": "대기열 패널",
|
||||
"refresh": "노드 정의 새로 고침",
|
||||
"resetView": "캔버스 보기 재설정",
|
||||
"run": "실행",
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "ComfyUI에 대하여",
|
||||
"Add Edit Model Step": "모델 편집 단계 추가",
|
||||
"Bottom Panel": "하단 패널",
|
||||
"Browse Templates": "템플릿 탐색",
|
||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||
"Canvas Performance": "캔버스 성능",
|
||||
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
||||
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
||||
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
|
||||
"Check for Updates": "업데이트 확인",
|
||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||
"Clear Workflow": "워크플로 지우기",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "서브그래프 종료",
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"File": "파일",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
"Focus Mode": "포커스 모드",
|
||||
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Help Center": "도움말 센터",
|
||||
"Increase Brush Size in MaskEditor": "마스크 편집기에서 브러시 크기 늘리기",
|
||||
"Interrupt": "중단",
|
||||
"Load Default Workflow": "기본 워크플로 불러오기",
|
||||
"Manage group nodes": "그룹 노드 관리",
|
||||
"Manager": "매니저",
|
||||
"Minimap": "미니맵",
|
||||
"Model Library": "모델 라이브러리",
|
||||
"Move Selected Nodes Down": "선택한 노드 아래로 이동",
|
||||
"Move Selected Nodes Left": "선택한 노드 왼쪽으로 이동",
|
||||
"Move Selected Nodes Right": "선택한 노드 오른쪽으로 이동",
|
||||
@@ -810,8 +805,6 @@
|
||||
"Mute/Unmute Selected Nodes": "선택한 노드 활성화/비활성화",
|
||||
"New": "새로 만들기",
|
||||
"Next Opened Workflow": "다음 열린 워크플로",
|
||||
"Node Library": "노드 라이브러리",
|
||||
"Node Links": "노드 링크",
|
||||
"Open": "열기",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "선택한 노드에 대해 3D 뷰어(베타) 열기",
|
||||
"Open Custom Nodes Folder": "사용자 정의 노드 폴더 열기",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "선택한 항목 고정/고정 해제",
|
||||
"Pin/Unpin Selected Nodes": "선택한 노드 고정/고정 해제",
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Queue Panel": "대기열 패널",
|
||||
"Queue Prompt": "실행 대기열에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 대기열 맨 앞에 프롬프트 추가",
|
||||
"Queue Selected Output Nodes": "선택한 출력 노드 대기열에 추가",
|
||||
@@ -842,21 +834,25 @@
|
||||
"Show Keybindings Dialog": "키 바인딩 대화상자 표시",
|
||||
"Show Settings Dialog": "설정 대화상자 표시",
|
||||
"Sign Out": "로그아웃",
|
||||
"Toggle Bottom Panel": "하단 패널 전환",
|
||||
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",
|
||||
"Toggle Focus Mode": "포커스 모드 전환",
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Queue Sidebar": "실행 대기열 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
"Toggle Theme (Dark/Light)": "테마 전환 (어두운/밝은)",
|
||||
"Toggle View Controls Bottom Panel": "뷰 컨트롤 하단 패널 전환",
|
||||
"Toggle Workflows Sidebar": "워크플로 사이드바 전환",
|
||||
"Toggle the Custom Nodes Manager": "커스텀 노드 매니저 전환",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "커스텀 노드 매니저 진행률 표시줄 전환",
|
||||
"Undo": "실행 취소",
|
||||
"Ungroup selected group nodes": "선택한 그룹 노드 그룹 해제",
|
||||
"Unpack the selected Subgraph": "선택한 서브그래프 풀기",
|
||||
"Workflows": "워크플로우",
|
||||
"Workflow": "워크플로",
|
||||
"Zoom In": "확대",
|
||||
"Zoom Out": "축소",
|
||||
"Zoom to fit": "화면에 맞추기"
|
||||
"Zoom Out": "축소"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "노드 색상",
|
||||
@@ -1201,13 +1197,6 @@
|
||||
"browseTemplates": "예제 템플릿 탐색",
|
||||
"downloads": "다운로드",
|
||||
"helpCenter": "도움말 센터",
|
||||
"labels": {
|
||||
"models": "모델",
|
||||
"nodes": "노드",
|
||||
"queue": "대기열",
|
||||
"templates": "템플릿",
|
||||
"workflows": "워크플로우"
|
||||
},
|
||||
"logout": "로그아웃",
|
||||
"modelLibrary": "모델 라이브러리",
|
||||
"newBlankWorkflow": "새 빈 워크플로 만들기",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "평면 목록 표시"
|
||||
},
|
||||
"templates": "템플릿",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "정말로 이 워크플로를 삭제하시겠습니까?",
|
||||
"confirmDeleteTitle": "워크플로 삭제",
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Группировать выбранные ноды"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "Распаковать выбранный подграф"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Преобразовать выбранные ноды в групповую ноду"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Клипспейс"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "Менеджер"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "Открыть рабочий процесс"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "Показать диалог настроек"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "Производительность холста"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "Центр поддержки"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Переключить тему (Тёмная/Светлая)"
|
||||
},
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
"versionMismatchWarning": "Предупреждение о несовместимости версий",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} Посетите https://docs.comfy.org/installation/update_comfyui#common-update-issues для инструкций по обновлению.",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
"workflow": "Рабочий процесс",
|
||||
"duplicate": "Дублировать"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Подгонять под выделенные",
|
||||
@@ -749,7 +750,6 @@
|
||||
"manageExtensions": "Управление расширениями",
|
||||
"onChange": "При изменении",
|
||||
"onChangeTooltip": "Рабочий процесс будет поставлен в очередь после внесения изменений",
|
||||
"queue": "Панель очереди",
|
||||
"refresh": "Обновить определения нод",
|
||||
"resetView": "Сбросить вид холста",
|
||||
"run": "Запустить",
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "О ComfyUI",
|
||||
"Add Edit Model Step": "Добавить или изменить шаг модели",
|
||||
"Bottom Panel": "Нижняя панель",
|
||||
"Browse Templates": "Просмотреть шаблоны",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Performance": "Производительность холста",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "Выйти из подграфа",
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"File": "Файл",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
"Focus Mode": "Режим фокуса",
|
||||
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Help Center": "Центр поддержки",
|
||||
"Increase Brush Size in MaskEditor": "Увеличить размер кисти в MaskEditor",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
"Manager": "Менеджер",
|
||||
"Minimap": "Мини-карта",
|
||||
"Model Library": "Библиотека моделей",
|
||||
"Move Selected Nodes Down": "Переместить выбранные узлы вниз",
|
||||
"Move Selected Nodes Left": "Переместить выбранные узлы влево",
|
||||
"Move Selected Nodes Right": "Переместить выбранные узлы вправо",
|
||||
@@ -810,8 +805,6 @@
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
"Node Library": "Библиотека узлов",
|
||||
"Node Links": "Связи узлов",
|
||||
"Open": "Открыть",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "Открыть 3D-просмотрщик (бета) для выбранного узла",
|
||||
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
|
||||
"Queue Panel": "Панель очереди",
|
||||
"Queue Prompt": "Запрос в очереди",
|
||||
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
|
||||
"Queue Selected Output Nodes": "Добавить выбранные выходные узлы в очередь",
|
||||
@@ -842,21 +834,25 @@
|
||||
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
|
||||
"Show Settings Dialog": "Показать диалог настроек",
|
||||
"Sign Out": "Выйти",
|
||||
"Toggle Bottom Panel": "Переключить нижнюю панель",
|
||||
"Toggle Essential Bottom Panel": "Показать/скрыть основную нижнюю панель",
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
"Toggle Model Library Sidebar": "Показать/скрыть боковую панель библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Показать/скрыть боковую панель библиотеки узлов",
|
||||
"Toggle Queue Sidebar": "Показать/скрыть боковую панель очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
|
||||
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
|
||||
"Toggle View Controls Bottom Panel": "Показать/скрыть панель управления просмотром",
|
||||
"Toggle Workflows Sidebar": "Показать/скрыть боковую панель рабочих процессов",
|
||||
"Toggle the Custom Nodes Manager": "Переключить менеджер пользовательских узлов",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "Переключить индикатор выполнения менеджера пользовательских узлов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Unpack the selected Subgraph": "Распаковать выбранный подграф",
|
||||
"Workflows": "Рабочие процессы",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить",
|
||||
"Zoom to fit": "Масштабировать по размеру"
|
||||
"Zoom Out": "Уменьшить"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "Цвета узлов",
|
||||
@@ -1201,13 +1197,6 @@
|
||||
"browseTemplates": "Просмотреть примеры шаблонов",
|
||||
"downloads": "Загрузки",
|
||||
"helpCenter": "Центр поддержки",
|
||||
"labels": {
|
||||
"models": "Модели",
|
||||
"nodes": "Узлы",
|
||||
"queue": "Очередь",
|
||||
"templates": "Шаблоны",
|
||||
"workflows": "Воркфлоу"
|
||||
},
|
||||
"logout": "Выйти",
|
||||
"modelLibrary": "Библиотека моделей",
|
||||
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "Показать плоский список"
|
||||
},
|
||||
"templates": "Шаблоны",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "Вы уверены, что хотите удалить этот рабочий процесс?",
|
||||
"confirmDeleteTitle": "Удалить рабочий процесс?",
|
||||
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "群組所選節點"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "解開所選子圖"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "將選取的節點轉換為群組節點"
|
||||
},
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "Clipspace"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "管理器"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "開啟工作流程"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "顯示設定對話框"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "畫布效能"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "說明中心"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "切換主題(深色/淺色)"
|
||||
},
|
||||
|
||||
@@ -403,7 +403,8 @@
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
|
||||
"videoFailedToLoad": "無法載入影片",
|
||||
"workflow": "工作流程"
|
||||
"workflow": "工作流程",
|
||||
"duplicate": "複製"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "適合視窗",
|
||||
@@ -749,7 +750,6 @@
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "變更時",
|
||||
"onChangeTooltip": "每當有變更時,工作流程會排入佇列",
|
||||
"queue": "佇列面板",
|
||||
"refresh": "重新整理節點定義",
|
||||
"resetView": "重設畫布視圖",
|
||||
"run": "執行",
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "關於 ComfyUI",
|
||||
"Add Edit Model Step": "新增編輯模型步驟",
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "瀏覽範本",
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Performance": "畫布效能",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
"Clear Workflow": "清除工作流程",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "離開子圖",
|
||||
"Export": "匯出",
|
||||
"Export (API)": "匯出(API)",
|
||||
"File": "檔案",
|
||||
"Fit Group To Contents": "群組貼合內容",
|
||||
"Focus Mode": "專注模式",
|
||||
"Fit view to selected nodes": "視圖貼合選取節點",
|
||||
"Give Feedback": "提供意見回饋",
|
||||
"Group Selected Nodes": "群組選取節點",
|
||||
"Help": "說明",
|
||||
"Help Center": "說明中心",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Interrupt": "中斷",
|
||||
"Load Default Workflow": "載入預設工作流程",
|
||||
"Manage group nodes": "管理群組節點",
|
||||
"Manager": "管理員",
|
||||
"Minimap": "縮圖地圖",
|
||||
"Model Library": "模型庫",
|
||||
"Move Selected Nodes Down": "選取節點下移",
|
||||
"Move Selected Nodes Left": "選取節點左移",
|
||||
"Move Selected Nodes Right": "選取節點右移",
|
||||
@@ -810,8 +805,6 @@
|
||||
"Mute/Unmute Selected Nodes": "靜音/取消靜音選取節點",
|
||||
"New": "新增",
|
||||
"Next Opened Workflow": "下一個已開啟的工作流程",
|
||||
"Node Library": "節點庫",
|
||||
"Node Links": "節點連結",
|
||||
"Open": "開啟",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "為選取的節點開啟 3D 檢視器(Beta 版)",
|
||||
"Open Custom Nodes Folder": "開啟自訂節點資料夾",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "釘選/取消釘選選取項目",
|
||||
"Pin/Unpin Selected Nodes": "釘選/取消釘選選取節點",
|
||||
"Previous Opened Workflow": "上一個已開啟的工作流程",
|
||||
"Queue Panel": "佇列面板",
|
||||
"Queue Prompt": "加入提示至佇列",
|
||||
"Queue Prompt (Front)": "將提示加入佇列前端",
|
||||
"Queue Selected Output Nodes": "將選取的輸出節點加入佇列",
|
||||
@@ -842,21 +834,25 @@
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Settings Dialog": "顯示設定對話框",
|
||||
"Sign Out": "登出",
|
||||
"Toggle Bottom Panel": "切換下方面板",
|
||||
"Toggle Essential Bottom Panel": "切換基本下方面板",
|
||||
"Toggle Focus Mode": "切換專注模式",
|
||||
"Toggle Logs Bottom Panel": "切換日誌下方面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切換搜尋框",
|
||||
"Toggle Terminal Bottom Panel": "切換終端機底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切換主題(深色/淺色)",
|
||||
"Toggle View Controls Bottom Panel": "切換檢視控制下方面板",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切換自訂節點管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切換自訂節點管理器進度條",
|
||||
"Undo": "復原",
|
||||
"Ungroup selected group nodes": "取消群組選取的群組節點",
|
||||
"Unpack the selected Subgraph": "解包所選子圖",
|
||||
"Workflows": "工作流程",
|
||||
"Workflow": "工作流程",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "縮小",
|
||||
"Zoom to fit": "縮放至適合大小"
|
||||
"Zoom Out": "縮小"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "節點顏色",
|
||||
@@ -1201,13 +1197,6 @@
|
||||
"browseTemplates": "瀏覽範例模板",
|
||||
"downloads": "下載",
|
||||
"helpCenter": "說明中心",
|
||||
"labels": {
|
||||
"models": "模型",
|
||||
"nodes": "節點",
|
||||
"queue": "佇列",
|
||||
"templates": "範本",
|
||||
"workflows": "工作流程"
|
||||
},
|
||||
"logout": "登出",
|
||||
"modelLibrary": "模型庫",
|
||||
"newBlankWorkflow": "建立新的空白工作流程",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "顯示平面清單"
|
||||
},
|
||||
"templates": "範本",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您確定要刪除這個工作流程嗎?",
|
||||
"confirmDeleteTitle": "刪除工作流程?",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"label": "重启"
|
||||
},
|
||||
"Comfy_3DViewer_Open3DViewer": {
|
||||
"label": "为所选节点开启 3D 浏览器(Beta 版)"
|
||||
"label": "為所選節點開啟 3D 檢視器(Beta 版)"
|
||||
},
|
||||
"Comfy_BrowseTemplates": {
|
||||
"label": "浏览模板"
|
||||
@@ -75,7 +75,7 @@
|
||||
"label": "锁定视图"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "画布切换小地图"
|
||||
"label": "畫布切換小地圖"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "忽略/取消忽略选中节点"
|
||||
@@ -123,7 +123,7 @@
|
||||
"label": "将选区转换为子图"
|
||||
},
|
||||
"Comfy_Graph_ExitSubgraph": {
|
||||
"label": "退出子图"
|
||||
"label": "退出子圖"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "适应节点框到内容"
|
||||
@@ -131,9 +131,6 @@
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "添加框到选中节点"
|
||||
},
|
||||
"Comfy_Graph_UnpackSubgraph": {
|
||||
"label": "解开所选子图"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "将选中节点转换为组节点"
|
||||
},
|
||||
@@ -171,10 +168,10 @@
|
||||
"label": "切换进度对话框"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Decrease": {
|
||||
"label": "减小 MaskEditor 中的笔刷大小"
|
||||
"label": "減小 MaskEditor 中的筆刷大小"
|
||||
},
|
||||
"Comfy_MaskEditor_BrushSize_Increase": {
|
||||
"label": "增加 MaskEditor 画笔大小"
|
||||
"label": "增加 MaskEditor 畫筆大小"
|
||||
},
|
||||
"Comfy_MaskEditor_OpenMaskEditor": {
|
||||
"label": "打开选中节点的遮罩编辑器"
|
||||
@@ -185,9 +182,6 @@
|
||||
"Comfy_OpenClipspace": {
|
||||
"label": "打开剪贴板"
|
||||
},
|
||||
"Comfy_OpenManagerDialog": {
|
||||
"label": "管理器"
|
||||
},
|
||||
"Comfy_OpenWorkflow": {
|
||||
"label": "打开工作流"
|
||||
},
|
||||
@@ -215,12 +209,6 @@
|
||||
"Comfy_ShowSettingsDialog": {
|
||||
"label": "显示设置对话框"
|
||||
},
|
||||
"Comfy_ToggleCanvasInfo": {
|
||||
"label": "画布性能"
|
||||
},
|
||||
"Comfy_ToggleHelpCenter": {
|
||||
"label": "说明中心"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "切换主题"
|
||||
},
|
||||
@@ -255,13 +243,13 @@
|
||||
"label": "切换日志底部面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
|
||||
"label": "切换基础底部面板"
|
||||
"label": "切換基本下方面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
|
||||
"label": "切换视图控制底部面板"
|
||||
"label": "切換檢視控制底部面板"
|
||||
},
|
||||
"Workspace_ToggleBottomPanel_Shortcuts": {
|
||||
"label": "显示快捷键对话框"
|
||||
"label": "顯示快捷鍵對話框"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "切换焦点模式"
|
||||
|
||||
@@ -83,10 +83,10 @@
|
||||
}
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "清空工作流",
|
||||
"deleteWorkflow": "删除工作流",
|
||||
"duplicate": "复制",
|
||||
"enterNewName": "输入新名称"
|
||||
"clearWorkflow": "清除工作流程",
|
||||
"deleteWorkflow": "刪除工作流程",
|
||||
"duplicate": "複製",
|
||||
"enterNewName": "輸入新名稱"
|
||||
},
|
||||
"chatHistory": {
|
||||
"cancelEdit": "取消",
|
||||
@@ -218,7 +218,7 @@
|
||||
"WEBCAM": "摄像头"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "存在未保存的工作流;任何未保存的更改都将丢失。忽略此警告并退出?",
|
||||
"confirmQuit": "有未保存的工作流程开启;任何未保存的更改都将丢失。忽略此警告并退出?",
|
||||
"confirmReinstall": "这将清除您的 extra_models_config.yaml 文件,并重新开始安装。您确定吗?",
|
||||
"quit": "退出",
|
||||
"reinstall": "重新安装"
|
||||
@@ -272,7 +272,7 @@
|
||||
"category": "类别",
|
||||
"choose_file_to_upload": "选择要上传的文件",
|
||||
"clear": "清除",
|
||||
"clearFilters": "清除筛选",
|
||||
"clearFilters": "清除篩選",
|
||||
"close": "关闭",
|
||||
"color": "颜色",
|
||||
"comingSoon": "即将推出",
|
||||
@@ -297,7 +297,7 @@
|
||||
"devices": "设备",
|
||||
"disableAll": "禁用全部",
|
||||
"disabling": "禁用中",
|
||||
"dismiss": "关闭",
|
||||
"dismiss": "關閉",
|
||||
"download": "下载",
|
||||
"duplicate": "复制",
|
||||
"edit": "编辑",
|
||||
@@ -313,8 +313,8 @@
|
||||
"filter": "过滤",
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能与后端版本 {backendVersion} 不兼容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "转到节点",
|
||||
"help": "帮助",
|
||||
"icon": "图标",
|
||||
@@ -400,10 +400,11 @@
|
||||
"upload": "上传",
|
||||
"usageHint": "使用提示",
|
||||
"user": "用户",
|
||||
"versionMismatchWarning": "版本兼容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 请参考 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新说明。",
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
"workflow": "工作流",
|
||||
"duplicate": "复制"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "适应视图",
|
||||
@@ -411,7 +412,7 @@
|
||||
"resetView": "重置视图",
|
||||
"selectMode": "选择模式",
|
||||
"toggleLinkVisibility": "切换连线可见性",
|
||||
"toggleMinimap": "切换小地图",
|
||||
"toggleMinimap": "切換小地圖",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
@@ -594,7 +595,7 @@
|
||||
"wireframe": "线框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 浏览器中打开",
|
||||
"openIn3DViewer": "在 3D 檢視器中開啟",
|
||||
"previewOutput": "预览输出",
|
||||
"removeBackgroundImage": "移除背景图片",
|
||||
"resizeNodeMatchOutput": "调整节点以匹配输出",
|
||||
@@ -612,14 +613,14 @@
|
||||
"uploadTexture": "上传纹理",
|
||||
"viewer": {
|
||||
"apply": "套用",
|
||||
"cameraSettings": "相机设置",
|
||||
"cameraType": "相机类型",
|
||||
"cameraSettings": "相機設定",
|
||||
"cameraType": "相機類型",
|
||||
"cancel": "取消",
|
||||
"exportSettings": "导出设置",
|
||||
"lightSettings": "灯光设置",
|
||||
"modelSettings": "模型设置",
|
||||
"sceneSettings": "场景设置",
|
||||
"title": "3D 浏览器(测试版)"
|
||||
"exportSettings": "匯出設定",
|
||||
"lightSettings": "燈光設定",
|
||||
"modelSettings": "模型設定",
|
||||
"sceneSettings": "場景設定",
|
||||
"title": "3D 檢視器(測試版)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
@@ -740,22 +741,21 @@
|
||||
"disabled": "禁用",
|
||||
"disabledTooltip": "工作流将不会自动执行",
|
||||
"execute": "执行",
|
||||
"help": "帮助",
|
||||
"help": "說明",
|
||||
"hideMenu": "隐藏菜单",
|
||||
"instant": "实时",
|
||||
"instantTooltip": "工作流将会在生成完成后立即执行",
|
||||
"interrupt": "取消当前任务",
|
||||
"light": "淺色",
|
||||
"manageExtensions": "管理扩展功能",
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "更改时",
|
||||
"onChangeTooltip": "一旦进行更改,工作流将添加到执行队列",
|
||||
"queue": "队列面板",
|
||||
"refresh": "刷新节点",
|
||||
"resetView": "重置视图",
|
||||
"run": "运行",
|
||||
"runWorkflow": "运行工作流(Shift插队)",
|
||||
"runWorkflowFront": "运行工作流(插队)",
|
||||
"settings": "设置",
|
||||
"runWorkflow": "运行工作流程(Shift排在前面)",
|
||||
"runWorkflowFront": "运行工作流程(排在前面)",
|
||||
"settings": "設定",
|
||||
"showMenu": "显示菜单",
|
||||
"theme": "主題",
|
||||
"toggleBottomPanel": "底部面板"
|
||||
@@ -763,11 +763,11 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "关于ComfyUI",
|
||||
"Add Edit Model Step": "添加编辑模型步骤",
|
||||
"Bottom Panel": "底部面板",
|
||||
"Browse Templates": "浏览模板",
|
||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||
"Canvas Performance": "画布性能",
|
||||
"Canvas Toggle Link Visibility": "切换连线可见性",
|
||||
"Canvas Toggle Lock": "切换视图锁定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Updates": "检查更新",
|
||||
"Clear Pending Tasks": "清除待处理任务",
|
||||
"Clear Workflow": "清除工作流",
|
||||
@@ -781,7 +781,7 @@
|
||||
"Contact Support": "联系支持",
|
||||
"Convert Selection to Subgraph": "将选中内容转换为子图",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中减小笔刷大小",
|
||||
"Decrease Brush Size in MaskEditor": "在 MaskEditor 中減小筆刷大小",
|
||||
"Delete Selected Items": "删除选定的项目",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
@@ -789,20 +789,15 @@
|
||||
"Exit Subgraph": "退出子圖",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"File": "文件",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
"Focus Mode": "专注模式",
|
||||
"Fit view to selected nodes": "适应视图到选中节点",
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Help Center": "帮助中心",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大笔刷大小",
|
||||
"Increase Brush Size in MaskEditor": "在 MaskEditor 中增大筆刷大小",
|
||||
"Interrupt": "中断",
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
"Manager": "管理器",
|
||||
"Minimap": "缩略地图",
|
||||
"Model Library": "模型库",
|
||||
"Move Selected Nodes Down": "下移所选节点",
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
"Move Selected Nodes Right": "右移所选节点",
|
||||
@@ -810,10 +805,8 @@
|
||||
"Mute/Unmute Selected Nodes": "静音/取消静音选定节点",
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
"Node Library": "节点库",
|
||||
"Node Links": "节点连线",
|
||||
"Open": "打开",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "为所选节点开启 3D 浏览器(Beta 版)",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "為所選節點開啟 3D 檢視器(Beta 版)",
|
||||
"Open Custom Nodes Folder": "打开自定义节点文件夹",
|
||||
"Open DevTools": "打开开发者工具",
|
||||
"Open Inputs Folder": "打开输入文件夹",
|
||||
@@ -826,7 +819,6 @@
|
||||
"Pin/Unpin Selected Items": "固定/取消固定选定项目",
|
||||
"Pin/Unpin Selected Nodes": "固定/取消固定选定节点",
|
||||
"Previous Opened Workflow": "上一个打开的工作流",
|
||||
"Queue Panel": "队列面板",
|
||||
"Queue Prompt": "执行提示词",
|
||||
"Queue Prompt (Front)": "执行提示词 (优先执行)",
|
||||
"Queue Selected Output Nodes": "将所选输出节点加入队列",
|
||||
@@ -839,31 +831,35 @@
|
||||
"Restart": "重启",
|
||||
"Save": "保存",
|
||||
"Save As": "另存为",
|
||||
"Show Keybindings Dialog": "显示快捷键对话框",
|
||||
"Show Keybindings Dialog": "顯示快捷鍵對話框",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Essential Bottom Panel": "切换基本下方面板",
|
||||
"Toggle Bottom Panel": "切换底部面板",
|
||||
"Toggle Essential Bottom Panel": "切換基本下方面板",
|
||||
"Toggle Focus Mode": "切换专注模式",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切換模型庫側邊欄",
|
||||
"Toggle Node Library Sidebar": "切換節點庫側邊欄",
|
||||
"Toggle Queue Sidebar": "切換佇列側邊欄",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle View Controls Bottom Panel": "切换视图控制下方面板",
|
||||
"Toggle View Controls Bottom Panel": "切換檢視控制下方面板",
|
||||
"Toggle Workflows Sidebar": "切換工作流程側邊欄",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
"Unpack the selected Subgraph": "解開所選子圖",
|
||||
"Workflows": "工作流程",
|
||||
"Workflow": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面",
|
||||
"Zoom to fit": "缩放至适合大小"
|
||||
"Zoom Out": "缩小画面"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "节点颜色",
|
||||
"renderBypassState": "显示绕过状态",
|
||||
"renderErrorState": "显示错误状态",
|
||||
"showGroups": "显示框架/群组",
|
||||
"showLinks": "显示连线"
|
||||
"nodeColors": "節點顏色",
|
||||
"renderBypassState": "顯示繞過狀態",
|
||||
"renderErrorState": "顯示錯誤狀態",
|
||||
"showGroups": "顯示框架/群組",
|
||||
"showLinks": "顯示連結"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
@@ -1131,7 +1127,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D 浏览器",
|
||||
"3DViewer": "3D 檢視器",
|
||||
"API Nodes": "API 节点",
|
||||
"About": "关于",
|
||||
"Appearance": "外观",
|
||||
@@ -1185,29 +1181,22 @@
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本功能",
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"manageShortcuts": "管理快捷键",
|
||||
"noKeybinding": "无快捷键",
|
||||
"keyboardShortcuts": "鍵盤快捷鍵",
|
||||
"manageShortcuts": "管理快捷鍵",
|
||||
"noKeybinding": "無快捷鍵",
|
||||
"subcategories": {
|
||||
"node": "节点",
|
||||
"node": "節點",
|
||||
"panelControls": "面板控制",
|
||||
"queue": "队列",
|
||||
"view": "视图",
|
||||
"workflow": "工作流"
|
||||
"queue": "佇列",
|
||||
"view": "檢視",
|
||||
"workflow": "工作流程"
|
||||
},
|
||||
"viewControls": "视图控制"
|
||||
"viewControls": "檢視控制"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"browseTemplates": "浏览示例模板",
|
||||
"downloads": "下载",
|
||||
"helpCenter": "帮助中心",
|
||||
"labels": {
|
||||
"models": "模型",
|
||||
"nodes": "节点",
|
||||
"queue": "队列",
|
||||
"templates": "模板",
|
||||
"workflows": "工作流"
|
||||
},
|
||||
"logout": "登出",
|
||||
"modelLibrary": "模型库",
|
||||
"newBlankWorkflow": "创建空白工作流",
|
||||
@@ -1245,7 +1234,6 @@
|
||||
},
|
||||
"showFlatList": "平铺结果"
|
||||
},
|
||||
"templates": "模板",
|
||||
"workflowTab": {
|
||||
"confirmDelete": "您确定要删除此工作流吗?",
|
||||
"confirmDeleteTitle": "删除工作流?",
|
||||
@@ -1292,8 +1280,8 @@
|
||||
"Video": "视频生成",
|
||||
"Video API": "视频 API"
|
||||
},
|
||||
"loadingMore": "正在加载更多模板...",
|
||||
"searchPlaceholder": "搜索模板...",
|
||||
"loadingMore": "正在載入更多範本...",
|
||||
"searchPlaceholder": "搜尋範本...",
|
||||
"template": {
|
||||
"3D": {
|
||||
"3d_hunyuan3d_image_to_model": "混元3D 2.0 图生模型",
|
||||
@@ -1616,7 +1604,7 @@
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchBalance": "获取余额失败:{error}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 浏览器失败",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 檢視器失敗",
|
||||
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
|
||||
"failedToPurchaseCredits": "购买积分失败:{error}",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
@@ -1673,7 +1661,7 @@
|
||||
"required": "必填"
|
||||
},
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "关闭",
|
||||
"dismiss": "關閉",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"title": "版本相容性警告",
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
"tooltip": "画布背景的图像 URL。你可以在输出面板中右键点击一张图片,并选择“设为背景”来使用它。"
|
||||
},
|
||||
"Comfy_Canvas_NavigationMode": {
|
||||
"name": "画布导航模式",
|
||||
"name": "畫布導航模式",
|
||||
"options": {
|
||||
"Left-Click Pan (Legacy)": "左键拖曳(旧版)",
|
||||
"Standard (New)": "标准(新)"
|
||||
"Left-Click Pan (Legacy)": "左鍵拖曳(舊版)",
|
||||
"Standard (New)": "標準(新)"
|
||||
}
|
||||
},
|
||||
"Comfy_Canvas_SelectionToolbox": {
|
||||
@@ -120,8 +120,8 @@
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "启用 3D 浏览器(测试版)",
|
||||
"tooltip": "为所选节点启用 3D 浏览器(测试版)。此功能可让您直接在全尺寸 3D 浏览器中浏览并与 3D 模型交互。"
|
||||
"name": "啟用 3D 檢視器(測試版)",
|
||||
"tooltip": "為所選節點啟用 3D 檢視器(測試版)。此功能可讓您直接在全尺寸 3D 檢視器中瀏覽並互動 3D 模型。"
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初始背景颜色",
|
||||
@@ -338,7 +338,7 @@
|
||||
"Disabled": "禁用",
|
||||
"Top": "顶部"
|
||||
},
|
||||
"tooltip": "菜单列位置。在移动设备上,菜单始终显示于顶端。"
|
||||
"tooltip": "選單列位置。在行動裝置上,選單始終顯示於頂端。"
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "校验工作流"
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import type {
|
||||
HeaderMap,
|
||||
HeaderProviderContext,
|
||||
IHeaderProvider
|
||||
} from '@/types/headerTypes'
|
||||
|
||||
/**
|
||||
* Header provider for authentication headers.
|
||||
* Automatically adds Firebase Bearer tokens or API keys to outgoing requests.
|
||||
*
|
||||
* Priority order:
|
||||
* 1. Firebase Bearer token (if user is authenticated)
|
||||
* 2. API key (if configured)
|
||||
* 3. No authentication header
|
||||
*/
|
||||
export class AuthHeaderProvider implements IHeaderProvider {
|
||||
async provideHeaders(_context: HeaderProviderContext): Promise<HeaderMap> {
|
||||
// Try to get Firebase auth header first (includes fallback to API key)
|
||||
const authHeader = await useFirebaseAuthStore().getAuthHeader()
|
||||
|
||||
if (authHeader) {
|
||||
return authHeader
|
||||
}
|
||||
|
||||
// No authentication available
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Header provider specifically for API key authentication.
|
||||
* Only provides API key headers, ignoring Firebase auth.
|
||||
* Useful for specific endpoints that require API key auth.
|
||||
*/
|
||||
export class ApiKeyHeaderProvider implements IHeaderProvider {
|
||||
provideHeaders(_context: HeaderProviderContext): HeaderMap {
|
||||
const apiKeyHeader = useApiKeyAuthStore().getAuthHeader()
|
||||
return apiKeyHeader || {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Header provider specifically for Firebase Bearer token authentication.
|
||||
* Only provides Firebase auth headers, ignoring API keys.
|
||||
* Useful for specific endpoints that require Firebase auth.
|
||||
*/
|
||||
export class FirebaseAuthHeaderProvider implements IHeaderProvider {
|
||||
async provideHeaders(_context: HeaderProviderContext): Promise<HeaderMap> {
|
||||
const firebaseStore = useFirebaseAuthStore()
|
||||
|
||||
// Only get Firebase token, not the fallback API key
|
||||
const token = await firebaseStore.getIdToken()
|
||||
|
||||
if (token) {
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import axios from 'axios'
|
||||
|
||||
import defaultClientFeatureFlags from '@/config/clientFeatureFlags.json'
|
||||
import type {
|
||||
DisplayComponentWsMessage,
|
||||
@@ -33,7 +35,6 @@ import type {
|
||||
NodeId
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { WorkflowTemplates } from '@/types/workflowTemplateTypes'
|
||||
|
||||
@@ -328,7 +329,7 @@ export class ComfyApi extends EventTarget {
|
||||
} else {
|
||||
options.headers['Comfy-User'] = this.user
|
||||
}
|
||||
return fetchWithHeaders(this.apiURL(route), options)
|
||||
return fetch(this.apiURL(route), options)
|
||||
}
|
||||
|
||||
override addEventListener<TEvent extends keyof ApiEvents>(
|
||||
@@ -598,9 +599,9 @@ export class ComfyApi extends EventTarget {
|
||||
* Gets the index of core workflow templates.
|
||||
*/
|
||||
async getCoreWorkflowTemplates(): Promise<WorkflowTemplates[]> {
|
||||
const res = await fetchWithHeaders(this.fileURL('/templates/index.json'))
|
||||
const contentType = res.headers.get('content-type')
|
||||
return contentType?.includes('application/json') ? await res.json() : []
|
||||
const res = await axios.get(this.fileURL('/templates/index.json'))
|
||||
const contentType = res.headers['content-type']
|
||||
return contentType?.includes('application/json') ? res.data : []
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1001,31 +1002,22 @@ export class ComfyApi extends EventTarget {
|
||||
}
|
||||
|
||||
async getLogs(): Promise<string> {
|
||||
const response = await fetchWithHeaders(this.internalURL('/logs'))
|
||||
return response.text()
|
||||
return (await axios.get(this.internalURL('/logs'))).data
|
||||
}
|
||||
|
||||
async getRawLogs(): Promise<LogsRawResponse> {
|
||||
const response = await fetchWithHeaders(this.internalURL('/logs/raw'))
|
||||
return response.json()
|
||||
return (await axios.get(this.internalURL('/logs/raw'))).data
|
||||
}
|
||||
|
||||
async subscribeLogs(enabled: boolean): Promise<void> {
|
||||
await fetchWithHeaders(this.internalURL('/logs/subscribe'), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
enabled,
|
||||
clientId: this.clientId
|
||||
})
|
||||
return await axios.patch(this.internalURL('/logs/subscribe'), {
|
||||
enabled,
|
||||
clientId: this.clientId
|
||||
})
|
||||
}
|
||||
|
||||
async getFolderPaths(): Promise<Record<string, string[]>> {
|
||||
const response = await fetchWithHeaders(this.internalURL('/folder_paths'))
|
||||
return response.json()
|
||||
return (await axios.get(this.internalURL('/folder_paths'))).data
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1034,8 +1026,7 @@ export class ComfyApi extends EventTarget {
|
||||
* @returns The custom nodes i18n data
|
||||
*/
|
||||
async getCustomNodesI18n(): Promise<Record<string, any>> {
|
||||
const response = await fetchWithHeaders(this.apiURL('/i18n'))
|
||||
return response.json()
|
||||
return (await axios.get(this.apiURL('/i18n'))).data
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,6 @@ import { getSvgMetadata } from '@/scripts/metadata/svg'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useSubgraphService } from '@/services/subgraphService'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
@@ -534,7 +533,7 @@ export class ComfyApp {
|
||||
if (match) {
|
||||
const uri = event.dataTransfer.getData(match)?.split('\n')?.[0]
|
||||
if (uri) {
|
||||
const blob = await (await fetchWithHeaders(uri)).blob()
|
||||
const blob = await (await fetch(uri)).blob()
|
||||
await this.handleFile(new File([blob], uri, { type: blob.type }))
|
||||
}
|
||||
}
|
||||
@@ -799,9 +798,6 @@ export class ComfyApp {
|
||||
await useWorkspaceStore().workflow.syncWorkflows()
|
||||
await useExtensionService().loadExtensions()
|
||||
|
||||
// Call preInit hook before any other initialization
|
||||
await useExtensionService().invokeExtensionsAsync('preInit')
|
||||
|
||||
this.#addProcessKeyHandler()
|
||||
this.#addConfigureHandler()
|
||||
this.#addApiUpdateHandlers()
|
||||
|
||||
@@ -2,7 +2,6 @@ import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { createAxiosWithHeaders } from '@/services/networkClientAdapter'
|
||||
import {
|
||||
type InstallPackParams,
|
||||
type InstalledPacksResponse,
|
||||
@@ -36,7 +35,7 @@ enum ManagerRoute {
|
||||
REBOOT = 'manager/reboot'
|
||||
}
|
||||
|
||||
const managerApiClient = createAxiosWithHeaders({
|
||||
const managerApiClient = axios.create({
|
||||
baseURL: api.apiURL(''),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { createAxiosWithHeaders } from '@/services/networkClientAdapter'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
const API_BASE_URL = 'https://api.comfy.org'
|
||||
|
||||
const registryApiClient = createAxiosWithHeaders({
|
||||
const registryApiClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { createAxiosWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { type components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
@@ -23,7 +22,7 @@ type CustomerEventsResponseQuery =
|
||||
|
||||
export type AuditLog = components['schemas']['AuditLog']
|
||||
|
||||
const customerApiClient = createAxiosWithHeaders({
|
||||
const customerApiClient = axios.create({
|
||||
baseURL: COMFY_API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import type {
|
||||
HeaderMap,
|
||||
HeaderProviderContext,
|
||||
HeaderProviderOptions,
|
||||
HeaderValue,
|
||||
IHeaderProvider,
|
||||
IHeaderProviderRegistration
|
||||
} from '@/types/headerTypes'
|
||||
|
||||
/**
|
||||
* Internal registration entry
|
||||
*/
|
||||
interface HeaderProviderEntry {
|
||||
id: string
|
||||
provider: IHeaderProvider
|
||||
options: HeaderProviderOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for HTTP header providers
|
||||
* Follows VSCode extension patterns for registration and lifecycle
|
||||
*/
|
||||
class HeaderRegistry {
|
||||
private providers: HeaderProviderEntry[] = []
|
||||
private nextId = 1
|
||||
|
||||
/**
|
||||
* Registers a header provider
|
||||
* @param provider - The header provider implementation
|
||||
* @param options - Registration options
|
||||
* @returns Registration handle for disposal
|
||||
*/
|
||||
registerHeaderProvider(
|
||||
provider: IHeaderProvider,
|
||||
options: HeaderProviderOptions = {}
|
||||
): IHeaderProviderRegistration {
|
||||
const id = `header-provider-${this.nextId++}`
|
||||
|
||||
const entry: HeaderProviderEntry = {
|
||||
id,
|
||||
provider,
|
||||
options: {
|
||||
priority: options.priority ?? 0,
|
||||
filter: options.filter
|
||||
}
|
||||
}
|
||||
|
||||
// Insert provider in priority order (higher priority = later in array)
|
||||
const insertIndex = this.providers.findIndex(
|
||||
(p) => (p.options.priority ?? 0) > (entry.options.priority ?? 0)
|
||||
)
|
||||
if (insertIndex === -1) {
|
||||
this.providers.push(entry)
|
||||
} else {
|
||||
this.providers.splice(insertIndex, 0, entry)
|
||||
}
|
||||
|
||||
// Return disposable handle
|
||||
return {
|
||||
id,
|
||||
dispose: () => {
|
||||
const index = this.providers.findIndex((p) => p.id === id)
|
||||
if (index !== -1) {
|
||||
this.providers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all headers for a request by combining all registered providers
|
||||
* @param context - Request context
|
||||
* @returns Combined headers from all providers
|
||||
*/
|
||||
async getHeaders(context: HeaderProviderContext): Promise<HeaderMap> {
|
||||
const result: HeaderMap = {}
|
||||
|
||||
// Process providers in order (lower priority first, so higher priority can override)
|
||||
for (const entry of this.providers) {
|
||||
// Check filter if provided
|
||||
if (entry.options.filter && !entry.options.filter(context)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = await entry.provider.provideHeaders(context)
|
||||
|
||||
// Merge headers, resolving any function values
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
result[key] = await this.resolveHeaderValue(value)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error getting headers from provider ${entry.id}:`, error)
|
||||
// Continue with other providers even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a header value, handling functions
|
||||
*/
|
||||
private async resolveHeaderValue(
|
||||
value: HeaderValue
|
||||
): Promise<string | number | boolean> {
|
||||
if (typeof value === 'function') {
|
||||
return await value()
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all registered providers
|
||||
*/
|
||||
clear(): void {
|
||||
this.providers = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the count of registered providers
|
||||
*/
|
||||
get providerCount(): number {
|
||||
return this.providers.length
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const headerRegistry = new HeaderRegistry()
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
|
||||
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
|
||||
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
|
||||
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
@@ -64,7 +63,6 @@ export const useLitegraphService = () => {
|
||||
const toastStore = useToastStore()
|
||||
const widgetStore = useWidgetStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
|
||||
|
||||
// TODO: Dedupe `registerNodeDef`; this should remain synchronous.
|
||||
function registerSubgraphNodeDef(
|
||||
@@ -764,8 +762,15 @@ export const useLitegraphService = () => {
|
||||
options.push({
|
||||
content: 'Bypass',
|
||||
callback: () => {
|
||||
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
||||
app.canvas.setDirty(true, true)
|
||||
const mode =
|
||||
this.mode === LGraphEventMode.BYPASS
|
||||
? LGraphEventMode.ALWAYS
|
||||
: LGraphEventMode.BYPASS
|
||||
for (const item of app.canvas.selectedItems) {
|
||||
if (item instanceof LGraphNode) item.mode = mode
|
||||
}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
import { headerRegistry } from '@/services/headerRegistry'
|
||||
import type { HeaderProviderContext } from '@/types/headerTypes'
|
||||
|
||||
/**
|
||||
* Creates an axios instance with automatic header injection from the registry
|
||||
* @param config - Base axios configuration
|
||||
* @returns Axios instance with header injection
|
||||
*/
|
||||
export function createAxiosWithHeaders(
|
||||
config?: AxiosRequestConfig
|
||||
): AxiosInstance {
|
||||
const instance = axios.create(config)
|
||||
|
||||
// Add request interceptor to inject headers
|
||||
instance.interceptors.request.use(
|
||||
async (requestConfig) => {
|
||||
// Build context for header providers
|
||||
const context: HeaderProviderContext = {
|
||||
url: requestConfig.url || '',
|
||||
method: requestConfig.method || 'GET',
|
||||
body: requestConfig.data,
|
||||
config: requestConfig
|
||||
}
|
||||
|
||||
// Get headers from registry
|
||||
const registryHeaders = await headerRegistry.getHeaders(context)
|
||||
|
||||
// Merge with existing headers (registry headers take precedence)
|
||||
for (const [key, value] of Object.entries(registryHeaders)) {
|
||||
requestConfig.headers[key] = value
|
||||
}
|
||||
|
||||
return requestConfig
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the native fetch API with header injection from the registry
|
||||
* @param input - Request URL or Request object
|
||||
* @param init - Request initialization options
|
||||
* @returns Promise resolving to Response
|
||||
*/
|
||||
export async function fetchWithHeaders(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit
|
||||
): Promise<Response> {
|
||||
// Extract URL and method for context
|
||||
const url =
|
||||
typeof input === 'string'
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.toString()
|
||||
: input.url
|
||||
const method =
|
||||
init?.method || (input instanceof Request ? input.method : 'GET')
|
||||
|
||||
// Build context for header providers
|
||||
const context: HeaderProviderContext = {
|
||||
url,
|
||||
method,
|
||||
body: init?.body
|
||||
}
|
||||
|
||||
// Get headers from registry
|
||||
const registryHeaders = await headerRegistry.getHeaders(context)
|
||||
|
||||
// Convert registry headers to Headers object format
|
||||
const headers = new Headers(init?.headers)
|
||||
for (const [key, value] of Object.entries(registryHeaders)) {
|
||||
headers.set(key, String(value))
|
||||
}
|
||||
|
||||
// Perform fetch with merged headers
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { api } from '@/scripts/api'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
||||
import { extractCustomNodeName } from '@/utils/nodeHelpUtil'
|
||||
@@ -26,12 +25,12 @@ export class NodeHelpService {
|
||||
|
||||
// Try locale-specific path first
|
||||
const localePath = `/extensions/${customNodeName}/docs/${node.name}/${locale}.md`
|
||||
let res = await fetchWithHeaders(api.fileURL(localePath))
|
||||
let res = await fetch(api.fileURL(localePath))
|
||||
|
||||
if (!res.ok) {
|
||||
// Fall back to non-locale path
|
||||
const fallbackPath = `/extensions/${customNodeName}/docs/${node.name}.md`
|
||||
res = await fetchWithHeaders(api.fileURL(fallbackPath))
|
||||
res = await fetch(api.fileURL(fallbackPath))
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -46,7 +45,7 @@ export class NodeHelpService {
|
||||
locale: string
|
||||
): Promise<string> {
|
||||
const mdUrl = `/docs/${node.name}/${locale}.md`
|
||||
const res = await fetchWithHeaders(api.fileURL(mdUrl))
|
||||
const res = await fetch(api.fileURL(mdUrl))
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText)
|
||||
|
||||
@@ -2,11 +2,10 @@ import axios, { AxiosError, AxiosResponse } from 'axios'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { createAxiosWithHeaders } from '@/services/networkClientAdapter'
|
||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||
import { isAbortError } from '@/utils/typeGuardUtil'
|
||||
|
||||
const releaseApiClient = createAxiosWithHeaders({
|
||||
const releaseApiClient = axios.create({
|
||||
baseURL: COMFY_API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -17,7 +17,6 @@ export interface ComfyCommand {
|
||||
versionAdded?: string
|
||||
confirmation?: string // If non-nullish, this command will prompt for confirmation
|
||||
source?: string
|
||||
active?: () => boolean // Getter to check if the command is active/toggled on
|
||||
category?: 'essentials' | 'view-controls' // For shortcuts panel organization
|
||||
}
|
||||
|
||||
@@ -31,7 +30,6 @@ export class ComfyCommandImpl implements ComfyCommand {
|
||||
versionAdded?: string
|
||||
confirmation?: string
|
||||
source?: string
|
||||
active?: () => boolean
|
||||
category?: 'essentials' | 'view-controls'
|
||||
|
||||
constructor(command: ComfyCommand) {
|
||||
@@ -44,7 +42,6 @@ export class ComfyCommandImpl implements ComfyCommand {
|
||||
this.versionAdded = command.versionAdded
|
||||
this.confirmation = command.confirmation
|
||||
this.source = command.source
|
||||
this.active = command.active
|
||||
this.category = command.category
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios'
|
||||
import {
|
||||
type Auth,
|
||||
GithubAuthProvider,
|
||||
@@ -21,7 +20,6 @@ import { useFirebaseAuth } from 'vuefire'
|
||||
|
||||
import { COMFY_API_BASE_URL } from '@/config/comfyApi'
|
||||
import { t } from '@/i18n'
|
||||
import { createAxiosWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
|
||||
import { type AuthHeader } from '@/types/authTypes'
|
||||
import { operations } from '@/types/comfyRegistryTypes'
|
||||
@@ -46,15 +44,6 @@ export class FirebaseAuthStoreError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// Customer API client - follows the same pattern as other services
|
||||
// Now with automatic header injection from the registry
|
||||
const customerApiClient = createAxiosWithHeaders({
|
||||
baseURL: COMFY_API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
// State
|
||||
const loading = ref(false)
|
||||
@@ -140,27 +129,27 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
)
|
||||
}
|
||||
|
||||
let balanceData
|
||||
try {
|
||||
const response = await customerApiClient.get('/customers/balance', {
|
||||
headers: authHeader
|
||||
})
|
||||
balanceData = response.data
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
if (error.response.status === 404) {
|
||||
// Customer not found is expected for new users
|
||||
return null
|
||||
}
|
||||
const errorData = error.response.data
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToFetchBalance', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/balance`, {
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
throw error
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
// Customer not found is expected for new users
|
||||
return null
|
||||
}
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToFetchBalance', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const balanceData = await response.json()
|
||||
// Update the last balance update time
|
||||
lastBalanceUpdateTime.value = new Date()
|
||||
balance.value = balanceData
|
||||
@@ -176,26 +165,23 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
let createCustomerResJson: CreateCustomerResponse
|
||||
try {
|
||||
const createCustomerRes = await customerApiClient.post(
|
||||
'/customers',
|
||||
{},
|
||||
{
|
||||
headers: authHeader
|
||||
}
|
||||
)
|
||||
createCustomerResJson = createCustomerRes.data
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: error.response?.statusText || error.message
|
||||
})
|
||||
)
|
||||
const createCustomerRes = await fetch(`${COMFY_API_BASE_URL}/customers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
throw error
|
||||
})
|
||||
if (!createCustomerRes.ok) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
error: createCustomerRes.statusText
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const createCustomerResJson: CreateCustomerResponse =
|
||||
await createCustomerRes.json()
|
||||
if (!createCustomerResJson?.id) {
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToCreateCustomer', {
|
||||
@@ -296,26 +282,25 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
customerCreated.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await customerApiClient.post(
|
||||
'/customers/credit',
|
||||
requestBodyContent,
|
||||
{
|
||||
headers: authHeader
|
||||
}
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/credit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBodyContent)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToInitiateCreditPurchase', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const errorData = error.response.data
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToInitiateCreditPurchase', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const initiateCreditPurchase = async (
|
||||
@@ -331,26 +316,27 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
|
||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await customerApiClient.post(
|
||||
'/customers/billing',
|
||||
requestBody,
|
||||
{
|
||||
headers: authHeader
|
||||
}
|
||||
const response = await fetch(`${COMFY_API_BASE_URL}/customers/billing`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...authHeader,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
...(requestBody && {
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const errorData = error.response.data
|
||||
throw new FirebaseAuthStoreError(
|
||||
t('toastMessages.failedToAccessBillingPortal', {
|
||||
error: errorData.message
|
||||
})
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useHelpCenterStore = defineStore('helpCenter', () => {
|
||||
const isVisible = ref(false)
|
||||
|
||||
const toggle = () => {
|
||||
isVisible.value = !isVisible.value
|
||||
}
|
||||
|
||||
const show = () => {
|
||||
isVisible.value = true
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
isVisible.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
toggle,
|
||||
show,
|
||||
hide
|
||||
}
|
||||
})
|
||||
@@ -10,7 +10,6 @@ import { useCommandStore } from './commandStore'
|
||||
export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
const commandStore = useCommandStore()
|
||||
const menuItems = ref<MenuItem[]>([])
|
||||
const menuItemHasActiveStateChildren = ref<Record<string, boolean>>({})
|
||||
|
||||
const registerMenuGroup = (path: string[], items: MenuItem[]) => {
|
||||
let currentLevel = menuItems.value
|
||||
@@ -46,14 +45,6 @@ export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
}
|
||||
// Add the new items to the last level
|
||||
currentLevel.push(...items)
|
||||
|
||||
// Store if any of the children have active state as we will hide the icon if they do
|
||||
const parentPath = path.join('.')
|
||||
if (!menuItemHasActiveStateChildren.value[parentPath]) {
|
||||
menuItemHasActiveStateChildren.value[parentPath] = items.some(
|
||||
(item) => item.comfyCommand?.active
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const registerCommands = (path: string[], commandIds: string[]) => {
|
||||
@@ -66,8 +57,7 @@ export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
label: command.menubarLabel,
|
||||
icon: command.icon,
|
||||
tooltip: command.tooltip,
|
||||
comfyCommand: command,
|
||||
parentPath: path.join('.')
|
||||
comfyCommand: command
|
||||
}) as MenuItem
|
||||
)
|
||||
registerMenuGroup(path, items)
|
||||
@@ -102,7 +92,6 @@ export const useMenuItemStore = defineStore('menuItem', () => {
|
||||
registerMenuGroup,
|
||||
registerCommands,
|
||||
loadExtensionMenuCommands,
|
||||
registerCoreMenuCommands,
|
||||
menuItemHasActiveStateChildren
|
||||
registerCoreMenuCommands
|
||||
}
|
||||
})
|
||||
|
||||
@@ -226,14 +226,6 @@ export const useReleaseStore = defineStore('release', () => {
|
||||
return
|
||||
}
|
||||
|
||||
// Skip fetching if API nodes are disabled via argv
|
||||
if (
|
||||
systemStatsStore.systemStats?.system?.argv?.includes(
|
||||
'--disable-api-nodes'
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useQueueSidebarTab } from '@/composables/sidebarTabs/useQueueSidebarTab
|
||||
import { useWorkflowsSidebarTab } from '@/composables/sidebarTabs/useWorkflowsSidebarTab'
|
||||
import { t, te } from '@/i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
@@ -39,34 +38,16 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
: String(tab.tooltip)
|
||||
: undefined
|
||||
|
||||
const menubarLabelFunction = () => {
|
||||
const menubarLabelKeys: Record<string, string> = {
|
||||
queue: 'menu.queue',
|
||||
'node-library': 'sideToolbar.nodeLibrary',
|
||||
'model-library': 'sideToolbar.modelLibrary',
|
||||
workflows: 'sideToolbar.workflows'
|
||||
}
|
||||
|
||||
const key = menubarLabelKeys[tab.id]
|
||||
if (key && te(key)) {
|
||||
return t(key)
|
||||
}
|
||||
|
||||
return tab.title
|
||||
}
|
||||
|
||||
useCommandStore().registerCommand({
|
||||
id: `Workspace.ToggleSidebarTab.${tab.id}`,
|
||||
icon: typeof tab.icon === 'string' ? tab.icon : undefined,
|
||||
icon: tab.icon,
|
||||
label: labelFunction,
|
||||
menubarLabel: menubarLabelFunction,
|
||||
tooltip: tooltipFunction,
|
||||
versionAdded: '1.3.9',
|
||||
category: 'view-controls' as const,
|
||||
function: () => {
|
||||
toggleSidebarTab(tab.id)
|
||||
},
|
||||
active: () => activeSidebarTab.value?.id === tab.id,
|
||||
source: 'System'
|
||||
})
|
||||
}
|
||||
@@ -92,25 +73,6 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
registerSidebarTab(useNodeLibrarySidebarTab())
|
||||
registerSidebarTab(useModelLibrarySidebarTab())
|
||||
registerSidebarTab(useWorkflowsSidebarTab())
|
||||
|
||||
const menuStore = useMenuItemStore()
|
||||
|
||||
menuStore.registerCommands(
|
||||
['View'],
|
||||
[
|
||||
'Workspace.ToggleBottomPanel',
|
||||
'Comfy.BrowseTemplates',
|
||||
'Workspace.ToggleFocusMode',
|
||||
'Comfy.ToggleCanvasInfo',
|
||||
'Comfy.Canvas.ToggleMinimap',
|
||||
'Comfy.Canvas.ToggleLinkVisibility'
|
||||
]
|
||||
)
|
||||
|
||||
menuStore.registerCommands(
|
||||
['View'],
|
||||
['Comfy.Canvas.ZoomIn', 'Comfy.Canvas.ZoomOut', 'Comfy.Canvas.FitView']
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -70,13 +70,6 @@ export interface ComfyExtension {
|
||||
* Badges to add to the about page
|
||||
*/
|
||||
aboutPageBadges?: AboutPageBadge[]
|
||||
/**
|
||||
* Allows the extension to run code before the app is initialized. This is the earliest lifecycle hook.
|
||||
* Called before the canvas is created and before any other extension hooks.
|
||||
* Useful for registering services, header providers, or other cross-cutting concerns.
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
preInit?(app: ComfyApp): Promise<void> | void
|
||||
/**
|
||||
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
|
||||
* @param app The ComfyUI app instance
|
||||
|
||||
@@ -6,10 +6,9 @@ import type { ComfyCommand } from '@/stores/commandStore'
|
||||
export interface BaseSidebarTabExtension {
|
||||
id: string
|
||||
title: string
|
||||
icon?: string | Component
|
||||
icon?: string
|
||||
iconBadge?: string | (() => string | null)
|
||||
tooltip?: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface BaseBottomPanelExtension {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
|
||||
/**
|
||||
* Header value can be a string, number, boolean, or a function that returns one of these
|
||||
*/
|
||||
export type HeaderValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| (() => string | number | boolean | Promise<string | number | boolean>)
|
||||
|
||||
/**
|
||||
* Header provider interface for extensions to implement
|
||||
*/
|
||||
export interface IHeaderProvider {
|
||||
/**
|
||||
* Provides headers for HTTP requests
|
||||
* @param context - Request context containing URL and method
|
||||
* @returns Headers to be added to the request
|
||||
*/
|
||||
provideHeaders(context: HeaderProviderContext): HeaderMap | Promise<HeaderMap>
|
||||
}
|
||||
|
||||
/**
|
||||
* Context passed to header providers
|
||||
*/
|
||||
export interface HeaderProviderContext {
|
||||
/** The URL being requested */
|
||||
url: string
|
||||
/** HTTP method */
|
||||
method: string
|
||||
/** Optional request body */
|
||||
body?: any
|
||||
/** Original request config if available */
|
||||
config?: AxiosRequestConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Map of header names to values
|
||||
*/
|
||||
export type HeaderMap = Record<string, HeaderValue>
|
||||
|
||||
/**
|
||||
* Registration handle returned when registering a header provider
|
||||
*/
|
||||
export interface IHeaderProviderRegistration {
|
||||
/** Unique ID for this registration */
|
||||
id: string
|
||||
/** Disposes of this registration */
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for registering a header provider
|
||||
*/
|
||||
export interface HeaderProviderOptions {
|
||||
/** Priority for this provider (higher = runs later, can override earlier providers) */
|
||||
priority?: number
|
||||
/** Optional filter to limit which requests this provider applies to */
|
||||
filter?: (context: HeaderProviderContext) => boolean
|
||||
}
|
||||
@@ -16,17 +16,12 @@ export const whileMouseDown = (
|
||||
callback(iteration++)
|
||||
}, interval)
|
||||
|
||||
const dispose = () => {
|
||||
const dispose = useEventListener(element, 'mouseup', () => {
|
||||
clearInterval(intervalId)
|
||||
disposeGlobal()
|
||||
disposeLocal()
|
||||
}
|
||||
|
||||
// Listen for mouseup globally to catch cases where user drags out of element
|
||||
const disposeGlobal = useEventListener(document, 'mouseup', dispose)
|
||||
const disposeLocal = useEventListener(element, 'mouseup', dispose)
|
||||
dispose()
|
||||
})
|
||||
|
||||
return {
|
||||
dispose: dispose
|
||||
dispose
|
||||
}
|
||||
}
|
||||
|
||||
12
src/vite-env.d.ts
vendored
@@ -1,17 +1,5 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module 'virtual:icons/*' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '~icons/*' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent
|
||||
export default component
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__COMFYUI_FRONTEND_VERSION__: string
|
||||
|
||||
@@ -114,16 +114,6 @@ describe('useNodePricing', () => {
|
||||
expect(price).toBe('$1.40/Run')
|
||||
})
|
||||
|
||||
it('should return high price for kling-v2-1-master model', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('KlingImage2VideoNode', [
|
||||
{ name: 'model_name', value: 'v2-1-master' }
|
||||
])
|
||||
|
||||
const price = getNodeDisplayPrice(node)
|
||||
expect(price).toBe('$1.40/Run')
|
||||
})
|
||||
|
||||
it('should return standard price for kling-v1-6 model', () => {
|
||||
const { getNodeDisplayPrice } = useNodePricing()
|
||||
const node = createMockNode('KlingImage2VideoNode', [
|
||||
|
||||
@@ -2,7 +2,6 @@ import { flushPromises } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useTemplateWorkflows } from '@/composables/useTemplateWorkflows'
|
||||
import { fetchWithHeaders } from '@/services/networkClientAdapter'
|
||||
import { useWorkflowTemplatesStore } from '@/stores/workflowTemplatesStore'
|
||||
|
||||
// Mock the store
|
||||
@@ -42,11 +41,6 @@ vi.mock('@/stores/dialogStore', () => ({
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn()
|
||||
|
||||
// Mock fetchWithHeaders
|
||||
vi.mock('@/services/networkClientAdapter', () => ({
|
||||
fetchWithHeaders: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useTemplateWorkflows', () => {
|
||||
let mockWorkflowTemplatesStore: any
|
||||
|
||||
@@ -106,11 +100,6 @@ describe('useTemplateWorkflows', () => {
|
||||
vi.mocked(fetch).mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({ workflow: 'data' })
|
||||
} as unknown as Response)
|
||||
|
||||
// Also mock fetchWithHeaders
|
||||
vi.mocked(fetchWithHeaders).mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({ workflow: 'data' })
|
||||
} as unknown as Response)
|
||||
})
|
||||
|
||||
it('should load templates from store', async () => {
|
||||
@@ -269,9 +258,7 @@ describe('useTemplateWorkflows', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith(
|
||||
'mock-file-url/templates/template1.json'
|
||||
)
|
||||
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
|
||||
expect(loadingTemplateId.value).toBe(null) // Should reset after loading
|
||||
})
|
||||
|
||||
@@ -286,9 +273,7 @@ describe('useTemplateWorkflows', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(vi.mocked(fetchWithHeaders)).toHaveBeenCalledWith(
|
||||
'mock-file-url/templates/template1.json'
|
||||
)
|
||||
expect(fetch).toHaveBeenCalledWith('mock-file-url/templates/template1.json')
|
||||
})
|
||||
|
||||
it('should handle errors when loading templates', async () => {
|
||||
@@ -297,10 +282,8 @@ describe('useTemplateWorkflows', () => {
|
||||
// Set the store as loaded
|
||||
mockWorkflowTemplatesStore.isLoaded = true
|
||||
|
||||
// Mock fetchWithHeaders to throw an error
|
||||
vi.mocked(fetchWithHeaders).mockRejectedValueOnce(
|
||||
new Error('Failed to fetch')
|
||||
)
|
||||
// Mock fetch to throw an error
|
||||
vi.mocked(fetch).mockRejectedValueOnce(new Error('Failed to fetch'))
|
||||
|
||||
// Spy on console.error
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
import axios from 'axios'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useRemoteWidget } from '@/composables/widgets/useRemoteWidget'
|
||||
import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
|
||||
// Hoist the mock to avoid hoisting issues
|
||||
const mockAxiosInstance = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
interceptors: {
|
||||
request: {
|
||||
use: vi.fn()
|
||||
},
|
||||
response: {
|
||||
use: vi.fn()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('axios', () => {
|
||||
return {
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
create: vi.fn(() => mockAxiosInstance)
|
||||
get: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Mock networkClientAdapter to return the same axios instance
|
||||
vi.mock('@/services/networkClientAdapter', () => ({
|
||||
createAxiosWithHeaders: vi.fn(() => mockAxiosInstance)
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
i18n: {
|
||||
global: {
|
||||
@@ -81,12 +63,12 @@ const createMockOptions = (inputOverrides = {}) => ({
|
||||
})
|
||||
|
||||
function mockAxiosResponse(data: unknown, status = 200) {
|
||||
vi.mocked(mockAxiosInstance.get).mockResolvedValueOnce({ data, status })
|
||||
vi.mocked(axios.get).mockResolvedValueOnce({ data, status })
|
||||
}
|
||||
|
||||
function mockAxiosError(error: Error | string) {
|
||||
const err = error instanceof Error ? error : new Error(error)
|
||||
vi.mocked(mockAxiosInstance.get).mockRejectedValueOnce(err)
|
||||
vi.mocked(axios.get).mockRejectedValueOnce(err)
|
||||
}
|
||||
|
||||
function createHookWithData(data: unknown, inputOverrides = {}) {
|
||||
@@ -114,7 +96,7 @@ describe('useRemoteWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset mocks
|
||||
vi.mocked(mockAxiosInstance.get).mockReset()
|
||||
vi.mocked(axios.get).mockReset()
|
||||
// Reset cache between tests
|
||||
vi.spyOn(Map.prototype, 'get').mockClear()
|
||||
vi.spyOn(Map.prototype, 'set').mockClear()
|
||||
@@ -155,7 +137,7 @@ describe('useRemoteWidget', () => {
|
||||
const mockData = ['optionA', 'optionB']
|
||||
const { hook, result } = await setupHookWithResponse(mockData)
|
||||
expect(result).toEqual(mockData)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledWith(
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledWith(
|
||||
hook.cacheKey.split(';')[0], // Get the route part from cache key
|
||||
expect.any(Object)
|
||||
)
|
||||
@@ -234,7 +216,7 @@ describe('useRemoteWidget', () => {
|
||||
await getResolvedValue(hook)
|
||||
await getResolvedValue(hook)
|
||||
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('permanent widgets should re-fetch if refreshValue is called', async () => {
|
||||
@@ -255,12 +237,12 @@ describe('useRemoteWidget', () => {
|
||||
|
||||
const hook = useRemoteWidget(createMockOptions())
|
||||
await getResolvedValue(hook)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.setSystemTime(Date.now() + FIRST_BACKOFF)
|
||||
const secondData = await getResolvedValue(hook)
|
||||
expect(secondData).toBe('Loading...')
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should treat empty refresh field as permanent', async () => {
|
||||
@@ -269,7 +251,7 @@ describe('useRemoteWidget', () => {
|
||||
await getResolvedValue(hook)
|
||||
await getResolvedValue(hook)
|
||||
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -285,7 +267,7 @@ describe('useRemoteWidget', () => {
|
||||
const newData = await getResolvedValue(hook)
|
||||
|
||||
expect(newData).toEqual(mockData2)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should not refresh when data is not stale', async () => {
|
||||
@@ -296,7 +278,7 @@ describe('useRemoteWidget', () => {
|
||||
vi.setSystemTime(Date.now() + 128)
|
||||
await getResolvedValue(hook)
|
||||
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should use backoff instead of refresh after error', async () => {
|
||||
@@ -308,13 +290,13 @@ describe('useRemoteWidget', () => {
|
||||
mockAxiosError('Network error')
|
||||
vi.setSystemTime(Date.now() + refresh)
|
||||
await getResolvedValue(hook)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
|
||||
|
||||
mockAxiosResponse(['second success'])
|
||||
vi.setSystemTime(Date.now() + FIRST_BACKOFF)
|
||||
const thirdData = await getResolvedValue(hook)
|
||||
expect(thirdData).toEqual(['second success'])
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(3)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should use last valid value after error', async () => {
|
||||
@@ -328,7 +310,7 @@ describe('useRemoteWidget', () => {
|
||||
const secondData = await getResolvedValue(hook)
|
||||
|
||||
expect(secondData).toEqual(['a valid value'])
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -350,15 +332,15 @@ describe('useRemoteWidget', () => {
|
||||
expect(entry1?.error).toBeTruthy()
|
||||
|
||||
await getResolvedValue(hook)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.setSystemTime(Date.now() + 500)
|
||||
await getResolvedValue(hook)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1) // Still backing off
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1) // Still backing off
|
||||
|
||||
vi.setSystemTime(Date.now() + 3000)
|
||||
await getResolvedValue(hook)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(2)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2)
|
||||
expect(entry1?.data).toBeDefined()
|
||||
})
|
||||
|
||||
@@ -436,9 +418,7 @@ describe('useRemoteWidget', () => {
|
||||
|
||||
it('should prevent duplicate in-flight requests', async () => {
|
||||
const promise = Promise.resolve({ data: ['non-duplicate'] })
|
||||
vi.mocked(mockAxiosInstance.get).mockImplementationOnce(
|
||||
() => promise as any
|
||||
)
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => promise as any)
|
||||
|
||||
const hook = useRemoteWidget(createMockOptions())
|
||||
const [result1, result2] = await Promise.all([
|
||||
@@ -447,7 +427,7 @@ describe('useRemoteWidget', () => {
|
||||
])
|
||||
|
||||
expect(result1).toBe(result2)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -466,7 +446,7 @@ describe('useRemoteWidget', () => {
|
||||
|
||||
expect(data1).toEqual(['shared data'])
|
||||
expect(data2).toEqual(['shared data'])
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
expect(hook1.getCachedValue()).toBe(hook2.getCachedValue())
|
||||
})
|
||||
|
||||
@@ -487,7 +467,7 @@ describe('useRemoteWidget', () => {
|
||||
expect(data2).toBe(data1)
|
||||
expect(data3).toBe(data1)
|
||||
expect(data4).toBe(data1)
|
||||
expect(vi.mocked(mockAxiosInstance.get)).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(1)
|
||||
expect(hook1.getCachedValue()).toBe(hook2.getCachedValue())
|
||||
expect(hook2.getCachedValue()).toBe(hook3.getCachedValue())
|
||||
expect(hook3.getCachedValue()).toBe(hook4.getCachedValue())
|
||||
@@ -499,9 +479,7 @@ describe('useRemoteWidget', () => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
|
||||
vi.mocked(mockAxiosInstance.get).mockImplementationOnce(
|
||||
() => delayedPromise as any
|
||||
)
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any)
|
||||
|
||||
const hook = useRemoteWidget(createMockOptions())
|
||||
hook.getValue()
|
||||
@@ -522,9 +500,7 @@ describe('useRemoteWidget', () => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
|
||||
vi.mocked(mockAxiosInstance.get).mockImplementationOnce(
|
||||
() => delayedPromise as any
|
||||
)
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any)
|
||||
|
||||
let hook = useRemoteWidget(createMockOptions())
|
||||
const fetchPromise = hook.getValue()
|
||||
|
||||