Compare commits
34 Commits
austin/fas
...
fix/clear-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d00db2143 | ||
|
|
6e9c6c6f2e | ||
|
|
9669100c14 | ||
|
|
ac825bdb87 | ||
|
|
327e37db04 | ||
|
|
47714c2740 | ||
|
|
f7ac0aa39e | ||
|
|
e83f33aeb2 | ||
|
|
b1dfbfaa09 | ||
|
|
e6ef99e92c | ||
|
|
f5a784e561 | ||
|
|
e8b45204f2 | ||
|
|
5df793b721 | ||
|
|
79d3b2c291 | ||
|
|
916c1248e3 | ||
|
|
b5f91977c8 | ||
|
|
a8b4928acc | ||
|
|
7f25280da4 | ||
|
|
4bf9b94cd4 | ||
|
|
a2246cce7a | ||
|
|
d73e8e4aa3 | ||
|
|
7ef4ea6f25 | ||
|
|
d5f17f7d9f | ||
|
|
0d0576faab | ||
|
|
b0d7a7f0f4 | ||
|
|
12ee5de73b | ||
|
|
6db4750d96 | ||
|
|
30907f99f1 | ||
|
|
284bdce61b | ||
|
|
7fcef2ba89 | ||
|
|
54db655a23 | ||
|
|
82c3cd3cd2 | ||
|
|
c9d74777ba | ||
|
|
be8916b4ce |
@@ -122,7 +122,7 @@ echo " pnpm build - Build for production"
|
||||
echo " pnpm test:unit - Run unit tests"
|
||||
echo " pnpm typecheck - Run TypeScript checks"
|
||||
echo " pnpm lint - Run ESLint"
|
||||
echo " pnpm format - Format code with Prettier"
|
||||
echo " pnpm format - Format code with oxfmt"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Run 'pnpm dev' to start developing"
|
||||
|
||||
6
.github/workflows/ci-lint-format.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Run Stylelint with auto-fix
|
||||
run: pnpm stylelint:fix
|
||||
|
||||
- name: Run Prettier with auto-format
|
||||
- name: Run oxfmt with auto-format
|
||||
run: pnpm format
|
||||
|
||||
- name: Check for changes
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
git commit -m "[automated] Apply ESLint and Prettier fixes"
|
||||
git commit -m "[automated] Apply ESLint and Oxfmt fixes"
|
||||
git push
|
||||
|
||||
- name: Final validation
|
||||
@@ -80,7 +80,7 @@ jobs:
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Prettier formatting'
|
||||
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Oxfmt formatting'
|
||||
})
|
||||
|
||||
- name: Comment on PR about manual fix needed
|
||||
|
||||
18
.i18nrc.cjs
@@ -1,7 +1,7 @@
|
||||
// This file is intentionally kept in CommonJS format (.cjs)
|
||||
// to resolve compatibility issues with dependencies that require CommonJS.
|
||||
// Do not convert this file to ESModule format unless all dependencies support it.
|
||||
const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
const { defineConfig } = require('@lobehub/i18n-cli')
|
||||
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4.1',
|
||||
@@ -10,7 +10,19 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR', 'fa'],
|
||||
outputLocales: [
|
||||
'zh',
|
||||
'zh-TW',
|
||||
'ru',
|
||||
'ja',
|
||||
'ko',
|
||||
'fr',
|
||||
'es',
|
||||
'ar',
|
||||
'tr',
|
||||
'pt-BR',
|
||||
'fa'
|
||||
],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
@@ -26,4 +38,4 @@ module.exports = defineConfig({
|
||||
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
|
||||
- Maintain consistency with terminology used in Persian software and design applications.
|
||||
`
|
||||
});
|
||||
})
|
||||
|
||||
20
.oxfmtrc.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"ignorePatterns": [
|
||||
"packages/registry-types/src/comfyRegistryTypes.ts",
|
||||
"src/types/generatedManagerTypes.ts",
|
||||
"**/*.md",
|
||||
"**/*.json",
|
||||
"**/*.css",
|
||||
"**/*.yaml",
|
||||
"**/*.yml",
|
||||
"**/*.html",
|
||||
"**/*.svg",
|
||||
"**/*.xml"
|
||||
]
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
packages/registry-types/src/comfyRegistryTypes.ts
|
||||
src/types/generatedManagerTypes.ts
|
||||
11
.prettierrc
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 80,
|
||||
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true,
|
||||
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
@@ -96,15 +96,15 @@ const config: StorybookConfig = {
|
||||
}
|
||||
]
|
||||
},
|
||||
esbuild: {
|
||||
// Prevent minification of identifiers to preserve _sfc_main
|
||||
minifyIdentifiers: false,
|
||||
keepNames: true
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
|
||||
rolldownOptions: {
|
||||
experimental: {
|
||||
strictExecutionOrder: true
|
||||
},
|
||||
treeshake: false,
|
||||
output: {
|
||||
keepNames: true
|
||||
},
|
||||
onwarn: (warning, warn) => {
|
||||
// Suppress specific warnings
|
||||
if (
|
||||
|
||||
15
.vscode/extensions.json
vendored
@@ -1,25 +1,22 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"antfu.vite",
|
||||
"austenc.tailwind-docs",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"donjayamanne.githistory",
|
||||
"eamodio.gitlens",
|
||||
"esbenp.prettier-vscode",
|
||||
"figma.figma-vscode-extension",
|
||||
"github.vscode-github-actions",
|
||||
"github.vscode-pull-request-github",
|
||||
"hbenl.vscode-test-explorer",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"lokalise.i18n-ally",
|
||||
"ms-playwright.playwright",
|
||||
"oxc.oxc-vscode",
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"vitest.explorer",
|
||||
"vue.volar",
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"deque-systems.vscode-axe-linter",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"donjayamanne.githistory",
|
||||
"wix.vscode-import-cost",
|
||||
"prograhammer.tslint-vue",
|
||||
"antfu.vite"
|
||||
"wix.vscode-import-cost"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ See @docs/guidance/*.md for file-type-specific conventions (auto-loaded by glob)
|
||||
- Build output: `dist/`
|
||||
- Configs
|
||||
- `vite.config.mts`
|
||||
- `vitest.config.ts`
|
||||
- `playwright.config.ts`
|
||||
- `eslint.config.ts`
|
||||
- `.prettierrc`
|
||||
- `.oxfmtrc.json`
|
||||
- `.oxlintrc.json`
|
||||
- etc.
|
||||
|
||||
## Monorepo Architecture
|
||||
@@ -46,7 +46,7 @@ The project uses **Nx** for build orchestration and task management
|
||||
- `pnpm test:unit`: Run Vitest unit tests
|
||||
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
|
||||
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
|
||||
- `pnpm format` / `pnpm format:check`: Prettier
|
||||
- `pnpm format` / `pnpm format:check`: oxfmt
|
||||
- `pnpm typecheck`: Vue TSC type checking
|
||||
- `pnpm storybook`: Start Storybook development server
|
||||
|
||||
@@ -72,7 +72,7 @@ The project uses **Nx** for build orchestration and task management
|
||||
- Composition API only
|
||||
- Tailwind 4 styling
|
||||
- Avoid `<style>` blocks
|
||||
- Style: (see `.prettierrc`)
|
||||
- Style: (see `.oxfmtrc.json`)
|
||||
- Indent 2 spaces
|
||||
- single quotes
|
||||
- no trailing semicolons
|
||||
|
||||
@@ -64,7 +64,7 @@ export default defineConfig(() => {
|
||||
})
|
||||
],
|
||||
build: {
|
||||
minify: SHOULD_MINIFY ? ('esbuild' as const) : false,
|
||||
minify: SHOULD_MINIFY,
|
||||
target: 'es2022',
|
||||
sourcemap: true
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ComfyNodeSearchBox {
|
||||
'.comfy-vue-node-search-container input[type="text"]'
|
||||
)
|
||||
this.dropdown = page.locator(
|
||||
'.comfy-vue-node-search-container .comfy-autocomplete-list'
|
||||
'.comfy-vue-node-search-container .p-autocomplete-list'
|
||||
)
|
||||
this.filterSelectionPanel = new ComfyNodeSearchFilterSelectionPanel(page)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export class ComfyNodeSearchBox {
|
||||
await this.input.fill(nodeName)
|
||||
await this.dropdown.waitFor({ state: 'visible' })
|
||||
await this.dropdown
|
||||
.locator('.option-container')
|
||||
.locator('li')
|
||||
.nth(options?.suggestionIndex || 0)
|
||||
.click()
|
||||
}
|
||||
|
||||
@@ -79,48 +79,15 @@ export class SubgraphSlotReference {
|
||||
|
||||
const node =
|
||||
type === 'input' ? currentGraph.inputNode : currentGraph.outputNode
|
||||
const slots =
|
||||
type === 'input' ? currentGraph.inputs : currentGraph.outputs
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`No ${type} node found in subgraph`)
|
||||
}
|
||||
|
||||
// Calculate position for next available slot
|
||||
// const nextSlotIndex = slots?.length || 0
|
||||
// const slotHeight = 20
|
||||
// const slotY = node.pos[1] + 30 + nextSlotIndex * slotHeight
|
||||
|
||||
// Find last slot position
|
||||
const lastSlot = slots.at(-1)
|
||||
let slotX: number
|
||||
let slotY: number
|
||||
|
||||
if (lastSlot) {
|
||||
// If there are existing slots, position the new one below the last one
|
||||
const gapHeight = 20
|
||||
slotX = lastSlot.pos[0]
|
||||
slotY = lastSlot.pos[1] + gapHeight
|
||||
} else {
|
||||
// No existing slots - use slotAnchorX if available, otherwise calculate from node position
|
||||
if (currentGraph.slotAnchorX !== undefined) {
|
||||
// The actual slot X position seems to be slotAnchorX - 10
|
||||
slotX = currentGraph.slotAnchorX - 10
|
||||
} else {
|
||||
// Fallback: calculate from node edge
|
||||
slotX =
|
||||
type === 'input'
|
||||
? node.pos[0] + node.size[0] - 10 // Right edge for input node
|
||||
: node.pos[0] + 10 // Left edge for output node
|
||||
}
|
||||
// For Y position when no slots exist, use middle of node
|
||||
slotY = node.pos[1] + node.size[1] / 2
|
||||
}
|
||||
|
||||
// Convert from offset to canvas coordinates
|
||||
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
|
||||
slotX,
|
||||
slotY
|
||||
node.emptySlot.pos[0],
|
||||
node.emptySlot.pos[1]
|
||||
])
|
||||
return canvasPos
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
@@ -4,9 +4,7 @@ import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
// WORKAROUND: eslint-plugin-prettier causes segfault on Node.js 24 + Windows
|
||||
// See: https://github.com/nodejs/node/issues/58690
|
||||
// Prettier is still run separately in lint-staged, so this is safe to disable
|
||||
// eslint-config-prettier disables ESLint rules that conflict with formatters (oxfmt)
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
import { configs as storybookConfigs } from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
@@ -111,7 +109,7 @@ export default defineConfig([
|
||||
tseslintConfigs.recommended,
|
||||
// Difference in typecheck on CI vs Local
|
||||
pluginVue.configs['flat/recommended'],
|
||||
// Use eslint-config-prettier instead of eslint-plugin-prettier to avoid Node 24 segfault
|
||||
// Disables ESLint rules that conflict with formatters
|
||||
eslintConfigPrettier,
|
||||
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types
|
||||
storybookConfigs['flat/recommended'],
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
'tests-ui/**': () => 'echo "Files in tests-ui/ are deprecated. Colocate tests with source files." && exit 1',
|
||||
|
||||
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
]
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
// Convert absolute paths to relative paths for better ESLint resolution
|
||||
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
|
||||
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
return [
|
||||
`pnpm exec prettier --cache --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
'tests-ui/**': () =>
|
||||
'echo "Files in tests-ui/ are deprecated. Colocate tests with source files." && exit 1',
|
||||
|
||||
'./**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [
|
||||
@@ -14,7 +17,7 @@ function formatAndEslint(fileNames: string[]) {
|
||||
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
|
||||
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
return [
|
||||
`pnpm exec prettier --cache --write ${joinedPaths}`,
|
||||
`pnpm exec oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
|
||||
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.38.4",
|
||||
"version": "1.38.8",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -22,10 +22,8 @@
|
||||
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
|
||||
"dev": "nx serve",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
|
||||
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different",
|
||||
"format:check": "oxfmt --check",
|
||||
"format": "oxfmt --write",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
@@ -63,14 +61,12 @@
|
||||
"@nx/vite": "catalog:",
|
||||
"@pinia/testing": "catalog:",
|
||||
"@playwright/test": "catalog:",
|
||||
"@prettier/plugin-oxc": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@storybook/addon-docs": "catalog:",
|
||||
"@storybook/addon-mcp": "catalog:",
|
||||
"@storybook/vue3": "catalog:",
|
||||
"@storybook/vue3-vite": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@trivago/prettier-plugin-sort-imports": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@types/jsdom": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
@@ -101,11 +97,11 @@
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"oxfmt": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"oxlint-tsgolint": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"pretty-bytes": "catalog:",
|
||||
"rollup-plugin-visualizer": "catalog:",
|
||||
"storybook": "catalog:",
|
||||
|
||||
323
pnpm-lock.yaml
generated
@@ -48,9 +48,6 @@ catalogs:
|
||||
'@playwright/test':
|
||||
specifier: ^1.57.0
|
||||
version: 1.57.0
|
||||
'@prettier/plugin-oxc':
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
'@primeuix/forms':
|
||||
specifier: 0.0.2
|
||||
version: 0.0.2
|
||||
@@ -96,9 +93,6 @@ catalogs:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.12
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.2
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
@@ -207,6 +201,9 @@ catalogs:
|
||||
nx:
|
||||
specifier: 22.2.6
|
||||
version: 22.2.6
|
||||
oxfmt:
|
||||
specifier: ^0.26.0
|
||||
version: 0.26.0
|
||||
oxlint:
|
||||
specifier: ^1.33.0
|
||||
version: 1.33.0
|
||||
@@ -222,9 +219,6 @@ catalogs:
|
||||
postcss-html:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: ^3.7.4
|
||||
version: 3.7.4
|
||||
pretty-bytes:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
@@ -540,9 +534,6 @@ importers:
|
||||
'@playwright/test':
|
||||
specifier: 'catalog:'
|
||||
version: 1.57.0
|
||||
'@prettier/plugin-oxc':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
'@sentry/vite-plugin':
|
||||
specifier: 'catalog:'
|
||||
version: 4.6.0
|
||||
@@ -561,9 +552,6 @@ importers:
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.12(vite@8.0.0-beta.8(@types/node@24.10.4)(esbuild@0.27.1)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)
|
||||
'@types/fs-extra':
|
||||
specifier: 'catalog:'
|
||||
version: 11.0.4
|
||||
@@ -654,6 +642,9 @@ importers:
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 22.2.6
|
||||
oxfmt:
|
||||
specifier: 'catalog:'
|
||||
version: 0.26.0
|
||||
oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.33.0(oxlint-tsgolint@0.9.1)
|
||||
@@ -666,9 +657,6 @@ importers:
|
||||
postcss-html:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 3.7.4
|
||||
pretty-bytes:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
@@ -2517,95 +2505,6 @@ packages:
|
||||
'@one-ini/wasm@0.1.1':
|
||||
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
|
||||
|
||||
'@oxc-parser/binding-android-arm64@0.99.0':
|
||||
resolution: {integrity: sha512-V4jhmKXgQQdRnm73F+r3ZY4pUEsijQeSraFeaCGng7abSNJGs76X6l82wHnmjLGFAeY00LWtjcELs7ZmbJ9+lA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@oxc-parser/binding-darwin-arm64@0.99.0':
|
||||
resolution: {integrity: sha512-Rp41nf9zD5FyLZciS9l1GfK8PhYqrD5kEGxyTOA2esTLeAy37rZxetG2E3xteEolAkeb2WDkVrlxPtibeAncMg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-parser/binding-darwin-x64@0.99.0':
|
||||
resolution: {integrity: sha512-WVonp40fPPxo5Gs0POTI57iEFv485TvNKOHMwZRhigwZRhZY2accEAkYIhei9eswF4HN5B44Wybkz7Gd1Qr/5Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxc-parser/binding-freebsd-x64@0.99.0':
|
||||
resolution: {integrity: sha512-H30bjOOttPmG54gAqu6+HzbLEzuNOYO2jZYrIq4At+NtLJwvNhXz28Hf5iEAFZIH/4hMpLkM4VN7uc+5UlNW3Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@oxc-parser/binding-linux-arm-gnueabihf@0.99.0':
|
||||
resolution: {integrity: sha512-0Z/Th0SYqzSRDPs6tk5lQdW0i73UCupnim3dgq2oW0//UdLonV/5wIZCArfKGC7w9y4h8TxgXpgtIyD1kKzzlQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-arm-musleabihf@0.99.0':
|
||||
resolution: {integrity: sha512-xo0wqNd5bpbzQVNpAIFbHk1xa+SaS/FGBABCd942SRTnrpxl6GeDj/s1BFaGcTl8MlwlKVMwOcyKrw/2Kdfquw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-gnu@0.99.0':
|
||||
resolution: {integrity: sha512-u26I6LKoLTPTd4Fcpr0aoAtjnGf5/ulMllo+QUiBhupgbVCAlaj4RyXH/mvcjcsl2bVBv9E/gYJZz2JjxQWXBA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.99.0':
|
||||
resolution: {integrity: sha512-qhftDo2D37SqCEl3ZTa367NqWSZNb1Ddp34CTmShLKFrnKdNiUn55RdokLnHtf1AL5ssaQlYDwBECX7XiBWOhw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.99.0':
|
||||
resolution: {integrity: sha512-zxn/xkf519f12FKkpL5XwJipsylfSSnm36h6c1zBDTz4fbIDMGyIhHfWfwM7uUmHo9Aqw1pLxFpY39Etv398+Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.99.0':
|
||||
resolution: {integrity: sha512-Y1eSDKDS5E4IVC7Oxw+NbYAKRmJPMJTIjW+9xOWwteDHkFqpocKe0USxog+Q1uhzalD9M0p9eXWEWdGQCMDBMQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.99.0':
|
||||
resolution: {integrity: sha512-YVJMfk5cFWB8i2/nIrbk6n15bFkMHqWnMIWkVx7r2KwpTxHyFMfu2IpeVKo1ITDSmt5nBrGdLHD36QRlu2nDLg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.99.0':
|
||||
resolution: {integrity: sha512-2+SDPrie5f90A1b9EirtVggOgsqtsYU5raZwkDYKyS1uvJzjqHCDhG/f4TwQxHmIc5YkczdQfwvN91lwmjsKYQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxc-parser/binding-wasm32-wasi@0.99.0':
|
||||
resolution: {integrity: sha512-DKA4j0QerUWSMADziLM5sAyM7V53Fj95CV9SjP77bPfEfT7MnvFKnneaRMqPK1cpzjAGiQF52OBUIKyk0dwOQA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@oxc-parser/binding-win32-arm64-msvc@0.99.0':
|
||||
resolution: {integrity: sha512-EaB3AvsxqdNUhh9FOoAxRZ2L4PCRwDlDb//QXItwyOJrX7XS+uGK9B1KEUV4FZ/7rDhHsWieLt5e07wl2Ti5AQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-parser/binding-win32-x64-msvc@0.99.0':
|
||||
resolution: {integrity: sha512-sJN1Q8h7ggFOyDn0zsHaXbP/MklAVUvhrbq0LA46Qum686P3SZQHjbATqJn9yaVEvaSKXCshgl0vQ1gWkGgpcQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxc-project/runtime@0.108.0':
|
||||
resolution: {integrity: sha512-J1cESY4anMO4i9KtCPmCfQAzAR00Uw4SWsDPFP10CIwDMugkh34UrTKByuYKuPaHy0XAk8LlJiZJq2OLMfbuIQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -2613,9 +2512,6 @@ packages:
|
||||
'@oxc-project/types@0.108.0':
|
||||
resolution: {integrity: sha512-7lf13b2IA/kZO6xgnIZA88sq3vwrxWk+2vxf6cc+omwYCRTiA5e63Beqf3fz/v8jEviChWWmFYBwzfSeyrsj7Q==}
|
||||
|
||||
'@oxc-project/types@0.99.0':
|
||||
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.15.0':
|
||||
resolution: {integrity: sha512-Q+lWuFfq7whNelNJIP1dhXaVz4zO9Tu77GcQHyxDWh3MaCoO2Bisphgzmsh4ZoUe2zIchQh6OvQL99GlWHg9Tw==}
|
||||
cpu: [arm]
|
||||
@@ -2716,6 +2612,46 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/darwin-arm64@0.26.0':
|
||||
resolution: {integrity: sha512-AAGc+8CffkiWeVgtWf4dPfQwHEE5c/j/8NWH7VGVxxJRCZFdmWcqCXprvL2H6qZFewvDLrFbuSPRCqYCpYGaTQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/darwin-x64@0.26.0':
|
||||
resolution: {integrity: sha512-xFx5ijCTjw577wJvFlZEMmKDnp3HSCcbYdCsLRmC5i3TZZiDe9DEYh3P46uqhzj8BkEw1Vm1ZCWdl48aEYAzvQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@oxfmt/linux-arm64-gnu@0.26.0':
|
||||
resolution: {integrity: sha512-GubkQeQT5d3B/Jx/IiR7NMkSmXrCZcVI0BPh1i7mpFi8HgD1hQ/LbhiBKAMsMqs5bbugdQOgBEl8bOhe8JhW1g==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/linux-arm64-musl@0.26.0':
|
||||
resolution: {integrity: sha512-OEypUwK69bFPj+aa3/LYCnlIUPgoOLu//WNcriwpnWNmt47808Ht7RJSg+MNK8a7pSZHpXJ5/E6CRK/OTwFdaQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/linux-x64-gnu@0.26.0':
|
||||
resolution: {integrity: sha512-xO6iEW2bC6ZHyOTPmPWrg/nM6xgzyRPaS84rATy6F8d79wz69LdRdJ3l/PXlkqhi7XoxhvX4ExysA0Nf10ZZEQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/linux-x64-musl@0.26.0':
|
||||
resolution: {integrity: sha512-Z3KuZFC+MIuAyFCXBHY71kCsdRq1ulbsbzTe71v+hrEv7zVBn6yzql+/AZcgfIaKzWO9OXNuz5WWLWDmVALwow==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@oxfmt/win32-arm64@0.26.0':
|
||||
resolution: {integrity: sha512-3zRbqwVWK1mDhRhTknlQFpRFL9GhEB5GfU6U7wawnuEwpvi39q91kJ+SRJvJnhyPCARkjZBd1V8XnweN5IFd1g==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@oxfmt/win32-x64@0.26.0':
|
||||
resolution: {integrity: sha512-m8TfIljU22i9UEIkD+slGPifTFeaCwIUfxszN3E6ABWP1KQbtwSw9Ak0TdoikibvukF/dtbeyG3WW63jv9DnEg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.9.1':
|
||||
resolution: {integrity: sha512-vk+8kChWqN+F+QUOvp4/6jDTlDCzXPgYGkxdi6EOUSOmCP1ix0uYOlIi/ytH2imXmC8YfPgLR/1BhqbsuDKuew==}
|
||||
cpu: [arm64]
|
||||
@@ -2824,10 +2760,6 @@ packages:
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@prettier/plugin-oxc@0.1.3':
|
||||
resolution: {integrity: sha512-aABz3zIRilpWMekbt1FL1JVBQrQLR8L4Td2SRctECrWSsXGTNn/G1BqNSKCdbvQS1LWstAXfqcXzDki7GAAJyg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@primeuix/forms@0.0.2':
|
||||
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
|
||||
engines: {node: '>=12.11.0'}
|
||||
@@ -3571,22 +3503,6 @@ packages:
|
||||
'@tmcp/auth':
|
||||
optional: true
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2':
|
||||
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
|
||||
engines: {node: '>18.12'}
|
||||
peerDependencies:
|
||||
'@vue/compiler-sfc': 3.x
|
||||
prettier: 2.x - 3.x
|
||||
prettier-plugin-svelte: 3.x
|
||||
svelte: 4.x || 5.x
|
||||
peerDependenciesMeta:
|
||||
'@vue/compiler-sfc':
|
||||
optional: true
|
||||
prettier-plugin-svelte:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
|
||||
'@tweenjs/tween.js@23.1.3':
|
||||
resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
|
||||
|
||||
@@ -6039,9 +5955,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
javascript-natural-sort@0.7.1:
|
||||
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
|
||||
|
||||
jest-diff@30.2.0:
|
||||
resolution: {integrity: sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@@ -6889,13 +6802,14 @@ packages:
|
||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
oxc-parser@0.99.0:
|
||||
resolution: {integrity: sha512-MpS1lbd2vR0NZn1v0drpgu7RUFu3x9Rd0kxExObZc2+F+DIrV0BOMval/RO3BYGwssIOerII6iS8EbbpCCZQpQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
|
||||
oxc-resolver@11.15.0:
|
||||
resolution: {integrity: sha512-Hk2J8QMYwmIO9XTCUiOH00+Xk2/+aBxRUnhrSlANDyCnLYc32R1WSIq1sU2yEdlqd53FfMpPEpnBYIKQMzliJw==}
|
||||
|
||||
oxfmt@0.26.0:
|
||||
resolution: {integrity: sha512-UDD1wFNwfeorMm2ZY0xy1KRAAvJ5NjKBfbDmiMwGP7baEHTq65cYpC0aPP+BGHc8weXUbSZaK8MdGyvuRUvS4Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
oxlint-tsgolint@0.9.1:
|
||||
resolution: {integrity: sha512-w1lIvUDkkiAPFyo268SFGrdh1LQ3Lcs1XShES7I4X75TliQA0os5XJ5hNZ4lYsSevqcofgEtq4xq7rBumv69iQ==}
|
||||
hasBin: true
|
||||
@@ -7796,6 +7710,10 @@ packages:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tinypool@2.0.0:
|
||||
resolution: {integrity: sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg==}
|
||||
engines: {node: ^20.0.0 || >=22.0.0}
|
||||
|
||||
tinyrainbow@2.0.0:
|
||||
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -10677,59 +10595,10 @@ snapshots:
|
||||
|
||||
'@one-ini/wasm@0.1.1': {}
|
||||
|
||||
'@oxc-parser/binding-android-arm64@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-darwin-arm64@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-darwin-x64@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-freebsd-x64@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm-gnueabihf@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm-musleabihf@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-gnu@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-arm64-musl@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-riscv64-gnu@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-s390x-gnu@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-x64-gnu@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-linux-x64-musl@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-wasm32-wasi@0.99.0':
|
||||
dependencies:
|
||||
'@napi-rs/wasm-runtime': 1.1.1
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-win32-arm64-msvc@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-parser/binding-win32-x64-msvc@0.99.0':
|
||||
optional: true
|
||||
|
||||
'@oxc-project/runtime@0.108.0': {}
|
||||
|
||||
'@oxc-project/types@0.108.0': {}
|
||||
|
||||
'@oxc-project/types@0.99.0': {}
|
||||
|
||||
'@oxc-resolver/binding-android-arm-eabi@11.15.0':
|
||||
optional: true
|
||||
|
||||
@@ -10792,6 +10661,30 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-x64-msvc@11.15.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/darwin-arm64@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/darwin-x64@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-arm64-gnu@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-arm64-musl@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-x64-gnu@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/linux-x64-musl@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/win32-arm64@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxfmt/win32-x64@0.26.0':
|
||||
optional: true
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@0.9.1':
|
||||
optional: true
|
||||
|
||||
@@ -10866,10 +10759,6 @@ snapshots:
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@prettier/plugin-oxc@0.1.3':
|
||||
dependencies:
|
||||
oxc-parser: 0.99.0
|
||||
|
||||
'@primeuix/forms@0.0.2':
|
||||
dependencies:
|
||||
'@primeuix/utils': 0.3.2
|
||||
@@ -11585,20 +11474,6 @@ snapshots:
|
||||
esm-env: 1.2.2
|
||||
tmcp: 1.19.0(typescript@5.9.3)
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.25)(prettier@3.7.4)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.5
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/traverse': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.7.4
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.25
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@tweenjs/tween.js@23.1.3': {}
|
||||
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
@@ -14431,8 +14306,6 @@ snapshots:
|
||||
filelist: 1.0.4
|
||||
minimatch: 3.1.2
|
||||
|
||||
javascript-natural-sort@0.7.1: {}
|
||||
|
||||
jest-diff@30.2.0:
|
||||
dependencies:
|
||||
'@jest/diff-sequences': 30.0.1
|
||||
@@ -15531,26 +15404,6 @@ snapshots:
|
||||
safe-push-apply: 1.0.0
|
||||
optional: true
|
||||
|
||||
oxc-parser@0.99.0:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.99.0
|
||||
optionalDependencies:
|
||||
'@oxc-parser/binding-android-arm64': 0.99.0
|
||||
'@oxc-parser/binding-darwin-arm64': 0.99.0
|
||||
'@oxc-parser/binding-darwin-x64': 0.99.0
|
||||
'@oxc-parser/binding-freebsd-x64': 0.99.0
|
||||
'@oxc-parser/binding-linux-arm-gnueabihf': 0.99.0
|
||||
'@oxc-parser/binding-linux-arm-musleabihf': 0.99.0
|
||||
'@oxc-parser/binding-linux-arm64-gnu': 0.99.0
|
||||
'@oxc-parser/binding-linux-arm64-musl': 0.99.0
|
||||
'@oxc-parser/binding-linux-riscv64-gnu': 0.99.0
|
||||
'@oxc-parser/binding-linux-s390x-gnu': 0.99.0
|
||||
'@oxc-parser/binding-linux-x64-gnu': 0.99.0
|
||||
'@oxc-parser/binding-linux-x64-musl': 0.99.0
|
||||
'@oxc-parser/binding-wasm32-wasi': 0.99.0
|
||||
'@oxc-parser/binding-win32-arm64-msvc': 0.99.0
|
||||
'@oxc-parser/binding-win32-x64-msvc': 0.99.0
|
||||
|
||||
oxc-resolver@11.15.0:
|
||||
optionalDependencies:
|
||||
'@oxc-resolver/binding-android-arm-eabi': 11.15.0
|
||||
@@ -15574,6 +15427,19 @@ snapshots:
|
||||
'@oxc-resolver/binding-win32-ia32-msvc': 11.15.0
|
||||
'@oxc-resolver/binding-win32-x64-msvc': 11.15.0
|
||||
|
||||
oxfmt@0.26.0:
|
||||
dependencies:
|
||||
tinypool: 2.0.0
|
||||
optionalDependencies:
|
||||
'@oxfmt/darwin-arm64': 0.26.0
|
||||
'@oxfmt/darwin-x64': 0.26.0
|
||||
'@oxfmt/linux-arm64-gnu': 0.26.0
|
||||
'@oxfmt/linux-arm64-musl': 0.26.0
|
||||
'@oxfmt/linux-x64-gnu': 0.26.0
|
||||
'@oxfmt/linux-x64-musl': 0.26.0
|
||||
'@oxfmt/win32-arm64': 0.26.0
|
||||
'@oxfmt/win32-x64': 0.26.0
|
||||
|
||||
oxlint-tsgolint@0.9.1:
|
||||
optionalDependencies:
|
||||
'@oxlint-tsgolint/darwin-arm64': 0.9.1
|
||||
@@ -15754,7 +15620,8 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.7.4: {}
|
||||
prettier@3.7.4:
|
||||
optional: true
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
|
||||
@@ -16717,6 +16584,8 @@ snapshots:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tinypool@2.0.0: {}
|
||||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
tinyrainbow@3.0.3: {}
|
||||
|
||||
@@ -17,7 +17,6 @@ catalog:
|
||||
'@nx/vite': 22.2.6
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@playwright/test': ^1.57.0
|
||||
'@prettier/plugin-oxc': ^0.1.3
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
'@primeuix/utils': ^0.3.2
|
||||
@@ -33,7 +32,6 @@ catalog:
|
||||
'@storybook/vue3': ^10.1.9
|
||||
'@storybook/vue3-vite': ^10.1.9
|
||||
'@tailwindcss/vite': ^4.1.12
|
||||
'@trivago/prettier-plugin-sort-imports': ^5.2.0
|
||||
'@types/fs-extra': ^11.0.4
|
||||
'@types/jsdom': ^21.1.7
|
||||
'@types/node': ^24.1.0
|
||||
@@ -70,12 +68,12 @@ catalog:
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 22.2.6
|
||||
oxfmt: ^0.26.0
|
||||
oxlint: ^1.33.0
|
||||
oxlint-tsgolint: ^0.9.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^3.0.4
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.7.4
|
||||
pretty-bytes: ^7.1.0
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
|
||||
@@ -12,6 +12,7 @@ declare global {
|
||||
const __ALGOLIA_API_KEY__: string
|
||||
const __USE_PROD_CONFIG__: boolean
|
||||
const __DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
|
||||
const __IS_NIGHTLY__: boolean
|
||||
}
|
||||
|
||||
type GlobalWithDefines = typeof globalThis & {
|
||||
@@ -22,6 +23,7 @@ type GlobalWithDefines = typeof globalThis & {
|
||||
__ALGOLIA_API_KEY__: string
|
||||
__USE_PROD_CONFIG__: boolean
|
||||
__DISTRIBUTION__: 'desktop' | 'localhost' | 'cloud'
|
||||
__IS_NIGHTLY__: boolean
|
||||
window?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -36,6 +38,7 @@ globalWithDefines.__ALGOLIA_APP_ID__ = ''
|
||||
globalWithDefines.__ALGOLIA_API_KEY__ = ''
|
||||
globalWithDefines.__USE_PROD_CONFIG__ = false
|
||||
globalWithDefines.__DISTRIBUTION__ = 'localhost'
|
||||
globalWithDefines.__IS_NIGHTLY__ = false
|
||||
|
||||
// Provide a minimal window shim for Node environment
|
||||
// This is needed for code that checks window existence during imports
|
||||
|
||||
82
src/components/boundingbox/WidgetBoundingBox.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-[auto_1fr] gap-x-2 gap-y-1">
|
||||
<label class="content-center text-xs text-node-component-slot-text">
|
||||
{{ $t('boundingBox.x') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="x"
|
||||
type="number"
|
||||
:min="0"
|
||||
step="1"
|
||||
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
|
||||
/>
|
||||
<label class="content-center text-xs text-node-component-slot-text">
|
||||
{{ $t('boundingBox.y') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="y"
|
||||
type="number"
|
||||
:min="0"
|
||||
step="1"
|
||||
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
|
||||
/>
|
||||
<label class="content-center text-xs text-node-component-slot-text">
|
||||
{{ $t('boundingBox.width') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="width"
|
||||
type="number"
|
||||
:min="1"
|
||||
step="1"
|
||||
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
|
||||
/>
|
||||
<label class="content-center text-xs text-node-component-slot-text">
|
||||
{{ $t('boundingBox.height') }}
|
||||
</label>
|
||||
<input
|
||||
v-model.number="height"
|
||||
type="number"
|
||||
:min="1"
|
||||
step="1"
|
||||
class="h-7 rounded-lg border-none bg-component-node-widget-background px-2 text-xs text-component-node-foreground focus:outline-0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
|
||||
const modelValue = defineModel<Bounds>({
|
||||
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
|
||||
})
|
||||
|
||||
const x = computed({
|
||||
get: () => modelValue.value.x,
|
||||
set: (x) => {
|
||||
modelValue.value = { ...modelValue.value, x }
|
||||
}
|
||||
})
|
||||
|
||||
const y = computed({
|
||||
get: () => modelValue.value.y,
|
||||
set: (y) => {
|
||||
modelValue.value = { ...modelValue.value, y }
|
||||
}
|
||||
})
|
||||
|
||||
const width = computed({
|
||||
get: () => modelValue.value.width,
|
||||
set: (width) => {
|
||||
modelValue.value = { ...modelValue.value, width }
|
||||
}
|
||||
})
|
||||
|
||||
const height = computed({
|
||||
get: () => modelValue.value.height,
|
||||
set: (height) => {
|
||||
modelValue.value = { ...modelValue.value, height }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="node-actions touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
|
||||
>
|
||||
<slot name="actions" :node="props.node" />
|
||||
</div>
|
||||
|
||||
100
src/components/imagecrop/WidgetImageCrop.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div
|
||||
class="widget-expands relative flex h-full w-full flex-col gap-1"
|
||||
@pointerdown.stop
|
||||
@pointermove.stop
|
||||
@pointerup.stop
|
||||
>
|
||||
<!-- Image preview container -->
|
||||
<div
|
||||
ref="containerEl"
|
||||
class="relative min-h-0 flex-1 overflow-hidden rounded-[5px] bg-node-component-surface"
|
||||
>
|
||||
<div v-if="isLoading" class="flex size-full items-center justify-center">
|
||||
<span class="text-sm">{{ $t('imageCrop.loading') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!imageUrl"
|
||||
class="flex size-full flex-col items-center justify-center text-center"
|
||||
>
|
||||
<i class="mb-2 icon-[lucide--image] h-12 w-12" />
|
||||
<p class="text-sm">{{ $t('imageCrop.noInputImage') }}</p>
|
||||
</div>
|
||||
|
||||
<img
|
||||
v-else
|
||||
ref="imageEl"
|
||||
:src="imageUrl"
|
||||
:alt="$t('imageCrop.cropPreviewAlt')"
|
||||
draggable="false"
|
||||
class="block size-full object-contain select-none brightness-50"
|
||||
@load="handleImageLoad"
|
||||
@error="handleImageError"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="imageUrl && !isLoading"
|
||||
class="absolute box-content cursor-move overflow-hidden border-2 border-white"
|
||||
:style="cropBoxStyle"
|
||||
@pointerdown="handleDragStart"
|
||||
@pointermove="handleDragMove"
|
||||
@pointerup="handleDragEnd"
|
||||
>
|
||||
<div class="pointer-events-none size-full" :style="cropImageStyle" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="handle in resizeHandles"
|
||||
v-show="imageUrl && !isLoading"
|
||||
:key="handle.direction"
|
||||
:class="['absolute', handle.class]"
|
||||
:style="handle.style"
|
||||
@pointerdown="(e) => handleResizeStart(e, handle.direction)"
|
||||
@pointermove="handleResizeMove"
|
||||
@pointerup="handleResizeEnd"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<WidgetBoundingBox v-model="modelValue" class="shrink-0" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
|
||||
import WidgetBoundingBox from '@/components/boundingbox/WidgetBoundingBox.vue'
|
||||
import { useImageCrop } from '@/composables/useImageCrop'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<Bounds>({
|
||||
default: () => ({ x: 0, y: 0, width: 512, height: 512 })
|
||||
})
|
||||
|
||||
const imageEl = useTemplateRef<HTMLImageElement>('imageEl')
|
||||
const containerEl = useTemplateRef<HTMLDivElement>('containerEl')
|
||||
|
||||
const {
|
||||
imageUrl,
|
||||
isLoading,
|
||||
|
||||
cropBoxStyle,
|
||||
cropImageStyle,
|
||||
resizeHandles,
|
||||
|
||||
handleImageLoad,
|
||||
handleImageError,
|
||||
handleDragStart,
|
||||
handleDragMove,
|
||||
handleDragEnd,
|
||||
handleResizeStart,
|
||||
handleResizeMove,
|
||||
handleResizeEnd
|
||||
} = useImageCrop(props.nodeId, { imageEl, containerEl, modelValue })
|
||||
</script>
|
||||
52
src/components/primevueOverride/AutoCompletePlus.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- Auto complete with extra event "focused-option-changed" -->
|
||||
<script>
|
||||
import AutoComplete from 'primevue/autocomplete'
|
||||
|
||||
export default {
|
||||
name: 'AutoCompletePlus',
|
||||
extends: AutoComplete,
|
||||
emits: ['focused-option-changed'],
|
||||
data() {
|
||||
return {
|
||||
// Flag to determine if IME is active
|
||||
isComposing: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (typeof AutoComplete.mounted === 'function') {
|
||||
AutoComplete.mounted.call(this)
|
||||
}
|
||||
|
||||
// Retrieve the actual <input> element and attach IME events
|
||||
const inputEl = this.$el.querySelector('input')
|
||||
if (inputEl) {
|
||||
inputEl.addEventListener('compositionstart', () => {
|
||||
this.isComposing = true
|
||||
})
|
||||
inputEl.addEventListener('compositionend', () => {
|
||||
this.isComposing = false
|
||||
})
|
||||
}
|
||||
// Add a watcher on the focusedOptionIndex property
|
||||
this.$watch(
|
||||
() => this.focusedOptionIndex,
|
||||
(newVal, oldVal) => {
|
||||
// Emit a custom event when focusedOptionIndex changes
|
||||
this.$emit('focused-option-changed', newVal)
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
// Override onKeyDown to block Enter when IME is active
|
||||
onKeyDown(event) {
|
||||
if (event.key === 'Enter' && this.isComposing) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
AutoComplete.methods.onKeyDown.call(this, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -200,7 +200,13 @@ const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {
|
||||
|
||||
if (item.state === 'running' || item.state === 'initialization') {
|
||||
// Running/initializing jobs: interrupt execution
|
||||
await api.interrupt(promptId)
|
||||
// Cloud backend uses deleteItem, local uses interrupt
|
||||
if (isCloud) {
|
||||
await api.deleteItem('queue', promptId)
|
||||
} else {
|
||||
await api.interrupt(promptId)
|
||||
}
|
||||
executionStore.clearInitializationByPromptId(promptId)
|
||||
await queueStore.update()
|
||||
} else if (item.state === 'pending') {
|
||||
// Pending jobs: remove from queue
|
||||
@@ -268,7 +274,15 @@ const inspectJobAsset = wrapWithErrorHandlingAsync(
|
||||
)
|
||||
|
||||
const cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => {
|
||||
// Capture pending promptIds before clearing
|
||||
const pendingPromptIds = queueStore.pendingTasks
|
||||
.map((task) => task.promptId)
|
||||
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
||||
|
||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||
|
||||
// Clear initialization state for removed prompts
|
||||
executionStore.clearInitializationByPromptIds(pendingPromptIds)
|
||||
})
|
||||
|
||||
const interruptAll = wrapWithErrorHandlingAsync(async () => {
|
||||
@@ -284,10 +298,14 @@ const interruptAll = wrapWithErrorHandlingAsync(async () => {
|
||||
// on cloud to ensure we cancel the workflow the user clicked.
|
||||
if (isCloud) {
|
||||
await Promise.all(promptIds.map((id) => api.deleteItem('queue', id)))
|
||||
executionStore.clearInitializationByPromptIds(promptIds)
|
||||
await queueStore.update()
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all(promptIds.map((id) => api.interrupt(id)))
|
||||
executionStore.clearInitializationByPromptIds(promptIds)
|
||||
await queueStore.update()
|
||||
})
|
||||
|
||||
const showClearHistoryDialog = () => {
|
||||
|
||||
@@ -25,13 +25,31 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const shownWidgets = widgets
|
||||
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced)
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
|
||||
return { widgets: shownWidgets, node }
|
||||
})
|
||||
})
|
||||
|
||||
const advancedWidgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const advancedWidgets = widgets
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden) && w.options?.advanced
|
||||
)
|
||||
.map((widget) => ({ node, widget }))
|
||||
return { widgets: advancedWidgets, node }
|
||||
})
|
||||
.filter(({ widgets }) => widgets.length > 0)
|
||||
})
|
||||
|
||||
const isMultipleNodesSelected = computed(
|
||||
() => widgetsSectionDataList.value.length > 1
|
||||
)
|
||||
@@ -56,6 +74,12 @@ const label = computed(() => {
|
||||
: t('rightSidePanel.inputsNone')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
|
||||
const advancedLabel = computed(() => {
|
||||
return !mustShowNodeTitle && !isMultipleNodesSelected.value
|
||||
? t('rightSidePanel.advancedInputs')
|
||||
: undefined // SectionWidgets display node titles by default
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -93,4 +117,16 @@ const label = computed(() => {
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
<SectionWidgets
|
||||
v-for="{ widgets, node } in advancedWidgetsSectionDataList"
|
||||
:key="`advanced-${node.id}`"
|
||||
:collapse="true"
|
||||
:node
|
||||
:label="advancedLabel"
|
||||
:widgets
|
||||
:show-locate-button="isMultipleNodesSelected"
|
||||
class="border-b border-interface-stroke"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { getNodeByExecutionId } from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { renameWidget } from '@/utils/widgetUtil'
|
||||
|
||||
import { renameWidget } from '../shared'
|
||||
import WidgetActions from './WidgetActions.vue'
|
||||
|
||||
const {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { InjectionKey, MaybeRefOrGetter } from 'vue'
|
||||
import { computed, toValue } from 'vue'
|
||||
|
||||
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
|
||||
@@ -205,67 +203,3 @@ function repeatItems<T>(items: T[]): T[] {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a widget and its corresponding input.
|
||||
* Handles both regular widgets and proxy widgets in subgraphs.
|
||||
*
|
||||
* @param widget The widget to rename
|
||||
* @param node The node containing the widget
|
||||
* @param newLabel The new label for the widget (empty string or undefined to clear)
|
||||
* @param parents Optional array of parent SubgraphNodes (for proxy widgets)
|
||||
* @returns true if the rename was successful, false otherwise
|
||||
*/
|
||||
export function renameWidget(
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode,
|
||||
newLabel: string,
|
||||
parents?: SubgraphNode[]
|
||||
): boolean {
|
||||
// For proxy widgets in subgraphs, we need to rename the original interior widget
|
||||
if (isProxyWidget(widget) && parents?.length) {
|
||||
const subgraph = parents[0].subgraph
|
||||
if (!subgraph) {
|
||||
console.error('Could not find subgraph for proxy widget')
|
||||
return false
|
||||
}
|
||||
const interiorNode = subgraph.getNodeById(parseInt(widget._overlay.nodeId))
|
||||
|
||||
if (!interiorNode) {
|
||||
console.error('Could not find interior node for proxy widget')
|
||||
return false
|
||||
}
|
||||
|
||||
const originalWidget = interiorNode.widgets?.find(
|
||||
(w) => w.name === widget._overlay.widgetName
|
||||
)
|
||||
|
||||
if (!originalWidget) {
|
||||
console.error('Could not find original widget for proxy widget')
|
||||
return false
|
||||
}
|
||||
|
||||
// Rename the original widget
|
||||
originalWidget.label = newLabel || undefined
|
||||
|
||||
// Also rename the corresponding input on the interior node
|
||||
const interiorInput = interiorNode.inputs?.find(
|
||||
(inp) => inp.widget?.name === widget._overlay.widgetName
|
||||
)
|
||||
if (interiorInput) {
|
||||
interiorInput.label = newLabel || undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Always rename the widget on the current node (either regular widget or proxy widget)
|
||||
const input = node.inputs?.find((inp) => inp.widget?.name === widget.name)
|
||||
|
||||
// Intentionally mutate the widget object here as it's a reference
|
||||
// to the actual widget in the graph
|
||||
widget.label = newLabel || undefined
|
||||
if (input) {
|
||||
input.label = newLabel || undefined
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -13,12 +13,20 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
:aria-label="$t('g.addNodeFilterCondition')"
|
||||
class="filter-button z-10"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
>
|
||||
<i class="pi pi-filter" />
|
||||
</Button>
|
||||
<Dialog
|
||||
v-model:visible="nodeSearchFilterVisible"
|
||||
class="min-w-96"
|
||||
dismissable-mask
|
||||
modal
|
||||
@hide="nextTick(() => inputRef?.focus())"
|
||||
@hide="reFocusInput"
|
||||
>
|
||||
<template #header>
|
||||
<h3>{{ $t('g.addNodeFilterCondition') }}</h3>
|
||||
@@ -28,84 +36,57 @@
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<div class="comfy-vue-node-search-box z-10 grow">
|
||||
<div
|
||||
class="flex w-full items-center bg-base-background rounded-lg py-1 px-4 border-primary-background border"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
:aria-label="$t('g.addNodeFilterCondition')"
|
||||
class="filter-button z-10 absolute -left-10"
|
||||
@click="nodeSearchFilterVisible = true"
|
||||
>
|
||||
<i class="pi pi-filter" />
|
||||
</Button>
|
||||
<template
|
||||
v-for="value in filters"
|
||||
<AutoCompletePlus
|
||||
ref="autoCompletePlus"
|
||||
:model-value="filters"
|
||||
class="comfy-vue-node-search-box z-10 grow"
|
||||
scroll-height="40vh"
|
||||
:placeholder="placeholder"
|
||||
:input-id="inputId"
|
||||
append-to="self"
|
||||
:suggestions="suggestions"
|
||||
:delay="100"
|
||||
:loading="!nodeFrequencyStore.isLoaded"
|
||||
complete-on-focus
|
||||
auto-option-focus
|
||||
force-selection
|
||||
multiple
|
||||
option-label="display_name"
|
||||
@complete="search($event.query)"
|
||||
@option-select="onAddNode($event.value)"
|
||||
@focused-option-changed="setHoverSuggestion($event)"
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<NodeSearchItem :node-def="option" :current-query="currentQuery" />
|
||||
</template>
|
||||
<!-- FilterAndValue -->
|
||||
<template #chip="{ value }">
|
||||
<SearchFilterChip
|
||||
v-if="value.filterDef && value.value"
|
||||
:key="`${value.filterDef.id}-${value.value}`"
|
||||
>
|
||||
<SearchFilterChip
|
||||
v-if="value.filterDef && value.value"
|
||||
:text="value.value"
|
||||
:badge="value.filterDef.invokeSequence.toUpperCase()"
|
||||
:badge-class="value.filterDef.invokeSequence + '-badge'"
|
||||
@remove="
|
||||
onRemoveFilter(
|
||||
$event,
|
||||
value as FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<input
|
||||
ref="inputRef"
|
||||
v-model="currentQuery"
|
||||
class="text-base h-5 bg-transparent border-0 focus:outline-0 flex-1"
|
||||
type="text"
|
||||
autofocus
|
||||
:placeholder="t('g.searchNodes') + '...'"
|
||||
@keydown.enter.prevent="onAddNode(hoveredSuggestion)"
|
||||
@keydown.down.prevent="updateIndexBy(1)"
|
||||
@keydown.up.prevent="updateIndexBy(-1)"
|
||||
:text="value.value"
|
||||
:badge="value.filterDef.invokeSequence.toUpperCase()"
|
||||
:badge-class="value.filterDef.invokeSequence + '-badge'"
|
||||
@remove="
|
||||
onRemoveFilter(
|
||||
$event,
|
||||
value as FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-bind="containerProps"
|
||||
class="bg-comfy-menu-bg p-1 rounded-lg border-border-subtle border max-h-150"
|
||||
>
|
||||
<div v-bind="wrapperProps" class="comfy-autocomplete-list">
|
||||
<NodeSearchItem
|
||||
v-for="{ data: option, index } in virtualList"
|
||||
:key="index"
|
||||
:class="
|
||||
cn(
|
||||
'p-1 rounded-sm',
|
||||
hoveredIndex === index && 'bg-secondary-background-hover'
|
||||
)
|
||||
"
|
||||
:node-def="option"
|
||||
:current-query="debouncedQuery"
|
||||
@click="onAddNode(option)"
|
||||
@pointerover="hoveredIndex = index"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="suggestions.length === 0"
|
||||
class="p-1"
|
||||
v-text="t('g.noResultsFound')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AutoCompletePlus>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { refDebounced, useVirtualList } from '@vueuse/core'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { computed, nextTick, ref, useTemplateRef, watchEffect } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NodePreview from '@/components/node/NodePreview.vue'
|
||||
import AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'
|
||||
import NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'
|
||||
import NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -114,7 +95,6 @@ import { useTelemetry } from '@/platform/telemetry'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'
|
||||
import type { FuseFilterWithValue } from '@/utils/fuseUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import SearchFilterChip from '../common/SearchFilterChip.vue'
|
||||
|
||||
@@ -131,52 +111,68 @@ const { filters, searchLimit = 64 } = defineProps<{
|
||||
searchLimit?: number
|
||||
}>()
|
||||
|
||||
const autoCompletePlus = ref()
|
||||
const nodeSearchFilterVisible = ref(false)
|
||||
const inputId = `comfy-vue-node-search-box-input-${Math.random()}`
|
||||
const suggestions = ref<ComfyNodeDefImpl[]>([])
|
||||
const hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)
|
||||
const currentQuery = ref('')
|
||||
const debouncedQuery = refDebounced(currentQuery, 100, { maxWait: 400 })
|
||||
const inputRef = useTemplateRef('inputRef')
|
||||
const placeholder = computed(() => {
|
||||
return filters.length === 0 ? t('g.searchNodes') + '...' : ''
|
||||
})
|
||||
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const nodeFrequencyStore = useNodeFrequencyStore()
|
||||
|
||||
watchEffect(() => {
|
||||
const query = debouncedQuery.value
|
||||
// Debounced search tracking (500ms as per implementation plan)
|
||||
const debouncedTrackSearch = debounce((query: string) => {
|
||||
if (query.trim()) {
|
||||
telemetry?.trackNodeSearch({ query })
|
||||
}
|
||||
})
|
||||
}, 500)
|
||||
|
||||
const suggestions = computed(() => {
|
||||
const query = debouncedQuery.value
|
||||
const search = (query: string) => {
|
||||
const queryIsEmpty = query === '' && filters.length === 0
|
||||
|
||||
return queryIsEmpty
|
||||
currentQuery.value = query
|
||||
suggestions.value = queryIsEmpty
|
||||
? nodeFrequencyStore.topNodeDefs
|
||||
: [
|
||||
...nodeDefStore.nodeSearchService.searchNode(query, filters, {
|
||||
limit: searchLimit
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
const {
|
||||
list: virtualList,
|
||||
containerProps,
|
||||
wrapperProps
|
||||
} = useVirtualList(suggestions, { itemHeight: 40 })
|
||||
// Track search queries with debounce
|
||||
debouncedTrackSearch(query)
|
||||
}
|
||||
|
||||
const emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])
|
||||
|
||||
// Track node selection and emit addNode event
|
||||
const onAddNode = (nodeDef?: ComfyNodeDefImpl) => {
|
||||
if (!nodeDef) return
|
||||
const onAddNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
telemetry?.trackNodeSearchResultSelected({
|
||||
node_type: nodeDef.name,
|
||||
last_query: debouncedQuery.value
|
||||
last_query: currentQuery.value
|
||||
})
|
||||
emit('addNode', nodeDef)
|
||||
}
|
||||
|
||||
let inputElement: HTMLInputElement | null = null
|
||||
const reFocusInput = async () => {
|
||||
inputElement ??= document.getElementById(inputId) as HTMLInputElement
|
||||
if (inputElement) {
|
||||
inputElement.blur()
|
||||
await nextTick(() => inputElement?.focus())
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
inputElement ??= document.getElementById(inputId) as HTMLInputElement
|
||||
if (inputElement) inputElement.focus()
|
||||
autoCompletePlus.value.hide = () => search('')
|
||||
search('')
|
||||
autoCompletePlus.value.show()
|
||||
})
|
||||
const onAddFilter = (
|
||||
filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>
|
||||
) => {
|
||||
@@ -190,16 +186,14 @@ const onRemoveFilter = async (
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
emit('removeFilter', filterAndValue)
|
||||
inputRef.value?.focus()
|
||||
await reFocusInput()
|
||||
}
|
||||
const hoveredIndex = ref<number>()
|
||||
const hoveredSuggestion = computed(() =>
|
||||
hoveredIndex.value ? suggestions.value[hoveredIndex.value] : undefined
|
||||
)
|
||||
function updateIndexBy(delta: number) {
|
||||
hoveredIndex.value = Math.max(
|
||||
0,
|
||||
Math.min(suggestions.value.length, (hoveredIndex.value ?? 0) + delta)
|
||||
)
|
||||
const setHoverSuggestion = (index: number) => {
|
||||
if (index === -1) {
|
||||
hoveredSuggestion.value = null
|
||||
return
|
||||
}
|
||||
const value = suggestions.value[index]
|
||||
hoveredSuggestion.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
},
|
||||
mask: { class: 'node-search-box-dialog-mask' },
|
||||
transition: {
|
||||
enterFromClass: 'opacity-0',
|
||||
enterActiveClass: 'transition-all duration-20 ease-out',
|
||||
leaveActiveClass: 'transition-all duration-20 ease-in',
|
||||
enterFromClass: 'opacity-0 scale-75',
|
||||
// 100ms is the duration of the transition in the dialog component
|
||||
enterActiveClass: 'transition-all duration-100 ease-out',
|
||||
leaveActiveClass: 'transition-all duration-100 ease-in',
|
||||
leaveToClass: 'opacity-0 scale-75'
|
||||
}
|
||||
}"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:label="$t('menu.help')"
|
||||
:tooltip="$t('sideToolbar.helpCenter')"
|
||||
:icon-badge="shouldShowRedDot ? '•' : ''"
|
||||
badge-class="-top-1 -right-1 min-w-2 w-2 h-2 p-0 rounded-full text-[0px] bg-[#ff3b30]"
|
||||
:is-small="isSmall"
|
||||
@click="toggleHelpCenter"
|
||||
/>
|
||||
@@ -21,24 +22,3 @@ defineProps<{
|
||||
|
||||
const { shouldShowRedDot, toggleHelpCenter } = useHelpCenter()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-badge) {
|
||||
background: #ff3b30;
|
||||
color: #ff3b30;
|
||||
min-width: 8px;
|
||||
height: 8px;
|
||||
padding: 0;
|
||||
border-radius: 9999px;
|
||||
font-size: 0;
|
||||
margin-top: 4px;
|
||||
margin-right: 4px;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.p-badge.p-badge-dot) {
|
||||
width: 8px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import OverlayBadge from 'primevue/overlaybadge'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
@@ -33,8 +32,7 @@ describe('SidebarIcon', () => {
|
||||
return mount(SidebarIcon, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
directives: { tooltip: Tooltip },
|
||||
components: { OverlayBadge }
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props: { ...exampleProps, ...props },
|
||||
...options
|
||||
@@ -54,9 +52,9 @@ describe('SidebarIcon', () => {
|
||||
it('creates badge when iconBadge prop is set', () => {
|
||||
const badge = '2'
|
||||
const wrapper = mountSidebarIcon({ iconBadge: badge })
|
||||
const badgeEl = wrapper.findComponent(OverlayBadge)
|
||||
const badgeEl = wrapper.find('.sidebar-icon-badge')
|
||||
expect(badgeEl.exists()).toBe(true)
|
||||
expect(badgeEl.find('.p-badge').text()).toEqual(badge)
|
||||
expect(badgeEl.text()).toEqual(badge)
|
||||
})
|
||||
|
||||
it('shows tooltip on hover', async () => {
|
||||
|
||||
@@ -17,22 +17,28 @@
|
||||
>
|
||||
<div class="side-bar-button-content">
|
||||
<slot name="icon">
|
||||
<OverlayBadge v-if="shouldShowBadge" :value="overlayValue">
|
||||
<div class="sidebar-icon-wrapper relative">
|
||||
<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"
|
||||
/>
|
||||
<component
|
||||
:is="icon"
|
||||
v-else-if="typeof icon === 'object'"
|
||||
class="side-bar-button-icon"
|
||||
/>
|
||||
<span
|
||||
v-if="shouldShowBadge"
|
||||
:class="
|
||||
cn(
|
||||
'sidebar-icon-badge absolute min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-base-foreground',
|
||||
badgeClass || '-top-1 -right-1'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ overlayValue }}
|
||||
</span>
|
||||
</div>
|
||||
</slot>
|
||||
<span v-if="label && !isSmall" class="side-bar-button-label">{{
|
||||
t(label)
|
||||
@@ -42,7 +48,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import OverlayBadge from 'primevue/overlaybadge'
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -57,6 +62,7 @@ const {
|
||||
tooltip = '',
|
||||
tooltipSuffix = '',
|
||||
iconBadge = '',
|
||||
badgeClass = '',
|
||||
label = '',
|
||||
isSmall = false
|
||||
} = defineProps<{
|
||||
@@ -65,6 +71,7 @@ const {
|
||||
tooltip?: string
|
||||
tooltipSuffix?: string
|
||||
iconBadge?: string | (() => string | null)
|
||||
badgeClass?: string
|
||||
label?: string
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
@@ -44,9 +44,15 @@
|
||||
:class="cn('px-2', activeJobItems.length && 'mt-2')"
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
class="flex items-center p-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
>
|
||||
{{ t('sideToolbar.generatedAssetsHeader') }}
|
||||
{{
|
||||
t(
|
||||
assetType === 'input'
|
||||
? 'sideToolbar.importedAssetsHeader'
|
||||
: 'sideToolbar.generatedAssetsHeader'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,9 +124,14 @@ import {
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { assets, isSelected } = defineProps<{
|
||||
const {
|
||||
assets,
|
||||
isSelected,
|
||||
assetType = 'output'
|
||||
} = defineProps<{
|
||||
assets: AssetItem[]
|
||||
isSelected: (assetId: string) => boolean
|
||||
assetType?: 'input' | 'output'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
v-if="isListView"
|
||||
:assets="displayAssets"
|
||||
:is-selected="isSelected"
|
||||
:asset-type="activeTab"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
@@ -243,6 +244,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { getJobDetail } from '@/services/jobOutputCache'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { ResultItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -256,6 +258,7 @@ interface JobOutputItem {
|
||||
const { t, n } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('output')
|
||||
@@ -510,7 +513,13 @@ const handleBulkDelete = async (assets: AssetItem[]) => {
|
||||
}
|
||||
|
||||
const handleClearQueue = async () => {
|
||||
const pendingPromptIds = queueStore.pendingTasks
|
||||
.map((task) => task.promptId)
|
||||
.filter((id): id is string => typeof id === 'string' && id.length > 0)
|
||||
|
||||
await commandStore.execute('Comfy.ClearPendingTasks')
|
||||
|
||||
executionStore.clearInitializationByPromptIds(pendingPromptIds)
|
||||
}
|
||||
|
||||
const handleBulkAddToWorkflow = async (assets: AssetItem[]) => {
|
||||
|
||||
@@ -398,6 +398,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
vueNodeData.set(id, extractVueNodeData(node))
|
||||
|
||||
const initializeVueNodeLayout = () => {
|
||||
// Check if the node was removed mid-sequence
|
||||
if (!nodeRefs.has(id)) return
|
||||
|
||||
// Extract actual positions after configure() has potentially updated them
|
||||
const nodePosition = { x: node.pos[0], y: node.pos[1] }
|
||||
const nodeSize = { width: node.size[0], height: node.size[1] }
|
||||
@@ -427,7 +430,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
} else {
|
||||
// Not during workflow loading - initialize layout immediately
|
||||
// This handles individual node additions during normal operation
|
||||
initializeVueNodeLayout()
|
||||
requestAnimationFrame(initializeVueNodeLayout)
|
||||
}
|
||||
|
||||
// Call original callback if provided
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { Ref } from 'vue'
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import type { MenuEntry } from '@/composables/queue/useJobMenu'
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: false
|
||||
}))
|
||||
|
||||
const downloadFileMock = vi.fn()
|
||||
vi.mock('@/base/common/downloadUtil', () => ({
|
||||
downloadFile: (...args: any[]) => downloadFileMock(...args)
|
||||
@@ -55,7 +59,8 @@ const workflowStoreMock = {
|
||||
createTemporary: vi.fn()
|
||||
}
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => workflowStoreMock
|
||||
useWorkflowStore: () => workflowStoreMock,
|
||||
ComfyWorkflow: class {}
|
||||
}))
|
||||
|
||||
const interruptMock = vi.fn()
|
||||
@@ -104,6 +109,13 @@ vi.mock('@/stores/queueStore', () => ({
|
||||
useQueueStore: () => queueStoreMock
|
||||
}))
|
||||
|
||||
const executionStoreMock = {
|
||||
clearInitializationByPromptId: vi.fn()
|
||||
}
|
||||
vi.mock('@/stores/executionStore', () => ({
|
||||
useExecutionStore: () => executionStoreMock
|
||||
}))
|
||||
|
||||
const getJobWorkflowMock = vi.fn()
|
||||
vi.mock('@/services/jobOutputCache', () => ({
|
||||
getJobWorkflow: (...args: any[]) => getJobWorkflowMock(...args)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { st, t } from '@/i18n'
|
||||
import { mapTaskOutputToAssetItem } from '@/platform/assets/composables/media/assetMappers'
|
||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -15,6 +16,7 @@ import { downloadBlob } from '@/scripts/utils'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { getJobWorkflow } from '@/services/jobOutputCache'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
@@ -44,6 +46,7 @@ export function useJobMenu(
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const litegraphService = useLitegraphService()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
@@ -72,10 +75,15 @@ export function useJobMenu(
|
||||
const target = resolveItem(item)
|
||||
if (!target) return
|
||||
if (target.state === 'running' || target.state === 'initialization') {
|
||||
await api.interrupt(target.id)
|
||||
if (isCloud) {
|
||||
await api.deleteItem('queue', target.id)
|
||||
} else {
|
||||
await api.interrupt(target.id)
|
||||
}
|
||||
} else if (target.state === 'pending') {
|
||||
await api.deleteItem('queue', target.id)
|
||||
}
|
||||
executionStore.clearInitializationByPromptId(target.id)
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AssetsSidebarTab from '@/components/sidebar/tabs/AssetsSidebarTab.vue'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useAssetsSidebarTab = (): SidebarTabExtension => {
|
||||
@@ -11,6 +12,12 @@ export const useAssetsSidebarTab = (): SidebarTabExtension => {
|
||||
tooltip: 'sideToolbar.assets',
|
||||
label: 'sideToolbar.labels.assets',
|
||||
component: markRaw(AssetsSidebarTab),
|
||||
type: 'vue'
|
||||
type: 'vue',
|
||||
iconBadge: () => {
|
||||
const queueStore = useQueueStore()
|
||||
return queueStore.pendingTasks.length > 0
|
||||
? queueStore.pendingTasks.length.toString()
|
||||
: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +123,7 @@ export const useContextMenuTranslation = () => {
|
||||
}
|
||||
|
||||
// for capture translation text of input and widget
|
||||
const extraInfo = (options.extra ||
|
||||
options.parentMenu?.options?.extra) as
|
||||
const extraInfo = (options.extra || options.parentMenu?.options?.extra) as
|
||||
| { inputs?: INodeInputSlot[]; widgets?: IWidget[] }
|
||||
| undefined
|
||||
// widgets and inputs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
@@ -95,6 +96,8 @@ export function useFeatureFlags() {
|
||||
)
|
||||
},
|
||||
get teamWorkspacesEnabled() {
|
||||
if (!isCloud) return false
|
||||
|
||||
return (
|
||||
remoteConfig.value.team_workspaces_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.TEAM_WORKSPACES_ENABLED, false)
|
||||
|
||||
469
src/composables/useImageCrop.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
|
||||
type ResizeDirection =
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'nw'
|
||||
| 'ne'
|
||||
| 'sw'
|
||||
| 'se'
|
||||
|
||||
const HANDLE_SIZE = 8
|
||||
const CORNER_SIZE = 10
|
||||
const MIN_CROP_SIZE = 16
|
||||
const CROP_BOX_BORDER = 2
|
||||
|
||||
interface UseImageCropOptions {
|
||||
imageEl: Ref<HTMLImageElement | null>
|
||||
containerEl: Ref<HTMLDivElement | null>
|
||||
modelValue: Ref<Bounds>
|
||||
}
|
||||
|
||||
export function useImageCrop(nodeId: NodeId, options: UseImageCropOptions) {
|
||||
const { imageEl, containerEl, modelValue } = options
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const node = ref<LGraphNode | null>(null)
|
||||
|
||||
const imageUrl = ref<string | null>(null)
|
||||
const isLoading = ref(false)
|
||||
|
||||
const naturalWidth = ref(0)
|
||||
const naturalHeight = ref(0)
|
||||
const displayedWidth = ref(0)
|
||||
const displayedHeight = ref(0)
|
||||
const scaleFactor = ref(1)
|
||||
const imageOffsetX = ref(0)
|
||||
const imageOffsetY = ref(0)
|
||||
|
||||
const cropX = computed({
|
||||
get: () => modelValue.value.x,
|
||||
set: (v: number) => {
|
||||
modelValue.value.x = v
|
||||
}
|
||||
})
|
||||
|
||||
const cropY = computed({
|
||||
get: () => modelValue.value.y,
|
||||
set: (v: number) => {
|
||||
modelValue.value.y = v
|
||||
}
|
||||
})
|
||||
|
||||
const cropWidth = computed({
|
||||
get: () => modelValue.value.width || 512,
|
||||
set: (v: number) => {
|
||||
modelValue.value.width = v
|
||||
}
|
||||
})
|
||||
|
||||
const cropHeight = computed({
|
||||
get: () => modelValue.value.height || 512,
|
||||
set: (v: number) => {
|
||||
modelValue.value.height = v
|
||||
}
|
||||
})
|
||||
|
||||
const isDragging = ref(false)
|
||||
const dragStartX = ref(0)
|
||||
const dragStartY = ref(0)
|
||||
const dragStartCropX = ref(0)
|
||||
const dragStartCropY = ref(0)
|
||||
|
||||
const isResizing = ref(false)
|
||||
const resizeDirection = ref<ResizeDirection | null>(null)
|
||||
const resizeStartX = ref(0)
|
||||
const resizeStartY = ref(0)
|
||||
const resizeStartCropX = ref(0)
|
||||
const resizeStartCropY = ref(0)
|
||||
const resizeStartCropWidth = ref(0)
|
||||
const resizeStartCropHeight = ref(0)
|
||||
|
||||
useResizeObserver(containerEl, () => {
|
||||
if (imageEl.value && imageUrl.value) {
|
||||
updateDisplayedDimensions()
|
||||
}
|
||||
})
|
||||
|
||||
const getInputImageUrl = (): string | null => {
|
||||
if (!node.value) return null
|
||||
|
||||
const inputNode = node.value.getInputNode(0)
|
||||
|
||||
if (!inputNode) return null
|
||||
|
||||
const urls = nodeOutputStore.getNodeImageUrls(inputNode)
|
||||
|
||||
if (urls?.length) {
|
||||
return urls[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const updateImageUrl = () => {
|
||||
imageUrl.value = getInputImageUrl()
|
||||
}
|
||||
|
||||
const updateDisplayedDimensions = () => {
|
||||
if (!imageEl.value || !containerEl.value) return
|
||||
|
||||
const img = imageEl.value
|
||||
const container = containerEl.value
|
||||
|
||||
naturalWidth.value = img.naturalWidth
|
||||
naturalHeight.value = img.naturalHeight
|
||||
|
||||
if (naturalWidth.value <= 0 || naturalHeight.value <= 0) {
|
||||
scaleFactor.value = 1
|
||||
return
|
||||
}
|
||||
|
||||
const containerWidth = container.clientWidth
|
||||
const containerHeight = container.clientHeight
|
||||
|
||||
const imageAspect = naturalWidth.value / naturalHeight.value
|
||||
const containerAspect = containerWidth / containerHeight
|
||||
|
||||
if (imageAspect > containerAspect) {
|
||||
displayedWidth.value = containerWidth
|
||||
displayedHeight.value = containerWidth / imageAspect
|
||||
imageOffsetX.value = 0
|
||||
imageOffsetY.value = (containerHeight - displayedHeight.value) / 2
|
||||
} else {
|
||||
displayedHeight.value = containerHeight
|
||||
displayedWidth.value = containerHeight * imageAspect
|
||||
imageOffsetX.value = (containerWidth - displayedWidth.value) / 2
|
||||
imageOffsetY.value = 0
|
||||
}
|
||||
|
||||
if (naturalWidth.value <= 0 || displayedWidth.value <= 0) {
|
||||
scaleFactor.value = 1
|
||||
} else {
|
||||
scaleFactor.value = displayedWidth.value / naturalWidth.value
|
||||
}
|
||||
}
|
||||
|
||||
const getEffectiveScale = (): number => {
|
||||
const container = containerEl.value
|
||||
|
||||
if (!container || naturalWidth.value <= 0 || displayedWidth.value <= 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const rect = container.getBoundingClientRect()
|
||||
const clientWidth = container.clientWidth
|
||||
|
||||
if (!clientWidth || !rect.width) return 1
|
||||
|
||||
const renderedDisplayedWidth =
|
||||
(displayedWidth.value / clientWidth) * rect.width
|
||||
|
||||
return renderedDisplayedWidth / naturalWidth.value
|
||||
}
|
||||
|
||||
const cropBoxStyle = computed(() => ({
|
||||
left: `${imageOffsetX.value + cropX.value * scaleFactor.value - CROP_BOX_BORDER}px`,
|
||||
top: `${imageOffsetY.value + cropY.value * scaleFactor.value - CROP_BOX_BORDER}px`,
|
||||
width: `${cropWidth.value * scaleFactor.value}px`,
|
||||
height: `${cropHeight.value * scaleFactor.value}px`
|
||||
}))
|
||||
|
||||
const cropImageStyle = computed(() => {
|
||||
if (!imageUrl.value) return {}
|
||||
|
||||
return {
|
||||
backgroundImage: `url(${imageUrl.value})`,
|
||||
backgroundSize: `${displayedWidth.value}px ${displayedHeight.value}px`,
|
||||
backgroundPosition: `-${cropX.value * scaleFactor.value}px -${cropY.value * scaleFactor.value}px`,
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}
|
||||
})
|
||||
|
||||
interface ResizeHandle {
|
||||
direction: ResizeDirection
|
||||
class: string
|
||||
style: {
|
||||
left: string
|
||||
top: string
|
||||
width?: string
|
||||
height?: string
|
||||
}
|
||||
}
|
||||
|
||||
const resizeHandles = computed<ResizeHandle[]>(() => {
|
||||
const x = imageOffsetX.value + cropX.value * scaleFactor.value
|
||||
const y = imageOffsetY.value + cropY.value * scaleFactor.value
|
||||
const w = cropWidth.value * scaleFactor.value
|
||||
const h = cropHeight.value * scaleFactor.value
|
||||
|
||||
return [
|
||||
{
|
||||
direction: 'top',
|
||||
class: 'h-2 cursor-ns-resize',
|
||||
style: {
|
||||
left: `${x + HANDLE_SIZE}px`,
|
||||
top: `${y - HANDLE_SIZE / 2}px`,
|
||||
width: `${Math.max(0, w - HANDLE_SIZE * 2)}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'bottom',
|
||||
class: 'h-2 cursor-ns-resize',
|
||||
style: {
|
||||
left: `${x + HANDLE_SIZE}px`,
|
||||
top: `${y + h - HANDLE_SIZE / 2}px`,
|
||||
width: `${Math.max(0, w - HANDLE_SIZE * 2)}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'left',
|
||||
class: 'w-2 cursor-ew-resize',
|
||||
style: {
|
||||
left: `${x - HANDLE_SIZE / 2}px`,
|
||||
top: `${y + HANDLE_SIZE}px`,
|
||||
height: `${Math.max(0, h - HANDLE_SIZE * 2)}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'right',
|
||||
class: 'w-2 cursor-ew-resize',
|
||||
style: {
|
||||
left: `${x + w - HANDLE_SIZE / 2}px`,
|
||||
top: `${y + HANDLE_SIZE}px`,
|
||||
height: `${Math.max(0, h - HANDLE_SIZE * 2)}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'nw',
|
||||
class: 'cursor-nwse-resize rounded-sm bg-white/80',
|
||||
style: {
|
||||
left: `${x - CORNER_SIZE / 2}px`,
|
||||
top: `${y - CORNER_SIZE / 2}px`,
|
||||
width: `${CORNER_SIZE}px`,
|
||||
height: `${CORNER_SIZE}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'ne',
|
||||
class: 'cursor-nesw-resize rounded-sm bg-white/80',
|
||||
style: {
|
||||
left: `${x + w - CORNER_SIZE / 2}px`,
|
||||
top: `${y - CORNER_SIZE / 2}px`,
|
||||
width: `${CORNER_SIZE}px`,
|
||||
height: `${CORNER_SIZE}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'sw',
|
||||
class: 'cursor-nesw-resize rounded-sm bg-white/80',
|
||||
style: {
|
||||
left: `${x - CORNER_SIZE / 2}px`,
|
||||
top: `${y + h - CORNER_SIZE / 2}px`,
|
||||
width: `${CORNER_SIZE}px`,
|
||||
height: `${CORNER_SIZE}px`
|
||||
}
|
||||
},
|
||||
{
|
||||
direction: 'se',
|
||||
class: 'cursor-nwse-resize rounded-sm bg-white/80',
|
||||
style: {
|
||||
left: `${x + w - CORNER_SIZE / 2}px`,
|
||||
top: `${y + h - CORNER_SIZE / 2}px`,
|
||||
width: `${CORNER_SIZE}px`,
|
||||
height: `${CORNER_SIZE}px`
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const handleImageLoad = () => {
|
||||
isLoading.value = false
|
||||
updateDisplayedDimensions()
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
isLoading.value = false
|
||||
imageUrl.value = null
|
||||
}
|
||||
|
||||
const capturePointer = (e: PointerEvent) =>
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId)
|
||||
|
||||
const releasePointer = (e: PointerEvent) =>
|
||||
(e.target as HTMLElement).releasePointerCapture(e.pointerId)
|
||||
|
||||
const handleDragStart = (e: PointerEvent) => {
|
||||
if (!imageUrl.value) return
|
||||
|
||||
isDragging.value = true
|
||||
dragStartX.value = e.clientX
|
||||
dragStartY.value = e.clientY
|
||||
dragStartCropX.value = cropX.value
|
||||
dragStartCropY.value = cropY.value
|
||||
capturePointer(e)
|
||||
}
|
||||
|
||||
const handleDragMove = (e: PointerEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const effectiveScale = getEffectiveScale()
|
||||
if (effectiveScale === 0) return
|
||||
|
||||
const deltaX = (e.clientX - dragStartX.value) / effectiveScale
|
||||
const deltaY = (e.clientY - dragStartY.value) / effectiveScale
|
||||
|
||||
const maxX = naturalWidth.value - cropWidth.value
|
||||
const maxY = naturalHeight.value - cropHeight.value
|
||||
|
||||
cropX.value = Math.round(
|
||||
Math.max(0, Math.min(maxX, dragStartCropX.value + deltaX))
|
||||
)
|
||||
cropY.value = Math.round(
|
||||
Math.max(0, Math.min(maxY, dragStartCropY.value + deltaY))
|
||||
)
|
||||
}
|
||||
|
||||
const handleDragEnd = (e: PointerEvent) => {
|
||||
if (!isDragging.value) return
|
||||
|
||||
isDragging.value = false
|
||||
releasePointer(e)
|
||||
}
|
||||
|
||||
const handleResizeStart = (e: PointerEvent, direction: ResizeDirection) => {
|
||||
if (!imageUrl.value) return
|
||||
|
||||
e.stopPropagation()
|
||||
isResizing.value = true
|
||||
resizeDirection.value = direction
|
||||
|
||||
resizeStartX.value = e.clientX
|
||||
resizeStartY.value = e.clientY
|
||||
resizeStartCropX.value = cropX.value
|
||||
resizeStartCropY.value = cropY.value
|
||||
resizeStartCropWidth.value = cropWidth.value
|
||||
resizeStartCropHeight.value = cropHeight.value
|
||||
capturePointer(e)
|
||||
}
|
||||
|
||||
const handleResizeMove = (e: PointerEvent) => {
|
||||
if (!isResizing.value || !resizeDirection.value) return
|
||||
|
||||
const effectiveScale = getEffectiveScale()
|
||||
if (effectiveScale === 0) return
|
||||
|
||||
const dir = resizeDirection.value
|
||||
const deltaX = (e.clientX - resizeStartX.value) / effectiveScale
|
||||
const deltaY = (e.clientY - resizeStartY.value) / effectiveScale
|
||||
|
||||
const affectsLeft = dir === 'left' || dir === 'nw' || dir === 'sw'
|
||||
const affectsRight = dir === 'right' || dir === 'ne' || dir === 'se'
|
||||
const affectsTop = dir === 'top' || dir === 'nw' || dir === 'ne'
|
||||
const affectsBottom = dir === 'bottom' || dir === 'sw' || dir === 'se'
|
||||
|
||||
let newX = resizeStartCropX.value
|
||||
let newY = resizeStartCropY.value
|
||||
let newWidth = resizeStartCropWidth.value
|
||||
let newHeight = resizeStartCropHeight.value
|
||||
|
||||
if (affectsLeft) {
|
||||
const maxDeltaX = resizeStartCropWidth.value - MIN_CROP_SIZE
|
||||
const minDeltaX = -resizeStartCropX.value
|
||||
const clampedDeltaX = Math.max(minDeltaX, Math.min(maxDeltaX, deltaX))
|
||||
newX = resizeStartCropX.value + clampedDeltaX
|
||||
newWidth = resizeStartCropWidth.value - clampedDeltaX
|
||||
} else if (affectsRight) {
|
||||
const maxWidth = naturalWidth.value - resizeStartCropX.value
|
||||
newWidth = Math.max(
|
||||
MIN_CROP_SIZE,
|
||||
Math.min(maxWidth, resizeStartCropWidth.value + deltaX)
|
||||
)
|
||||
}
|
||||
|
||||
if (affectsTop) {
|
||||
const maxDeltaY = resizeStartCropHeight.value - MIN_CROP_SIZE
|
||||
const minDeltaY = -resizeStartCropY.value
|
||||
const clampedDeltaY = Math.max(minDeltaY, Math.min(maxDeltaY, deltaY))
|
||||
newY = resizeStartCropY.value + clampedDeltaY
|
||||
newHeight = resizeStartCropHeight.value - clampedDeltaY
|
||||
} else if (affectsBottom) {
|
||||
const maxHeight = naturalHeight.value - resizeStartCropY.value
|
||||
newHeight = Math.max(
|
||||
MIN_CROP_SIZE,
|
||||
Math.min(maxHeight, resizeStartCropHeight.value + deltaY)
|
||||
)
|
||||
}
|
||||
|
||||
if (affectsLeft || affectsRight) {
|
||||
cropX.value = Math.round(newX)
|
||||
cropWidth.value = Math.round(newWidth)
|
||||
}
|
||||
if (affectsTop || affectsBottom) {
|
||||
cropY.value = Math.round(newY)
|
||||
cropHeight.value = Math.round(newHeight)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResizeEnd = (e: PointerEvent) => {
|
||||
if (!isResizing.value) return
|
||||
|
||||
isResizing.value = false
|
||||
resizeDirection.value = null
|
||||
releasePointer(e)
|
||||
}
|
||||
|
||||
const initialize = () => {
|
||||
if (nodeId != null) {
|
||||
node.value = app.rootGraph?.getNodeById(nodeId) || null
|
||||
}
|
||||
|
||||
updateImageUrl()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => nodeOutputStore.nodeOutputs,
|
||||
() => updateImageUrl(),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => nodeOutputStore.nodePreviewImages,
|
||||
() => updateImageUrl(),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
onMounted(initialize)
|
||||
|
||||
return {
|
||||
imageUrl,
|
||||
isLoading,
|
||||
|
||||
cropX,
|
||||
cropY,
|
||||
cropWidth,
|
||||
cropHeight,
|
||||
|
||||
cropBoxStyle,
|
||||
cropImageStyle,
|
||||
resizeHandles,
|
||||
|
||||
handleImageLoad,
|
||||
handleImageError,
|
||||
handleDragStart,
|
||||
handleDragMove,
|
||||
handleDragEnd,
|
||||
handleResizeStart,
|
||||
handleResizeMove,
|
||||
handleResizeEnd
|
||||
}
|
||||
}
|
||||
@@ -511,6 +511,22 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
hasSkeleton.value = load3d?.hasSkeleton() ?? false
|
||||
// Reset skeleton visibility when loading new model
|
||||
modelConfig.value.showSkeleton = false
|
||||
|
||||
if (load3d) {
|
||||
const node = nodeRef.value
|
||||
|
||||
const modelWidget = node?.widgets?.find(
|
||||
(w) => w.name === 'model_file' || w.name === 'image'
|
||||
)
|
||||
const value = modelWidget?.value
|
||||
if (typeof value === 'string') {
|
||||
void Load3dUtils.generateThumbnailIfNeeded(
|
||||
load3d,
|
||||
value,
|
||||
isPreview.value ? 'output' : 'input'
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
skeletonVisibilityChange: (value: boolean) => {
|
||||
modelConfig.value.showSkeleton = value
|
||||
|
||||
@@ -368,7 +368,7 @@ export class GroupNodeConfig {
|
||||
}
|
||||
|
||||
getNodeDef(
|
||||
node: GroupNodeData
|
||||
node: GroupNodeData | GroupNodeWorkflowData['nodes'][number]
|
||||
): GroupNodeDef | ComfyNodeDef | null | undefined {
|
||||
if (node.type) {
|
||||
const def = globalDefs[node.type]
|
||||
@@ -386,7 +386,8 @@ export class GroupNodeConfig {
|
||||
let type: string | number | null = linksFrom[0]?.[0]?.[5] ?? null
|
||||
if (type === 'COMBO') {
|
||||
// Use the array items
|
||||
const source = node.outputs?.[0]?.widget?.name
|
||||
const output = node.outputs?.[0] as GroupNodeOutput | undefined
|
||||
const source = output?.widget?.name
|
||||
const nodeIdx = linksFrom[0]?.[0]?.[2]
|
||||
if (source && nodeIdx != null) {
|
||||
const fromTypeName = this.nodeData.nodes[Number(nodeIdx)]?.type
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
|
||||
import {
|
||||
type LGraphNode,
|
||||
type LGraphNodeConstructor,
|
||||
LiteGraph
|
||||
import type {
|
||||
GroupNodeConfigEntry,
|
||||
GroupNodeWorkflowData,
|
||||
LGraphNode,
|
||||
LGraphNodeConstructor
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
import { type ComfyApp, app } from '../../scripts/app'
|
||||
@@ -15,18 +17,20 @@ import './groupNodeManage.css'
|
||||
|
||||
const ORDER: symbol = Symbol()
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function merge(target, source) {
|
||||
if (typeof target === 'object' && typeof source === 'object') {
|
||||
for (const key in source) {
|
||||
const sv = source[key]
|
||||
if (typeof sv === 'object') {
|
||||
let tv = target[key]
|
||||
if (!tv) tv = target[key] = {}
|
||||
merge(tv, source[key])
|
||||
} else {
|
||||
target[key] = sv
|
||||
function merge(
|
||||
target: Record<string, unknown>,
|
||||
source: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
for (const key in source) {
|
||||
const sv = source[key]
|
||||
if (typeof sv === 'object' && sv !== null) {
|
||||
let tv = target[key] as Record<string, unknown> | undefined
|
||||
if (!tv) {
|
||||
tv = target[key] = {}
|
||||
}
|
||||
merge(tv, sv as Record<string, unknown>)
|
||||
} else {
|
||||
target[key] = sv
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +38,7 @@ function merge(target, source) {
|
||||
}
|
||||
|
||||
export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
tabs: Record<
|
||||
tabs!: Record<
|
||||
'Inputs' | 'Outputs' | 'Widgets',
|
||||
{ tab: HTMLAnchorElement; page: HTMLElement }
|
||||
>
|
||||
@@ -52,31 +55,26 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
>
|
||||
>
|
||||
> = {}
|
||||
// @ts-expect-error fixme ts strict error
|
||||
nodeItems: any[]
|
||||
nodeItems!: HTMLLIElement[]
|
||||
app: ComfyApp
|
||||
// @ts-expect-error fixme ts strict error
|
||||
groupNodeType: LGraphNodeConstructor<LGraphNode>
|
||||
groupNodeDef: any
|
||||
groupData: any
|
||||
groupNodeType!: LGraphNodeConstructor<LGraphNode>
|
||||
groupData!: GroupNodeConfig
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
innerNodesList: HTMLUListElement
|
||||
// @ts-expect-error fixme ts strict error
|
||||
widgetsPage: HTMLElement
|
||||
// @ts-expect-error fixme ts strict error
|
||||
inputsPage: HTMLElement
|
||||
// @ts-expect-error fixme ts strict error
|
||||
outputsPage: HTMLElement
|
||||
draggable: any
|
||||
innerNodesList!: HTMLUListElement
|
||||
widgetsPage!: HTMLElement
|
||||
inputsPage!: HTMLElement
|
||||
outputsPage!: HTMLElement
|
||||
draggable: DraggableList | undefined
|
||||
|
||||
get selectedNodeInnerIndex() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
|
||||
get selectedNodeInnerIndex(): number {
|
||||
const index = this.selectedNodeIndex
|
||||
if (index == null) throw new Error('No node selected')
|
||||
const item = this.nodeItems[index]
|
||||
if (!item?.dataset.nodeindex) throw new Error('Invalid node item')
|
||||
return +item.dataset.nodeindex
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
constructor(app) {
|
||||
constructor(app: ComfyApp) {
|
||||
super()
|
||||
this.app = app
|
||||
this.element = $el('dialog.comfy-group-manage', {
|
||||
@@ -84,19 +82,15 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
}) as HTMLDialogElement
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
changeTab(tab) {
|
||||
changeTab(tab: keyof ManageGroupDialog['tabs']): void {
|
||||
this.tabs[this.selectedTab].tab.classList.remove('active')
|
||||
this.tabs[this.selectedTab].page.classList.remove('active')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.tabs[tab].tab.classList.add('active')
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.tabs[tab].page.classList.add('active')
|
||||
this.selectedTab = tab
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
changeNode(index, force?) {
|
||||
changeNode(index: number, force?: boolean): void {
|
||||
if (!force && this.selectedNodeIndex === index) return
|
||||
|
||||
if (this.selectedNodeIndex != null) {
|
||||
@@ -122,43 +116,41 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.groupNodeType = LiteGraph.registered_node_types[
|
||||
`${PREFIX}${SEPARATOR}` + this.selectedGroup
|
||||
] as unknown as LGraphNodeConstructor<LGraphNode>
|
||||
this.groupNodeDef = this.groupNodeType.nodeData
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
|
||||
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)!
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
changeGroup(group, reset = true) {
|
||||
changeGroup(group: string, reset = true): void {
|
||||
this.selectedGroup = group
|
||||
this.getGroupData()
|
||||
|
||||
const nodes = this.groupData.nodeData.nodes
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.nodeItems = nodes.map((n, i) =>
|
||||
$el(
|
||||
'li.draggable-item',
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + ''
|
||||
},
|
||||
onclick: () => {
|
||||
this.changeNode(i)
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el(
|
||||
'div',
|
||||
{
|
||||
textContent: n.title ?? n.type
|
||||
this.nodeItems = nodes.map(
|
||||
(n, i) =>
|
||||
$el(
|
||||
'li.draggable-item',
|
||||
{
|
||||
dataset: {
|
||||
nodeindex: n.index + ''
|
||||
},
|
||||
n.title
|
||||
? $el('span', {
|
||||
textContent: n.type
|
||||
})
|
||||
: []
|
||||
)
|
||||
]
|
||||
)
|
||||
onclick: () => {
|
||||
this.changeNode(i)
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el(
|
||||
'div',
|
||||
{
|
||||
textContent: n.title ?? n.type
|
||||
},
|
||||
n.title
|
||||
? $el('span', {
|
||||
textContent: n.type
|
||||
})
|
||||
: []
|
||||
)
|
||||
]
|
||||
) as HTMLLIElement
|
||||
)
|
||||
|
||||
this.innerNodesList.replaceChildren(...this.nodeItems)
|
||||
@@ -167,47 +159,46 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.selectedNodeIndex = null
|
||||
this.changeNode(0)
|
||||
} else {
|
||||
const items = this.draggable.getAllItems()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
let index = items.findIndex((item) => item.classList.contains('selected'))
|
||||
if (index === -1) index = this.selectedNodeIndex
|
||||
const items = this.draggable!.getAllItems()
|
||||
let index = items.findIndex((item: Element) =>
|
||||
item.classList.contains('selected')
|
||||
)
|
||||
if (index === -1) index = this.selectedNodeIndex!
|
||||
this.changeNode(index, true)
|
||||
}
|
||||
|
||||
const ordered = [...nodes]
|
||||
this.draggable?.dispose()
|
||||
this.draggable = new DraggableList(this.innerNodesList, 'li')
|
||||
this.draggable.addEventListener(
|
||||
'dragend',
|
||||
// @ts-expect-error fixme ts strict error
|
||||
({ detail: { oldPosition, newPosition } }) => {
|
||||
if (oldPosition === newPosition) return
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({
|
||||
nodeIndex: ordered[i].index,
|
||||
section: ORDER,
|
||||
prop: 'order',
|
||||
value: i
|
||||
})
|
||||
}
|
||||
this.draggable.addEventListener('dragend', (e: Event) => {
|
||||
const { oldPosition, newPosition } = (e as CustomEvent).detail
|
||||
if (oldPosition === newPosition) return
|
||||
ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
|
||||
for (let i = 0; i < ordered.length; i++) {
|
||||
this.storeModification({
|
||||
nodeIndex: ordered[i].index,
|
||||
section: ORDER,
|
||||
prop: 'order',
|
||||
value: i
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
storeModification(props: {
|
||||
nodeIndex?: number
|
||||
section: symbol
|
||||
section: string | symbol
|
||||
prop: string
|
||||
value: any
|
||||
value: unknown
|
||||
}) {
|
||||
const { nodeIndex, section, prop, value } = props
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const groupMod = (this.modifications[this.selectedGroup] ??= {})
|
||||
const nodesMod = (groupMod.nodes ??= {})
|
||||
const groupKey = this.selectedGroup!
|
||||
const groupMod = (this.modifications[groupKey] ??= {})
|
||||
const nodesMod = ((groupMod as Record<string, unknown>).nodes ??=
|
||||
{}) as Record<string, Record<symbol | string, Record<string, unknown>>>
|
||||
const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {})
|
||||
const typeMod = (nodeMod[section] ??= {})
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const objMod = (typeMod[prop] ??= {})
|
||||
Object.assign(objMod, value)
|
||||
} else {
|
||||
@@ -215,35 +206,45 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
getEditElement(section, prop, value, placeholder, checked, checkable = true) {
|
||||
if (value === placeholder) value = ''
|
||||
getEditElement(
|
||||
section: string,
|
||||
prop: string | number,
|
||||
value: unknown,
|
||||
placeholder: string,
|
||||
checked: boolean,
|
||||
checkable = true
|
||||
): HTMLDivElement {
|
||||
let displayValue = value === placeholder ? '' : value
|
||||
|
||||
const mods =
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.modifications[this.selectedGroup]?.nodes?.[
|
||||
this.selectedNodeInnerIndex
|
||||
]?.[section]?.[prop]
|
||||
if (mods) {
|
||||
if (mods.name != null) {
|
||||
value = mods.name
|
||||
const groupKey = this.selectedGroup!
|
||||
const mods = (
|
||||
this.modifications[groupKey] as Record<string, unknown> | undefined
|
||||
)?.nodes as
|
||||
| Record<
|
||||
number,
|
||||
Record<string, Record<string, { name?: string; visible?: boolean }>>
|
||||
>
|
||||
| undefined
|
||||
const modEntry = mods?.[this.selectedNodeInnerIndex]?.[section]?.[prop]
|
||||
if (modEntry) {
|
||||
if (modEntry.name != null) {
|
||||
displayValue = modEntry.name
|
||||
}
|
||||
if (mods.visible != null) {
|
||||
checked = mods.visible
|
||||
if (modEntry.visible != null) {
|
||||
checked = modEntry.visible
|
||||
}
|
||||
}
|
||||
|
||||
return $el('div', [
|
||||
$el('input', {
|
||||
value,
|
||||
value: displayValue as string,
|
||||
placeholder,
|
||||
type: 'text',
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onchange: (e) => {
|
||||
onchange: (e: Event) => {
|
||||
this.storeModification({
|
||||
section,
|
||||
prop,
|
||||
value: { name: e.target.value }
|
||||
prop: String(prop),
|
||||
value: { name: (e.target as HTMLInputElement).value }
|
||||
})
|
||||
}
|
||||
}),
|
||||
@@ -252,25 +253,23 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
type: 'checkbox',
|
||||
checked,
|
||||
disabled: !checkable,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onchange: (e) => {
|
||||
onchange: (e: Event) => {
|
||||
this.storeModification({
|
||||
section,
|
||||
prop,
|
||||
value: { visible: !!e.target.checked }
|
||||
prop: String(prop),
|
||||
value: { visible: !!(e.target as HTMLInputElement).checked }
|
||||
})
|
||||
}
|
||||
})
|
||||
])
|
||||
])
|
||||
]) as HTMLDivElement
|
||||
}
|
||||
|
||||
buildWidgetsPage() {
|
||||
const widgets =
|
||||
this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
|
||||
const items = Object.keys(widgets ?? {})
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input
|
||||
this.widgetsPage.replaceChildren(
|
||||
...items.map((oldName) => {
|
||||
@@ -289,28 +288,25 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
buildInputsPage() {
|
||||
const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]
|
||||
const items = Object.keys(inputs ?? {})
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.input
|
||||
this.inputsPage.replaceChildren(
|
||||
// @ts-expect-error fixme ts strict error
|
||||
...items
|
||||
.map((oldName) => {
|
||||
let value = inputs[oldName]
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
const elements = items
|
||||
.map((oldName) => {
|
||||
const value = inputs[oldName]
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.getEditElement(
|
||||
'input',
|
||||
oldName,
|
||||
value,
|
||||
oldName,
|
||||
config?.[oldName]?.visible !== false
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
return this.getEditElement(
|
||||
'input',
|
||||
oldName,
|
||||
value,
|
||||
oldName,
|
||||
config?.[oldName]?.visible !== false
|
||||
)
|
||||
})
|
||||
.filter((el): el is HTMLDivElement => el !== null)
|
||||
this.inputsPage.replaceChildren(...elements)
|
||||
return !!items.length
|
||||
}
|
||||
|
||||
@@ -323,38 +319,35 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
const groupOutputs =
|
||||
this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
const type = app.rootGraph.extra.groupNodes![this.selectedGroup!]!
|
||||
const config = type.config?.[this.selectedNodeInnerIndex]?.output
|
||||
const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]
|
||||
const checkable = node.type !== 'PrimitiveNode'
|
||||
this.outputsPage.replaceChildren(
|
||||
...outputs
|
||||
// @ts-expect-error fixme ts strict error
|
||||
.map((type, slot) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot]
|
||||
const oldName = innerNodeDef.output_name?.[slot] ?? type
|
||||
let value = config?.[slot]?.name
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null
|
||||
if (!value || value === oldName) {
|
||||
value = ''
|
||||
}
|
||||
return this.getEditElement(
|
||||
'output',
|
||||
slot,
|
||||
value,
|
||||
oldName,
|
||||
visible,
|
||||
checkable
|
||||
)
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
const elements = outputs.map((outputType: unknown, slot: number) => {
|
||||
const groupOutputIndex = groupOutputs?.[slot]
|
||||
const oldName = innerNodeDef?.output_name?.[slot] ?? String(outputType)
|
||||
let value = config?.[slot]?.name
|
||||
const visible = config?.[slot]?.visible || groupOutputIndex != null
|
||||
if (!value || value === oldName) {
|
||||
value = ''
|
||||
}
|
||||
return this.getEditElement(
|
||||
'output',
|
||||
slot,
|
||||
value,
|
||||
oldName,
|
||||
visible,
|
||||
checkable
|
||||
)
|
||||
})
|
||||
this.outputsPage.replaceChildren(...elements)
|
||||
return !!outputs.length
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
show(type?) {
|
||||
override show(groupNodeType?: string | HTMLElement | HTMLElement[]): void {
|
||||
// Extract string type - this method repurposes the show signature
|
||||
const nodeType =
|
||||
typeof groupNodeType === 'string' ? groupNodeType : undefined
|
||||
const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort(
|
||||
(a, b) => a.localeCompare(b)
|
||||
)
|
||||
@@ -371,24 +364,27 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
this.outputsPage
|
||||
])
|
||||
|
||||
this.tabs = [
|
||||
type TabName = 'Inputs' | 'Widgets' | 'Outputs'
|
||||
const tabEntries: [TabName, HTMLElement][] = [
|
||||
['Inputs', this.inputsPage],
|
||||
['Widgets', this.widgetsPage],
|
||||
['Outputs', this.outputsPage]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
].reduce((p, [name, page]: [string, HTMLElement]) => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
p[name] = {
|
||||
tab: $el('a', {
|
||||
onclick: () => {
|
||||
this.changeTab(name)
|
||||
},
|
||||
textContent: name
|
||||
}),
|
||||
page
|
||||
}
|
||||
return p
|
||||
}, {}) as any
|
||||
]
|
||||
this.tabs = tabEntries.reduce(
|
||||
(p, [name, page]) => {
|
||||
p[name] = {
|
||||
tab: $el('a', {
|
||||
onclick: () => {
|
||||
this.changeTab(name)
|
||||
},
|
||||
textContent: name
|
||||
}) as HTMLAnchorElement,
|
||||
page
|
||||
}
|
||||
return p
|
||||
},
|
||||
{} as ManageGroupDialog['tabs']
|
||||
)
|
||||
|
||||
const outer = $el('div.comfy-group-manage-outer', [
|
||||
$el('header', [
|
||||
@@ -396,15 +392,14 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
$el(
|
||||
'select',
|
||||
{
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onchange: (e) => {
|
||||
this.changeGroup(e.target.value)
|
||||
onchange: (e: Event) => {
|
||||
this.changeGroup((e.target as HTMLSelectElement).value)
|
||||
}
|
||||
},
|
||||
groupNodes.map((g) =>
|
||||
$el('option', {
|
||||
textContent: g,
|
||||
selected: `${PREFIX}${SEPARATOR}${g}` === type,
|
||||
selected: `${PREFIX}${SEPARATOR}${g}` === nodeType,
|
||||
value: g
|
||||
})
|
||||
)
|
||||
@@ -439,8 +434,7 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
`Are you sure you want to remove the node: "${this.selectedGroup}"`
|
||||
)
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
delete app.rootGraph.extra.groupNodes[this.selectedGroup]
|
||||
delete app.rootGraph.extra.groupNodes![this.selectedGroup!]
|
||||
LiteGraph.unregisterNodeType(
|
||||
`${PREFIX}${SEPARATOR}` + this.selectedGroup
|
||||
)
|
||||
@@ -454,97 +448,106 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
'button.comfy-btn',
|
||||
{
|
||||
onclick: async () => {
|
||||
let nodesByType
|
||||
let recreateNodes = []
|
||||
const types = {}
|
||||
type NodesByType = Record<string, LGraphNode[]>
|
||||
let nodesByType: NodesByType | undefined
|
||||
const recreateNodes: LGraphNode[] = []
|
||||
const types: Record<string, GroupNodeWorkflowData> = {}
|
||||
for (const g in this.modifications) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const type = app.rootGraph.extra.groupNodes[g]
|
||||
let config = (type.config ??= {})
|
||||
const groupNodeData = app.rootGraph.extra.groupNodes![g]!
|
||||
let config = (groupNodeData.config ??= {})
|
||||
|
||||
let nodeMods = this.modifications[g]?.nodes
|
||||
type NodeMods = Record<
|
||||
string,
|
||||
Record<symbol | string, Record<string, unknown>>
|
||||
>
|
||||
let nodeMods = this.modifications[g]?.nodes as
|
||||
| NodeMods
|
||||
| undefined
|
||||
if (nodeMods) {
|
||||
const keys = Object.keys(nodeMods)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (nodeMods[keys[0]][ORDER]) {
|
||||
if (nodeMods[keys[0]]?.[ORDER]) {
|
||||
// If any node is reordered, they will all need sequencing
|
||||
const orderedNodes = []
|
||||
const orderedMods = {}
|
||||
const orderedConfig = {}
|
||||
const orderedNodes: GroupNodeWorkflowData['nodes'] = []
|
||||
const orderedMods: NodeMods = {}
|
||||
const orderedConfig: Record<number, GroupNodeConfigEntry> =
|
||||
{}
|
||||
|
||||
for (const n of keys) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const order = nodeMods[n][ORDER].order
|
||||
orderedNodes[order] = type.nodes[+n]
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const order = (nodeMods[n][ORDER] as { order: number })
|
||||
.order
|
||||
orderedNodes[order] = groupNodeData.nodes[+n]
|
||||
orderedMods[order] = nodeMods[n]
|
||||
orderedNodes[order].index = order
|
||||
}
|
||||
|
||||
// Rewrite links
|
||||
for (const l of type.links) {
|
||||
// @ts-expect-error l[0]/l[2] used as node index
|
||||
if (l[0] != null) l[0] = type.nodes[l[0]].index
|
||||
// @ts-expect-error l[0]/l[2] used as node index
|
||||
if (l[2] != null) l[2] = type.nodes[l[2]].index
|
||||
const nodesLen = groupNodeData.nodes.length
|
||||
for (const l of groupNodeData.links) {
|
||||
const srcIdx = l[0] as number
|
||||
const dstIdx = l[2] as number
|
||||
if (srcIdx != null && srcIdx < nodesLen)
|
||||
l[0] = groupNodeData.nodes[srcIdx].index!
|
||||
if (dstIdx != null && dstIdx < nodesLen)
|
||||
l[2] = groupNodeData.nodes[dstIdx].index!
|
||||
}
|
||||
|
||||
// Rewrite externals
|
||||
if (type.external) {
|
||||
for (const ext of type.external) {
|
||||
if (ext[0] != null) {
|
||||
// @ts-expect-error ext[0] used as node index
|
||||
ext[0] = type.nodes[ext[0]].index
|
||||
if (groupNodeData.external) {
|
||||
for (const ext of groupNodeData.external) {
|
||||
const extIdx = ext[0] as number
|
||||
if (extIdx != null && extIdx < nodesLen) {
|
||||
ext[0] = groupNodeData.nodes[extIdx].index!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite modifications
|
||||
for (const id of keys) {
|
||||
// @ts-expect-error id used as node index
|
||||
if (config[id]) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
orderedConfig[type.nodes[id].index] = config[id]
|
||||
if (config[+id]) {
|
||||
orderedConfig[groupNodeData.nodes[+id].index!] =
|
||||
config[+id]
|
||||
}
|
||||
// @ts-expect-error id used as config key
|
||||
delete config[id]
|
||||
delete config[+id]
|
||||
}
|
||||
|
||||
type.nodes = orderedNodes
|
||||
groupNodeData.nodes = orderedNodes
|
||||
nodeMods = orderedMods
|
||||
type.config = config = orderedConfig
|
||||
groupNodeData.config = config = orderedConfig
|
||||
}
|
||||
|
||||
merge(config, nodeMods)
|
||||
merge(
|
||||
config as Record<string, unknown>,
|
||||
nodeMods as Record<string, unknown>
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
types[g] = type
|
||||
types[g] = groupNodeData
|
||||
|
||||
if (!nodesByType) {
|
||||
nodesByType = app.rootGraph.nodes.reduce((p, n) => {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
p[n.type] ??= []
|
||||
// @ts-expect-error fixme ts strict error
|
||||
p[n.type].push(n)
|
||||
return p
|
||||
}, {})
|
||||
nodesByType = app.rootGraph.nodes.reduce<NodesByType>(
|
||||
(p, n) => {
|
||||
const nodeType = n.type ?? ''
|
||||
p[nodeType] ??= []
|
||||
p[nodeType].push(n)
|
||||
return p
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
|
||||
if (nodes) recreateNodes.push(...nodes)
|
||||
const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
|
||||
if (groupTypeNodes) recreateNodes.push(...groupTypeNodes)
|
||||
}
|
||||
|
||||
await GroupNodeConfig.registerFromWorkflow(types, [])
|
||||
|
||||
for (const node of recreateNodes) {
|
||||
node.recreate()
|
||||
node.recreate?.()
|
||||
}
|
||||
|
||||
this.modifications = {}
|
||||
this.app.canvas.setDirty(true, true)
|
||||
this.changeGroup(this.selectedGroup, false)
|
||||
this.changeGroup(this.selectedGroup!, false)
|
||||
}
|
||||
},
|
||||
'Save'
|
||||
@@ -559,8 +562,8 @@ export class ManageGroupDialog extends ComfyDialog<HTMLDialogElement> {
|
||||
|
||||
this.element.replaceChildren(outer)
|
||||
this.changeGroup(
|
||||
type
|
||||
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === type) ??
|
||||
nodeType
|
||||
? (groupNodes.find((g) => `${PREFIX}${SEPARATOR}${g}` === nodeType) ??
|
||||
groupNodes[0])
|
||||
: groupNodes[0]
|
||||
)
|
||||
|
||||
12
src/extensions/core/imageCrop.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.ImageCrop',
|
||||
|
||||
async nodeCreated(node) {
|
||||
if (node.constructor.comfyClass !== 'ImageCrop') return
|
||||
|
||||
const [oldWidth, oldHeight] = node.size
|
||||
node.setSize([Math.max(oldWidth, 300), Math.max(oldHeight, 450)])
|
||||
}
|
||||
})
|
||||
@@ -10,6 +10,7 @@ import './groupNode'
|
||||
import './groupNodeManage'
|
||||
import './groupOptions'
|
||||
import './imageCompare'
|
||||
import './imageCrop'
|
||||
import './load3d'
|
||||
import './maskeditor'
|
||||
import './nodeTemplates'
|
||||
|
||||
@@ -754,6 +754,60 @@ class Load3d {
|
||||
this.forceRender()
|
||||
}
|
||||
|
||||
public async captureThumbnail(
|
||||
width: number = 256,
|
||||
height: number = 256
|
||||
): Promise<string> {
|
||||
if (!this.modelManager.currentModel) {
|
||||
throw new Error('No model loaded for thumbnail capture')
|
||||
}
|
||||
|
||||
const savedState = this.cameraManager.getCameraState()
|
||||
const savedCameraType = this.cameraManager.getCurrentCameraType()
|
||||
const savedGridVisible = this.sceneManager.gridHelper.visible
|
||||
|
||||
try {
|
||||
this.sceneManager.gridHelper.visible = false
|
||||
|
||||
if (savedCameraType !== 'perspective') {
|
||||
this.cameraManager.toggleCamera('perspective')
|
||||
}
|
||||
|
||||
const box = new THREE.Box3().setFromObject(this.modelManager.currentModel)
|
||||
const size = box.getSize(new THREE.Vector3())
|
||||
const center = box.getCenter(new THREE.Vector3())
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const distance = maxDim * 1.5
|
||||
|
||||
const cameraPosition = new THREE.Vector3(
|
||||
center.x - distance * 0.8,
|
||||
center.y + distance * 0.4,
|
||||
center.z + distance * 0.3
|
||||
)
|
||||
|
||||
this.cameraManager.perspectiveCamera.position.copy(cameraPosition)
|
||||
this.cameraManager.perspectiveCamera.lookAt(center)
|
||||
this.cameraManager.perspectiveCamera.updateProjectionMatrix()
|
||||
|
||||
if (this.controlsManager.controls) {
|
||||
this.controlsManager.controls.target.copy(center)
|
||||
this.controlsManager.controls.update()
|
||||
}
|
||||
|
||||
const result = await this.sceneManager.captureScene(width, height)
|
||||
return result.scene
|
||||
} finally {
|
||||
this.sceneManager.gridHelper.visible = savedGridVisible
|
||||
|
||||
if (savedCameraType !== 'perspective') {
|
||||
this.cameraManager.toggleCamera(savedCameraType)
|
||||
}
|
||||
this.cameraManager.setCameraState(savedState)
|
||||
this.controlsManager.controls?.update()
|
||||
}
|
||||
}
|
||||
|
||||
public remove(): void {
|
||||
if (this.contextMenuAbortController) {
|
||||
this.contextMenuAbortController.abort()
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import { t } from '@/i18n'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
class Load3dUtils {
|
||||
static async generateThumbnailIfNeeded(
|
||||
load3d: Load3d,
|
||||
modelPath: string,
|
||||
folderType: 'input' | 'output'
|
||||
): Promise<void> {
|
||||
const [subfolder, filename] = this.splitFilePath(modelPath)
|
||||
const thumbnailFilename = this.getThumbnailFilename(filename)
|
||||
|
||||
const exists = await this.fileExists(
|
||||
subfolder,
|
||||
thumbnailFilename,
|
||||
folderType
|
||||
)
|
||||
if (exists) return
|
||||
|
||||
const imageData = await load3d.captureThumbnail(256, 256)
|
||||
await this.uploadThumbnail(
|
||||
imageData,
|
||||
subfolder,
|
||||
thumbnailFilename,
|
||||
folderType
|
||||
)
|
||||
}
|
||||
|
||||
static async uploadTempImage(
|
||||
imageData: string,
|
||||
prefix: string,
|
||||
@@ -122,6 +147,46 @@ class Load3dUtils {
|
||||
|
||||
await Promise.all(uploadPromises)
|
||||
}
|
||||
|
||||
static getThumbnailFilename(modelFilename: string): string {
|
||||
return `${modelFilename}.png`
|
||||
}
|
||||
|
||||
static async fileExists(
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: string = 'input'
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const url = api.apiURL(this.getResourceURL(subfolder, filename, type))
|
||||
const response = await fetch(url, { method: 'HEAD' })
|
||||
return response.ok
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static async uploadThumbnail(
|
||||
imageData: string,
|
||||
subfolder: string,
|
||||
filename: string,
|
||||
type: string = 'input'
|
||||
): Promise<boolean> {
|
||||
const blob = await fetch(imageData).then((r) => r.blob())
|
||||
const file = new File([blob], filename, { type: 'image/png' })
|
||||
|
||||
const body = new FormData()
|
||||
body.append('image', file)
|
||||
body.append('subfolder', subfolder)
|
||||
body.append('type', type)
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
return resp.status === 200
|
||||
}
|
||||
}
|
||||
|
||||
export default Load3dUtils
|
||||
|
||||
@@ -75,7 +75,9 @@ useExtensionService().registerExtension({
|
||||
|
||||
for (const previewWidget of previewWidgets) {
|
||||
const text = message.text ?? ''
|
||||
previewWidget.value = Array.isArray(text) ? (text[0] ?? '') : text
|
||||
previewWidget.value = Array.isArray(text)
|
||||
? (text?.join('\n\n') ?? '')
|
||||
: text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeOutputWith, ResultItem } from '@/schemas/apiSchema'
|
||||
@@ -94,6 +95,17 @@ useExtensionService().registerExtension({
|
||||
const config = new Load3DConfiguration(load3d, node.properties)
|
||||
|
||||
const loadFolder = fileInfo.type as 'input' | 'output'
|
||||
|
||||
const onModelLoaded = () => {
|
||||
load3d.removeEventListener('modelLoadingEnd', onModelLoaded)
|
||||
void Load3dUtils.generateThumbnailIfNeeded(
|
||||
load3d,
|
||||
filePath,
|
||||
loadFolder
|
||||
)
|
||||
}
|
||||
load3d.addEventListener('modelLoadingEnd', onModelLoaded)
|
||||
|
||||
config.configureForSaveMesh(loadFolder, filePath)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -102,16 +102,23 @@ export interface LGraphConfig {
|
||||
links_ontop?: boolean
|
||||
}
|
||||
|
||||
export interface GroupNodeConfigEntry {
|
||||
input?: Record<string, { name?: string; visible?: boolean }>
|
||||
output?: Record<number, { name?: string; visible?: boolean }>
|
||||
}
|
||||
|
||||
export interface GroupNodeWorkflowData {
|
||||
external: (number | string)[][]
|
||||
links: SerialisedLLinkArray[]
|
||||
nodes: {
|
||||
index?: number
|
||||
type?: string
|
||||
title?: string
|
||||
inputs?: unknown[]
|
||||
outputs?: unknown[]
|
||||
widgets_values?: unknown[]
|
||||
}[]
|
||||
config?: Record<number, unknown>
|
||||
config?: Record<number, GroupNodeConfigEntry>
|
||||
}
|
||||
|
||||
export interface LGraphExtra extends Dictionary<unknown> {
|
||||
@@ -1571,8 +1578,21 @@ export class LGraph
|
||||
|
||||
// Inputs, outputs, and links
|
||||
const links = internalLinks.map((x) => x.asSerialisable())
|
||||
const inputs = mapSubgraphInputsAndLinks(resolvedInputLinks, links)
|
||||
const outputs = mapSubgraphOutputsAndLinks(resolvedOutputLinks, links)
|
||||
|
||||
const internalReroutes = new Map([...reroutes].map((r) => [r.id, r]))
|
||||
const externalReroutes = new Map(
|
||||
[...this.reroutes].filter(([id]) => !internalReroutes.has(id))
|
||||
)
|
||||
const inputs = mapSubgraphInputsAndLinks(
|
||||
resolvedInputLinks,
|
||||
links,
|
||||
internalReroutes
|
||||
)
|
||||
const outputs = mapSubgraphOutputsAndLinks(
|
||||
resolvedOutputLinks,
|
||||
links,
|
||||
externalReroutes
|
||||
)
|
||||
|
||||
// Prepare subgraph data
|
||||
const data = {
|
||||
@@ -1714,10 +1734,10 @@ export class LGraph
|
||||
// Reconnect output links in parent graph
|
||||
i = 0
|
||||
for (const [, connections] of outputsGroupedByOutput.entries()) {
|
||||
// Special handling: Subgraph output node
|
||||
i++
|
||||
for (const connection of connections) {
|
||||
const { input, inputNode, link, subgraphOutput } = connection
|
||||
// Special handling: Subgraph output node
|
||||
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
|
||||
link.origin_id = subgraphNode.id
|
||||
link.origin_slot = i - 1
|
||||
@@ -2013,33 +2033,50 @@ export class LGraph
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (!instance) {
|
||||
console.error('Broken Id link when unpacking')
|
||||
break
|
||||
}
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
parentId = instance.parentId
|
||||
}
|
||||
}
|
||||
if (!instance) continue
|
||||
parentId = newLink.iparent
|
||||
while (parentId) {
|
||||
const migratedId = rerouteIdMap.get(parentId)
|
||||
if (!migratedId) throw new Error('Broken Id link when unpacking')
|
||||
if (!migratedId) {
|
||||
console.error('Broken Id link when unpacking')
|
||||
break
|
||||
}
|
||||
instance.parentId = migratedId
|
||||
instance = this.reroutes.get(migratedId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (!instance) {
|
||||
console.error('Broken Id link when unpacking')
|
||||
break
|
||||
}
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
const oldReroute = subgraphNode.subgraph.reroutes.get(parentId)
|
||||
if (!oldReroute) throw new Error('Broken Id link when unpacking')
|
||||
if (!oldReroute) {
|
||||
console.error('Broken Id link when unpacking')
|
||||
break
|
||||
}
|
||||
parentId = oldReroute.parentId
|
||||
}
|
||||
if (!instance) break
|
||||
if (!newLink.externalFirst) {
|
||||
parentId = newLink.eparent
|
||||
while (parentId) {
|
||||
instance.parentId = parentId
|
||||
instance = this.reroutes.get(parentId)
|
||||
if (!instance) throw new Error('Broken Id link when unpacking')
|
||||
if (!instance) {
|
||||
console.error('Broken Id link when unpacking')
|
||||
break
|
||||
}
|
||||
if (instance.linkIds.has(linkInstance.id))
|
||||
throw new Error('Infinite parentId loop')
|
||||
instance.linkIds.add(linkInstance.id)
|
||||
@@ -2545,6 +2582,7 @@ export class Subgraph
|
||||
|
||||
this.inputNode.configure(data.inputNode)
|
||||
this.outputNode.configure(data.outputNode)
|
||||
for (const node of this.nodes) node.updateComputedDisabled()
|
||||
}
|
||||
|
||||
override configure(
|
||||
|
||||
@@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
})
|
||||
|
||||
function inner_clicked(
|
||||
this: ContextMenu<string>,
|
||||
this: ContextMenuDivElement,
|
||||
v?: string | IContextMenuValue<string>
|
||||
) {
|
||||
if (!node || typeof v === 'string' || !v?.value) return
|
||||
|
||||
const rect = this.root.getBoundingClientRect()
|
||||
const rect = this.getBoundingClientRect()
|
||||
canvas.showEditPropertyValue(node, v.value, {
|
||||
position: [rect.left, rect.top]
|
||||
})
|
||||
@@ -3187,7 +3187,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
// get node over
|
||||
const node = graph.getNodeOnPos(x, y, this.visible_nodes)
|
||||
const node = LiteGraph.vueNodesMode
|
||||
? null
|
||||
: graph.getNodeOnPos(x, y, this.visible_nodes)
|
||||
|
||||
const dragRect = this.dragging_rectangle
|
||||
if (dragRect) {
|
||||
|
||||
@@ -104,6 +104,8 @@ export type {
|
||||
} from './interfaces'
|
||||
export {
|
||||
LGraph,
|
||||
type GroupNodeConfigEntry,
|
||||
type GroupNodeWorkflowData,
|
||||
type LGraphTriggerAction,
|
||||
type LGraphTriggerParam
|
||||
} from './LGraph'
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { ResolvedConnection } from '@/lib/litegraph/src/LLink'
|
||||
import { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
@@ -259,10 +260,29 @@ export function groupResolvedByOutput(
|
||||
|
||||
return groupedByOutput
|
||||
}
|
||||
function mapReroutes(
|
||||
link: SerialisableLLink,
|
||||
reroutes: Map<RerouteId, Reroute>
|
||||
) {
|
||||
let child: SerialisableLLink | Reroute = link
|
||||
let nextReroute =
|
||||
child.parentId === undefined ? undefined : reroutes.get(child.parentId)
|
||||
|
||||
while (child.parentId !== undefined && nextReroute) {
|
||||
child = nextReroute
|
||||
nextReroute =
|
||||
child.parentId === undefined ? undefined : reroutes.get(child.parentId)
|
||||
}
|
||||
|
||||
const lastId = child.parentId
|
||||
child.parentId = undefined
|
||||
return lastId
|
||||
}
|
||||
|
||||
export function mapSubgraphInputsAndLinks(
|
||||
resolvedInputLinks: ResolvedConnection[],
|
||||
links: SerialisableLLink[]
|
||||
links: SerialisableLLink[],
|
||||
reroutes: Map<RerouteId, Reroute>
|
||||
): SubgraphIO[] {
|
||||
// Group matching links
|
||||
const groupedByOutput = groupResolvedByOutput(resolvedInputLinks)
|
||||
@@ -279,8 +299,10 @@ export function mapSubgraphInputsAndLinks(
|
||||
if (!input) continue
|
||||
|
||||
const linkData = link.asSerialisable()
|
||||
link.parentId = mapReroutes(link, reroutes)
|
||||
linkData.origin_id = SUBGRAPH_INPUT_ID
|
||||
linkData.origin_slot = inputs.length
|
||||
|
||||
links.push(linkData)
|
||||
inputLinks.push(linkData)
|
||||
}
|
||||
@@ -340,7 +362,8 @@ export function mapSubgraphInputsAndLinks(
|
||||
*/
|
||||
export function mapSubgraphOutputsAndLinks(
|
||||
resolvedOutputLinks: ResolvedConnection[],
|
||||
links: SerialisableLLink[]
|
||||
links: SerialisableLLink[],
|
||||
reroutes: Map<RerouteId, Reroute>
|
||||
): SubgraphIO[] {
|
||||
// Group matching links
|
||||
const groupedByOutput = groupResolvedByOutput(resolvedOutputLinks)
|
||||
@@ -355,10 +378,11 @@ export function mapSubgraphOutputsAndLinks(
|
||||
const { link, output } = resolved
|
||||
if (!output) continue
|
||||
|
||||
// Link
|
||||
const linkData = link.asSerialisable()
|
||||
linkData.parentId = mapReroutes(link, reroutes)
|
||||
linkData.target_id = SUBGRAPH_OUTPUT_ID
|
||||
linkData.target_slot = outputs.length
|
||||
|
||||
links.push(linkData)
|
||||
outputLinks.push(linkData)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
|
||||
import type { CanvasColour, Point, RequiredProps, Size } from '../interfaces'
|
||||
import type { CanvasPointer, LGraphCanvas, LGraphNode } from '../litegraph'
|
||||
import type { CanvasPointerEvent } from './events'
|
||||
@@ -88,6 +90,8 @@ export type IWidget =
|
||||
| ISelectButtonWidget
|
||||
| ITextareaWidget
|
||||
| IAssetWidget
|
||||
| IImageCropWidget
|
||||
| IBoundingBoxWidget
|
||||
|
||||
export interface IBooleanWidget extends IBaseWidget<boolean, 'toggle'> {
|
||||
type: 'toggle'
|
||||
@@ -259,6 +263,18 @@ export interface IAssetWidget extends IBaseWidget<
|
||||
value: string
|
||||
}
|
||||
|
||||
/** Image crop widget for cropping image */
|
||||
export interface IImageCropWidget extends IBaseWidget<Bounds, 'imagecrop'> {
|
||||
type: 'imagecrop'
|
||||
value: Bounds
|
||||
}
|
||||
|
||||
/** Bounding box widget for defining regions with numeric inputs */
|
||||
export interface IBoundingBoxWidget extends IBaseWidget<Bounds, 'boundingbox'> {
|
||||
type: 'boundingbox'
|
||||
value: Bounds
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid widget types. TS cannot provide easily extensible type safety for this at present.
|
||||
* Override linkedWidgets[]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '@/i18n'
|
||||
import { drawTextInArea } from '@/lib/litegraph/src/draw'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import type { Point } from '@/lib/litegraph/src/interfaces'
|
||||
@@ -141,6 +142,7 @@ export abstract class BaseWidget<
|
||||
// @ts-expect-error Prevent naming conflicts with custom nodes.
|
||||
labelBaseline,
|
||||
promoted,
|
||||
linkedWidgets,
|
||||
...safeValues
|
||||
} = widget
|
||||
|
||||
@@ -227,6 +229,41 @@ export abstract class BaseWidget<
|
||||
if (showText && !this.computedDisabled) ctx.stroke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a placeholder for widgets that only have a Vue implementation.
|
||||
* @param ctx The canvas context
|
||||
* @param options The options for drawing the widget
|
||||
* @param label The label to display (e.g., "ImageCrop", "BoundingBox")
|
||||
*/
|
||||
protected drawVueOnlyWarning(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width }: DrawWidgetOptions,
|
||||
label: string
|
||||
): void {
|
||||
const { y, height } = this
|
||||
|
||||
ctx.save()
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.outline_color
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
ctx.fillText(
|
||||
`${label}: ${t('widgets.node2only')}`,
|
||||
width / 2,
|
||||
y + height / 2
|
||||
)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared routine for drawing a label and value as text, truncated
|
||||
* if they exceed the available width.
|
||||
|
||||
22
src/lib/litegraph/src/widgets/BoundingBoxWidget.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { IBoundingBoxWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for defining bounding box regions.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class BoundingBoxWidget
|
||||
extends BaseWidget<IBoundingBoxWidget>
|
||||
implements IBoundingBoxWidget
|
||||
{
|
||||
override type = 'boundingbox' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'BoundingBox')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
22
src/lib/litegraph/src/widgets/ImageCropWidget.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { IImageCropWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for displaying an image crop preview.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class ImageCropWidget
|
||||
extends BaseWidget<IImageCropWidget>
|
||||
implements IImageCropWidget
|
||||
{
|
||||
override type = 'imagecrop' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'ImageCrop')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ITextareaWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for multi-line text input
|
||||
* This is a widget that only has a Vue widgets implementation
|
||||
* Widget for multi-line text input.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class TextareaWidget
|
||||
extends BaseWidget<ITextareaWidget>
|
||||
@@ -15,35 +13,10 @@ export class TextareaWidget
|
||||
override type = 'textarea' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.outline_color
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `Textarea: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
this.drawVueOnlyWarning(ctx, options, 'Textarea')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This is a widget that only has a Vue widgets implementation
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ITreeSelectWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for hierarchical tree selection
|
||||
* This is a widget that only has a Vue widgets implementation
|
||||
* Widget for hierarchical tree selection.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class TreeSelectWidget
|
||||
extends BaseWidget<ITreeSelectWidget>
|
||||
@@ -15,35 +13,10 @@ export class TreeSelectWidget
|
||||
override type = 'treeselect' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.outline_color
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `TreeSelect: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
this.drawVueOnlyWarning(ctx, options, 'TreeSelect')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This is a widget that only has a Vue widgets implementation
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { toClass } from '@/lib/litegraph/src/utils/type'
|
||||
import { AssetWidget } from './AssetWidget'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import { BooleanWidget } from './BooleanWidget'
|
||||
import { BoundingBoxWidget } from './BoundingBoxWidget'
|
||||
import { ButtonWidget } from './ButtonWidget'
|
||||
import { ChartWidget } from './ChartWidget'
|
||||
import { ColorWidget } from './ColorWidget'
|
||||
@@ -18,6 +19,7 @@ import { ComboWidget } from './ComboWidget'
|
||||
import { FileUploadWidget } from './FileUploadWidget'
|
||||
import { GalleriaWidget } from './GalleriaWidget'
|
||||
import { ImageCompareWidget } from './ImageCompareWidget'
|
||||
import { ImageCropWidget } from './ImageCropWidget'
|
||||
import { KnobWidget } from './KnobWidget'
|
||||
import { LegacyWidget } from './LegacyWidget'
|
||||
import { MarkdownWidget } from './MarkdownWidget'
|
||||
@@ -50,6 +52,8 @@ export type WidgetTypeMap = {
|
||||
selectbutton: SelectButtonWidget
|
||||
textarea: TextareaWidget
|
||||
asset: AssetWidget
|
||||
imagecrop: ImageCropWidget
|
||||
boundingbox: BoundingBoxWidget
|
||||
[key: string]: BaseWidget
|
||||
}
|
||||
|
||||
@@ -120,6 +124,10 @@ export function toConcreteWidget<TWidget extends IWidget | IBaseWidget>(
|
||||
return toClass(TextareaWidget, narrowedWidget, node)
|
||||
case 'asset':
|
||||
return toClass(AssetWidget, narrowedWidget, node)
|
||||
case 'imagecrop':
|
||||
return toClass(ImageCropWidget, narrowedWidget, node)
|
||||
case 'boundingbox':
|
||||
return toClass(BoundingBoxWidget, narrowedWidget, node)
|
||||
default: {
|
||||
if (wrapLegacyWidgets) return toClass(LegacyWidget, widget, node)
|
||||
}
|
||||
|
||||
@@ -238,6 +238,12 @@
|
||||
"title": "إنشاء حساب"
|
||||
}
|
||||
},
|
||||
"boundingBox": {
|
||||
"height": "الارتفاع",
|
||||
"width": "العرض",
|
||||
"x": "س",
|
||||
"y": "ص"
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "مسح سير العمل",
|
||||
"deleteBlueprint": "حذف المخطط",
|
||||
@@ -811,6 +817,7 @@
|
||||
"nodeSlotsError": "خطأ في فتحات العقدة",
|
||||
"nodeWidgetsError": "خطأ في عناصر واجهة العقدة",
|
||||
"nodes": "العُقَد",
|
||||
"nodesCount": "{count} عقدة | {count} عقدة | {count} عقدة",
|
||||
"nodesRunning": "العُقَد قيد التشغيل",
|
||||
"none": "لا شيء",
|
||||
"nothingToCopy": "لا يوجد ما يمكن نسخه",
|
||||
@@ -982,6 +989,11 @@
|
||||
"imageCompare": {
|
||||
"noImages": "لا توجد صور للمقارنة"
|
||||
},
|
||||
"imageCrop": {
|
||||
"cropPreviewAlt": "معاينة الاقتصاص",
|
||||
"loading": "جارٍ التحميل...",
|
||||
"noInputImage": "لا توجد صورة إدخال متصلة"
|
||||
},
|
||||
"importFailed": {
|
||||
"copyError": "خطأ في النسخ",
|
||||
"title": "فشل الاستيراد"
|
||||
@@ -1611,6 +1623,7 @@
|
||||
"3d": "ثلاثي الأبعاد",
|
||||
"3d_models": "نماذج ثلاثية الأبعاد",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "بايت دانس",
|
||||
"Gemini": "جيميني",
|
||||
"Ideogram": "إيديوغرام",
|
||||
@@ -1632,6 +1645,7 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "فيدو",
|
||||
"Wan": "وان",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_للاختبار",
|
||||
"advanced": "متقدم",
|
||||
"animation": "الرسوم المتحركة",
|
||||
|
||||
@@ -328,6 +328,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaImageEditNode": {
|
||||
"description": "حرر الصور باستخدام أحدث نموذج من Bria",
|
||||
"display_name": "تحرير صورة Bria",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "التحكم بعد التوليد"
|
||||
},
|
||||
"guidance_scale": {
|
||||
"name": "مقياس التوجيه",
|
||||
"tooltip": "القيمة الأعلى تجعل الصورة تتبع التوجيه بشكل أدق."
|
||||
},
|
||||
"image": {
|
||||
"name": "الصورة"
|
||||
},
|
||||
"mask": {
|
||||
"name": "القناع",
|
||||
"tooltip": "إذا لم يتم تحديده، سيتم تطبيق التحرير على الصورة بالكامل."
|
||||
},
|
||||
"model": {
|
||||
"name": "النموذج"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "الإشراف",
|
||||
"tooltip": "إعدادات الإشراف"
|
||||
},
|
||||
"moderation_prompt_content_moderation": {
|
||||
"name": "إشراف محتوى التوجيه"
|
||||
},
|
||||
"moderation_visual_input_moderation": {
|
||||
"name": "إشراف الإدخال البصري"
|
||||
},
|
||||
"moderation_visual_output_moderation": {
|
||||
"name": "إشراف الإخراج البصري"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "توجيه سلبي"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "التوجيه",
|
||||
"tooltip": "تعليمات لتحرير الصورة"
|
||||
},
|
||||
"seed": {
|
||||
"name": "البذرة"
|
||||
},
|
||||
"steps": {
|
||||
"name": "الخطوات"
|
||||
},
|
||||
"structured_prompt": {
|
||||
"name": "توجيه منظم",
|
||||
"tooltip": "سلسلة نصية تحتوي على توجيه التحرير المنظم بصيغة JSON. استخدم هذا بدلاً من التوجيه المعتاد للتحكم الدقيق والبرمجي."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "توجيه منظم",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDanceFirstLastFrameNode": {
|
||||
"description": "إنشاء فيديو باستخدام المطالبة النصية والإطار الأول والأخير.",
|
||||
"display_name": "تحويل الإطار الأول-الأخير من ByteDance إلى فيديو",
|
||||
@@ -13450,6 +13512,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextEncodeZImageOmni": {
|
||||
"display_name": "TextEncodeZImageOmni",
|
||||
"inputs": {
|
||||
"auto_resize_images": {
|
||||
"name": "تغيير حجم الصور تلقائياً"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
"image1": {
|
||||
"name": "الصورة ١"
|
||||
},
|
||||
"image2": {
|
||||
"name": "الصورة ٢"
|
||||
},
|
||||
"image3": {
|
||||
"name": "الصورة ٣"
|
||||
},
|
||||
"image_encoder": {
|
||||
"name": "مُرمّز الصورة"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "التوجيه"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "تحويل النص إلى أحرف صغيرة",
|
||||
"inputs": {
|
||||
@@ -16137,6 +16233,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedFlashVSRNode": {
|
||||
"description": "مُرقّي فيديو سريع وعالي الجودة يعزز الدقة ويعيد الوضوح للمقاطع منخفضة الدقة أو الضبابية.",
|
||||
"display_name": "ترقية فيديو FlashVSR",
|
||||
"inputs": {
|
||||
"target_resolution": {
|
||||
"name": "الدقة المستهدفة"
|
||||
},
|
||||
"video": {
|
||||
"name": "الفيديو"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedImageUpscaleNode": {
|
||||
"description": "عزز دقة وجودة الصورة، وارفع الصور إلى دقة 4K أو 8K للحصول على نتائج حادة ومفصلة.",
|
||||
"display_name": "ترقية صورة WaveSpeed",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "الصورة"
|
||||
},
|
||||
"model": {
|
||||
"name": "النموذج"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "الدقة المستهدفة"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebcamCapture": {
|
||||
"display_name": "التقاط كاميرا ويب",
|
||||
"inputs": {
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"nodesCount": "{count} nodes | {count} node | {count} nodes",
|
||||
"community": "Community",
|
||||
"all": "All",
|
||||
"versionMismatchWarning": "Version Compatibility Warning",
|
||||
@@ -702,6 +703,7 @@
|
||||
"noImportedFiles": "No imported files found",
|
||||
"noGeneratedFiles": "No generated files found",
|
||||
"generatedAssetsHeader": "Generated assets",
|
||||
"importedAssetsHeader": "Imported assets",
|
||||
"noFilesFoundMessage": "Upload files or generate content to see them here",
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
@@ -1426,6 +1428,7 @@
|
||||
"latent": "latent",
|
||||
"mask": "mask",
|
||||
"api node": "api node",
|
||||
"Bria": "Bria",
|
||||
"video": "video",
|
||||
"ByteDance": "ByteDance",
|
||||
"preprocessors": "preprocessors",
|
||||
@@ -1506,6 +1509,7 @@
|
||||
"": "",
|
||||
"camera": "camera",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"zimage": "zimage"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -1718,6 +1722,17 @@
|
||||
"unsupportedFileType": "Unsupported file type (supports .gltf, .glb, .obj, .fbx, .stl)",
|
||||
"uploadingModel": "Uploading 3D model..."
|
||||
},
|
||||
"imageCrop": {
|
||||
"loading": "Loading...",
|
||||
"noInputImage": "No input image connected",
|
||||
"cropPreviewAlt": "Crop preview"
|
||||
},
|
||||
"boundingBox": {
|
||||
"x": "X",
|
||||
"y": "Y",
|
||||
"width": "Width",
|
||||
"height": "Height"
|
||||
},
|
||||
"toastMessages": {
|
||||
"nothingToQueue": "Nothing to queue",
|
||||
"pleaseSelectOutputNodes": "Please select output nodes",
|
||||
|
||||
@@ -328,6 +328,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaImageEditNode": {
|
||||
"display_name": "Bria Image Edit",
|
||||
"description": "Edit images using Bria latest model",
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Instruction to edit image"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "negative_prompt"
|
||||
},
|
||||
"structured_prompt": {
|
||||
"name": "structured_prompt",
|
||||
"tooltip": "A string containing the structured edit prompt in JSON format. Use this instead of usual prompt for precise, programmatic control."
|
||||
},
|
||||
"seed": {
|
||||
"name": "seed"
|
||||
},
|
||||
"guidance_scale": {
|
||||
"name": "guidance_scale",
|
||||
"tooltip": "Higher value makes the image follow the prompt more closely."
|
||||
},
|
||||
"steps": {
|
||||
"name": "steps"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "moderation",
|
||||
"tooltip": "Moderation settings"
|
||||
},
|
||||
"mask": {
|
||||
"name": "mask",
|
||||
"tooltip": "If omitted, the edit applies to the entire image."
|
||||
},
|
||||
"control_after_generate": {
|
||||
"name": "control after generate"
|
||||
},
|
||||
"moderation_prompt_content_moderation": {
|
||||
"name": "prompt_content_moderation"
|
||||
},
|
||||
"moderation_visual_input_moderation": {
|
||||
"name": "visual_input_moderation"
|
||||
},
|
||||
"moderation_visual_output_moderation": {
|
||||
"name": "visual_output_moderation"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "structured_prompt",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDanceFirstLastFrameNode": {
|
||||
"display_name": "ByteDance First-Last-Frame to Video",
|
||||
"description": "Generate video using prompt and first and last frames.",
|
||||
@@ -13469,6 +13531,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextEncodeZImageOmni": {
|
||||
"display_name": "TextEncodeZImageOmni",
|
||||
"inputs": {
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt"
|
||||
},
|
||||
"auto_resize_images": {
|
||||
"name": "auto_resize_images"
|
||||
},
|
||||
"image_encoder": {
|
||||
"name": "image_encoder"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
},
|
||||
"image1": {
|
||||
"name": "image1"
|
||||
},
|
||||
"image2": {
|
||||
"name": "image2"
|
||||
},
|
||||
"image3": {
|
||||
"name": "image3"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "Text to Lowercase",
|
||||
"inputs": {
|
||||
@@ -16199,6 +16295,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedFlashVSRNode": {
|
||||
"display_name": "FlashVSR Video Upscale",
|
||||
"description": "Fast, high-quality video upscaler that boosts resolution and restores clarity for low-resolution or blurry footage.",
|
||||
"inputs": {
|
||||
"video": {
|
||||
"name": "video"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "target_resolution"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedImageUpscaleNode": {
|
||||
"display_name": "WaveSpeed Image Upscale",
|
||||
"description": "Boost image resolution and quality, upscaling photos to 4K or 8K for sharp, detailed results.",
|
||||
"inputs": {
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "target_resolution"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebcamCapture": {
|
||||
"display_name": "Webcam Capture",
|
||||
"inputs": {
|
||||
|
||||
@@ -238,6 +238,12 @@
|
||||
"title": "Crea una cuenta"
|
||||
}
|
||||
},
|
||||
"boundingBox": {
|
||||
"height": "Alto",
|
||||
"width": "Ancho",
|
||||
"x": "X",
|
||||
"y": "Y"
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "Limpiar flujo de trabajo",
|
||||
"deleteBlueprint": "Eliminar Plano",
|
||||
@@ -811,6 +817,7 @@
|
||||
"nodeSlotsError": "Error de Ranuras del Nodo",
|
||||
"nodeWidgetsError": "Error de Widgets del Nodo",
|
||||
"nodes": "Nodos",
|
||||
"nodesCount": "{count} nodos | {count} nodo | {count} nodos",
|
||||
"nodesRunning": "nodos en ejecución",
|
||||
"none": "Ninguno",
|
||||
"nothingToCopy": "Nada para copiar",
|
||||
@@ -982,6 +989,11 @@
|
||||
"imageCompare": {
|
||||
"noImages": "No hay imágenes para comparar"
|
||||
},
|
||||
"imageCrop": {
|
||||
"cropPreviewAlt": "Vista previa del recorte",
|
||||
"loading": "Cargando...",
|
||||
"noInputImage": "No hay imagen de entrada conectada"
|
||||
},
|
||||
"importFailed": {
|
||||
"copyError": "Error al copiar",
|
||||
"title": "Error de importación"
|
||||
@@ -1611,6 +1623,7 @@
|
||||
"3d": "3d",
|
||||
"3d_models": "modelos_3d",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "ByteDance",
|
||||
"Gemini": "Gemini",
|
||||
"Ideogram": "Ideogram",
|
||||
@@ -1632,6 +1645,7 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_para_pruebas",
|
||||
"advanced": "avanzado",
|
||||
"animation": "animación",
|
||||
|
||||
@@ -328,6 +328,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaImageEditNode": {
|
||||
"description": "Edita imágenes usando el modelo más reciente de Bria",
|
||||
"display_name": "Edición de Imagen Bria",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "control después de generar"
|
||||
},
|
||||
"guidance_scale": {
|
||||
"name": "escala_de_guía",
|
||||
"tooltip": "Un valor más alto hace que la imagen siga la instrucción más de cerca."
|
||||
},
|
||||
"image": {
|
||||
"name": "imagen"
|
||||
},
|
||||
"mask": {
|
||||
"name": "máscara",
|
||||
"tooltip": "Si se omite, la edición se aplica a toda la imagen."
|
||||
},
|
||||
"model": {
|
||||
"name": "modelo"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "moderación",
|
||||
"tooltip": "Configuración de moderación"
|
||||
},
|
||||
"moderation_prompt_content_moderation": {
|
||||
"name": "moderación_de_contenido_de_instrucción"
|
||||
},
|
||||
"moderation_visual_input_moderation": {
|
||||
"name": "moderación_visual_de_entrada"
|
||||
},
|
||||
"moderation_visual_output_moderation": {
|
||||
"name": "moderación_visual_de_salida"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "instrucción_negativa"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "instrucción",
|
||||
"tooltip": "Instrucción para editar la imagen"
|
||||
},
|
||||
"seed": {
|
||||
"name": "semilla"
|
||||
},
|
||||
"steps": {
|
||||
"name": "pasos"
|
||||
},
|
||||
"structured_prompt": {
|
||||
"name": "instrucción_estructurada",
|
||||
"tooltip": "Una cadena que contiene la instrucción de edición estructurada en formato JSON. Usa esto en lugar de la instrucción habitual para un control preciso y programático."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "instrucción_estructurada",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDanceFirstLastFrameNode": {
|
||||
"description": "Generar video usando prompt y primer y último fotograma.",
|
||||
"display_name": "ByteDance Primer-Último-Fotograma a Video",
|
||||
@@ -13450,6 +13512,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextEncodeZImageOmni": {
|
||||
"display_name": "TextEncodeZImageOmni",
|
||||
"inputs": {
|
||||
"auto_resize_images": {
|
||||
"name": "auto_redimensionar_imágenes"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
"image1": {
|
||||
"name": "imagen1"
|
||||
},
|
||||
"image2": {
|
||||
"name": "imagen2"
|
||||
},
|
||||
"image3": {
|
||||
"name": "imagen3"
|
||||
},
|
||||
"image_encoder": {
|
||||
"name": "codificador_de_imagen"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "instrucción"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "Convertir texto a minúsculas",
|
||||
"inputs": {
|
||||
@@ -16137,6 +16233,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedFlashVSRNode": {
|
||||
"description": "Escalador de video rápido y de alta calidad que aumenta la resolución y restaura la claridad de videos de baja resolución o borrosos.",
|
||||
"display_name": "FlashVSR Escalado de Video",
|
||||
"inputs": {
|
||||
"target_resolution": {
|
||||
"name": "resolución_objetivo"
|
||||
},
|
||||
"video": {
|
||||
"name": "video"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedImageUpscaleNode": {
|
||||
"description": "Aumenta la resolución y calidad de la imagen, escalando fotos a 4K u 8K para obtener resultados nítidos y detallados.",
|
||||
"display_name": "WaveSpeed Escalado de Imagen",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "imagen"
|
||||
},
|
||||
"model": {
|
||||
"name": "modelo"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "resolución_objetivo"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebcamCapture": {
|
||||
"display_name": "Captura de Webcam",
|
||||
"inputs": {
|
||||
|
||||
@@ -238,6 +238,12 @@
|
||||
"title": "ایجاد حساب کاربری"
|
||||
}
|
||||
},
|
||||
"boundingBox": {
|
||||
"height": "ارتفاع",
|
||||
"width": "عرض",
|
||||
"x": "ایکس",
|
||||
"y": "وای"
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "پاکسازی workflow",
|
||||
"deleteBlueprint": "حذف blueprint",
|
||||
@@ -811,6 +817,7 @@
|
||||
"nodeSlotsError": "خطا در slotهای node",
|
||||
"nodeWidgetsError": "خطا در ابزارکهای node",
|
||||
"nodes": "nodeها",
|
||||
"nodesCount": "{count} نود | {count} نود | {count} نود",
|
||||
"nodesRunning": "nodeها در حال اجرا هستند",
|
||||
"none": "هیچکدام",
|
||||
"nothingToCopy": "موردی برای کپی وجود ندارد",
|
||||
@@ -982,6 +989,11 @@
|
||||
"imageCompare": {
|
||||
"noImages": "تصویری برای مقایسه وجود ندارد"
|
||||
},
|
||||
"imageCrop": {
|
||||
"cropPreviewAlt": "پیشنمایش برش",
|
||||
"loading": "در حال بارگذاری...",
|
||||
"noInputImage": "هیچ تصویر ورودی متصل نیست"
|
||||
},
|
||||
"importFailed": {
|
||||
"copyError": "خطا در کپی",
|
||||
"title": "وارد کردن ناموفق بود"
|
||||
@@ -1611,6 +1623,7 @@
|
||||
"3d": "سهبعدی",
|
||||
"3d_models": "مدلهای سهبعدی",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "ByteDance",
|
||||
"Gemini": "Gemini",
|
||||
"Ideogram": "Ideogram",
|
||||
@@ -1632,6 +1645,7 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_for_testing",
|
||||
"advanced": "پیشرفته",
|
||||
"animation": "انیمیشن",
|
||||
|
||||
@@ -328,6 +328,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaImageEditNode": {
|
||||
"description": "ویرایش تصاویر با استفاده از جدیدترین مدل Bria",
|
||||
"display_name": "ویرایش تصویر Bria",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "کنترل پس از تولید"
|
||||
},
|
||||
"guidance_scale": {
|
||||
"name": "مقیاس راهنما",
|
||||
"tooltip": "مقدار بالاتر باعث میشود تصویر بیشتر از پرامپت پیروی کند."
|
||||
},
|
||||
"image": {
|
||||
"name": "تصویر"
|
||||
},
|
||||
"mask": {
|
||||
"name": "ماسک",
|
||||
"tooltip": "در صورت عدم انتخاب، ویرایش بر کل تصویر اعمال میشود."
|
||||
},
|
||||
"model": {
|
||||
"name": "مدل"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "تنظیمات نظارت",
|
||||
"tooltip": "تنظیمات نظارت"
|
||||
},
|
||||
"moderation_prompt_content_moderation": {
|
||||
"name": "نظارت بر محتوای پرامپت"
|
||||
},
|
||||
"moderation_visual_input_moderation": {
|
||||
"name": "نظارت بر ورودی تصویری"
|
||||
},
|
||||
"moderation_visual_output_moderation": {
|
||||
"name": "نظارت بر خروجی تصویری"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "پرامپت منفی"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "پرامپت",
|
||||
"tooltip": "دستورالعمل برای ویرایش تصویر"
|
||||
},
|
||||
"seed": {
|
||||
"name": "بذر"
|
||||
},
|
||||
"steps": {
|
||||
"name": "گامها"
|
||||
},
|
||||
"structured_prompt": {
|
||||
"name": "پرامپت ساختاریافته",
|
||||
"tooltip": "یک رشته شامل پرامپت ویرایش ساختاریافته در قالب JSON. برای کنترل دقیق و برنامهنویسی شده، به جای پرامپت معمولی از این گزینه استفاده کنید."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "پرامپت ساختاریافته",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDanceFirstLastFrameNode": {
|
||||
"description": "تولید ویدیو با استفاده از پرامپت و اولین و آخرین فریم.",
|
||||
"display_name": "تبدیل اولین و آخرین فریم به ویدیو ByteDance",
|
||||
@@ -13477,6 +13539,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextEncodeZImageOmni": {
|
||||
"display_name": "TextEncodeZImageOmni",
|
||||
"inputs": {
|
||||
"auto_resize_images": {
|
||||
"name": "تغییر اندازه خودکار تصاویر"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
"image1": {
|
||||
"name": "تصویر ۱"
|
||||
},
|
||||
"image2": {
|
||||
"name": "تصویر ۲"
|
||||
},
|
||||
"image3": {
|
||||
"name": "تصویر ۳"
|
||||
},
|
||||
"image_encoder": {
|
||||
"name": "رمزگذار تصویر"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "پرامپت"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "تبدیل متن به حروف کوچک",
|
||||
"inputs": {
|
||||
@@ -16168,6 +16264,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedFlashVSRNode": {
|
||||
"description": "افزایشدهنده سریع و با کیفیت ویدیو که وضوح را افزایش داده و شفافیت را برای ویدیوهای کمکیفیت یا تار بازمیگرداند.",
|
||||
"display_name": "افزایش کیفیت ویدیو FlashVSR",
|
||||
"inputs": {
|
||||
"target_resolution": {
|
||||
"name": "وضوح هدف"
|
||||
},
|
||||
"video": {
|
||||
"name": "ویدیو"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedImageUpscaleNode": {
|
||||
"description": "افزایش وضوح و کیفیت تصویر، ارتقاء عکسها به ۴K یا ۸K برای نتایج شفاف و با جزئیات.",
|
||||
"display_name": "افزایش کیفیت تصویر WaveSpeed",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "تصویر"
|
||||
},
|
||||
"model": {
|
||||
"name": "مدل"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "وضوح هدف"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebcamCapture": {
|
||||
"display_name": "دریافت از وبکم",
|
||||
"inputs": {
|
||||
|
||||
@@ -238,6 +238,12 @@
|
||||
"title": "Créer un compte"
|
||||
}
|
||||
},
|
||||
"boundingBox": {
|
||||
"height": "Hauteur",
|
||||
"width": "Largeur",
|
||||
"x": "X",
|
||||
"y": "Y"
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "Effacer le workflow",
|
||||
"deleteBlueprint": "Supprimer le plan",
|
||||
@@ -811,6 +817,7 @@
|
||||
"nodeSlotsError": "Erreur d'emplacements du nœud",
|
||||
"nodeWidgetsError": "Erreur de widgets du nœud",
|
||||
"nodes": "Nœuds",
|
||||
"nodesCount": "{count} nœuds | {count} nœud | {count} nœuds",
|
||||
"nodesRunning": "nœuds en cours d’exécution",
|
||||
"none": "Aucun",
|
||||
"nothingToCopy": "Rien à copier",
|
||||
@@ -982,6 +989,11 @@
|
||||
"imageCompare": {
|
||||
"noImages": "Aucune image à comparer"
|
||||
},
|
||||
"imageCrop": {
|
||||
"cropPreviewAlt": "Aperçu du recadrage",
|
||||
"loading": "Chargement...",
|
||||
"noInputImage": "Aucune image d'entrée connectée"
|
||||
},
|
||||
"importFailed": {
|
||||
"copyError": "Erreur de copie",
|
||||
"title": "Échec de l’importation"
|
||||
@@ -1611,6 +1623,7 @@
|
||||
"3d": "3d",
|
||||
"3d_models": "modèles_3d",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "ByteDance",
|
||||
"Gemini": "Gemini",
|
||||
"Ideogram": "Ideogram",
|
||||
@@ -1632,6 +1645,7 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_pour_test",
|
||||
"advanced": "avancé",
|
||||
"animation": "animation",
|
||||
|
||||
@@ -328,6 +328,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaImageEditNode": {
|
||||
"description": "Modifiez des images en utilisant le dernier modèle Bria",
|
||||
"display_name": "Bria Image Edit",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "contrôle après génération"
|
||||
},
|
||||
"guidance_scale": {
|
||||
"name": "échelle de guidage",
|
||||
"tooltip": "Une valeur plus élevée fait suivre l'image à l'invite de façon plus précise."
|
||||
},
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"mask": {
|
||||
"name": "masque",
|
||||
"tooltip": "Si omis, la modification s'applique à l'image entière."
|
||||
},
|
||||
"model": {
|
||||
"name": "modèle"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "modération",
|
||||
"tooltip": "Paramètres de modération"
|
||||
},
|
||||
"moderation_prompt_content_moderation": {
|
||||
"name": "modération_du_contenu_de_l'invite"
|
||||
},
|
||||
"moderation_visual_input_moderation": {
|
||||
"name": "modération_de_l'entrée_visuelle"
|
||||
},
|
||||
"moderation_visual_output_moderation": {
|
||||
"name": "modération_de_la_sortie_visuelle"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "invite négative"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "invite",
|
||||
"tooltip": "Instruction pour modifier l'image"
|
||||
},
|
||||
"seed": {
|
||||
"name": "graine"
|
||||
},
|
||||
"steps": {
|
||||
"name": "étapes"
|
||||
},
|
||||
"structured_prompt": {
|
||||
"name": "invite structurée",
|
||||
"tooltip": "Une chaîne contenant l'invite d'édition structurée au format JSON. Utilisez ceci à la place de l'invite habituelle pour un contrôle précis et programmatique."
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "invite structurée",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDanceFirstLastFrameNode": {
|
||||
"description": "Générer une vidéo en utilisant l'invite et les première et dernière images.",
|
||||
"display_name": "ByteDance Première-Dernière Image vers Vidéo",
|
||||
@@ -13450,6 +13512,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextEncodeZImageOmni": {
|
||||
"display_name": "TextEncodeZImageOmni",
|
||||
"inputs": {
|
||||
"auto_resize_images": {
|
||||
"name": "redimensionnement automatique des images"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
"image1": {
|
||||
"name": "image1"
|
||||
},
|
||||
"image2": {
|
||||
"name": "image2"
|
||||
},
|
||||
"image3": {
|
||||
"name": "image3"
|
||||
},
|
||||
"image_encoder": {
|
||||
"name": "encodeur d'image"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "invite"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "Texte en minuscules",
|
||||
"inputs": {
|
||||
@@ -16137,6 +16233,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedFlashVSRNode": {
|
||||
"description": "Upscaler vidéo rapide et de haute qualité qui augmente la résolution et restaure la clarté des séquences basse résolution ou floues.",
|
||||
"display_name": "FlashVSR Upscale Vidéo",
|
||||
"inputs": {
|
||||
"target_resolution": {
|
||||
"name": "résolution cible"
|
||||
},
|
||||
"video": {
|
||||
"name": "vidéo"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedImageUpscaleNode": {
|
||||
"description": "Augmentez la résolution et la qualité de l'image, en upscalant les photos en 4K ou 8K pour des résultats nets et détaillés.",
|
||||
"display_name": "WaveSpeed Upscale Image",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "image"
|
||||
},
|
||||
"model": {
|
||||
"name": "modèle"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "résolution cible"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebcamCapture": {
|
||||
"display_name": "Capture Webcam",
|
||||
"inputs": {
|
||||
|
||||
@@ -238,6 +238,12 @@
|
||||
"title": "アカウントを作成する"
|
||||
}
|
||||
},
|
||||
"boundingBox": {
|
||||
"height": "高さ",
|
||||
"width": "幅",
|
||||
"x": "X",
|
||||
"y": "Y"
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "ワークフローをクリア",
|
||||
"deleteBlueprint": "ブループリントを削除",
|
||||
@@ -811,6 +817,7 @@
|
||||
"nodeSlotsError": "ノードスロットエラー",
|
||||
"nodeWidgetsError": "ノードウィジェットエラー",
|
||||
"nodes": "ノード",
|
||||
"nodesCount": "{count} ノード | {count} ノード | {count} ノード",
|
||||
"nodesRunning": "ノードが実行中",
|
||||
"none": "なし",
|
||||
"nothingToCopy": "コピーするものがありません",
|
||||
@@ -982,6 +989,11 @@
|
||||
"imageCompare": {
|
||||
"noImages": "比較する画像がありません"
|
||||
},
|
||||
"imageCrop": {
|
||||
"cropPreviewAlt": "切り抜きプレビュー",
|
||||
"loading": "読み込み中...",
|
||||
"noInputImage": "入力画像が接続されていません"
|
||||
},
|
||||
"importFailed": {
|
||||
"copyError": "コピーエラー",
|
||||
"title": "インポート失敗"
|
||||
@@ -1611,6 +1623,7 @@
|
||||
"3d": "3d",
|
||||
"3d_models": "3Dモデル",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "ByteDance",
|
||||
"Gemini": "Gemini",
|
||||
"Ideogram": "Ideogram",
|
||||
@@ -1632,6 +1645,7 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_テスト用",
|
||||
"advanced": "高度な機能",
|
||||
"animation": "アニメーション",
|
||||
|
||||
@@ -328,6 +328,68 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"BriaImageEditNode": {
|
||||
"description": "Briaの最新モデルを使って画像を編集します",
|
||||
"display_name": "Bria画像編集",
|
||||
"inputs": {
|
||||
"control_after_generate": {
|
||||
"name": "生成後コントロール"
|
||||
},
|
||||
"guidance_scale": {
|
||||
"name": "ガイダンススケール",
|
||||
"tooltip": "値が高いほどプロンプトに忠実な画像になります。"
|
||||
},
|
||||
"image": {
|
||||
"name": "画像"
|
||||
},
|
||||
"mask": {
|
||||
"name": "マスク",
|
||||
"tooltip": "省略した場合、編集は画像全体に適用されます。"
|
||||
},
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"moderation": {
|
||||
"name": "モデレーション",
|
||||
"tooltip": "モデレーション設定"
|
||||
},
|
||||
"moderation_prompt_content_moderation": {
|
||||
"name": "プロンプト内容モデレーション"
|
||||
},
|
||||
"moderation_visual_input_moderation": {
|
||||
"name": "入力画像モデレーション"
|
||||
},
|
||||
"moderation_visual_output_moderation": {
|
||||
"name": "出力画像モデレーション"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "ネガティブプロンプト"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "プロンプト",
|
||||
"tooltip": "画像編集の指示"
|
||||
},
|
||||
"seed": {
|
||||
"name": "シード"
|
||||
},
|
||||
"steps": {
|
||||
"name": "ステップ数"
|
||||
},
|
||||
"structured_prompt": {
|
||||
"name": "構造化プロンプト",
|
||||
"tooltip": "JSON形式の構造化編集プロンプトを含む文字列。より正確でプログラム的な制御が必要な場合は通常のプロンプトの代わりに使用してください。"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
},
|
||||
"1": {
|
||||
"name": "構造化プロンプト",
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"ByteDanceFirstLastFrameNode": {
|
||||
"description": "プロンプトと最初・最後のフレームを使用して動画を生成します。",
|
||||
"display_name": "ByteDance 最初-最後フレームから動画生成",
|
||||
@@ -13450,6 +13512,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextEncodeZImageOmni": {
|
||||
"display_name": "TextEncodeZImageOmni",
|
||||
"inputs": {
|
||||
"auto_resize_images": {
|
||||
"name": "画像自動リサイズ"
|
||||
},
|
||||
"clip": {
|
||||
"name": "clip"
|
||||
},
|
||||
"image1": {
|
||||
"name": "画像1"
|
||||
},
|
||||
"image2": {
|
||||
"name": "画像2"
|
||||
},
|
||||
"image3": {
|
||||
"name": "画像3"
|
||||
},
|
||||
"image_encoder": {
|
||||
"name": "画像エンコーダ"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "プロンプト"
|
||||
},
|
||||
"vae": {
|
||||
"name": "vae"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"TextToLowercase": {
|
||||
"display_name": "テキストを小文字に変換",
|
||||
"inputs": {
|
||||
@@ -16137,6 +16233,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedFlashVSRNode": {
|
||||
"description": "低解像度やぼやけた映像の解像度を向上させ、鮮明さを復元する高速・高品質なビデオアップスケーラーです。",
|
||||
"display_name": "FlashVSRビデオ高解像度化",
|
||||
"inputs": {
|
||||
"target_resolution": {
|
||||
"name": "目標解像度"
|
||||
},
|
||||
"video": {
|
||||
"name": "ビデオ"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WavespeedImageUpscaleNode": {
|
||||
"description": "画像の解像度と品質を向上させ、写真を4Kや8Kにアップスケールしてシャープで詳細な結果を得られます。",
|
||||
"display_name": "WaveSpeed画像高解像度化",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "画像"
|
||||
},
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"target_resolution": {
|
||||
"name": "目標解像度"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"tooltip": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"WebcamCapture": {
|
||||
"display_name": "ウェブカメラキャプチャ",
|
||||
"inputs": {
|
||||
|
||||