Compare commits
2 Commits
v1.38.9
...
feat/works
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3940cc5a9c | ||
|
|
a0dc6432fc |
@@ -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 oxfmt"
|
||||
echo " pnpm format - Format code with Prettier"
|
||||
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 oxfmt with auto-format
|
||||
- name: Run Prettier 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 Oxfmt fixes"
|
||||
git commit -m "[automated] Apply ESLint and Prettier 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- Oxfmt 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- Prettier 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,19 +10,7 @@ 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.
|
||||
@@ -38,4 +26,4 @@ module.exports = defineConfig({
|
||||
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
|
||||
- Maintain consistency with terminology used in Persian software and design applications.
|
||||
`
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
]
|
||||
}
|
||||
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
packages/registry-types/src/comfyRegistryTypes.ts
|
||||
src/types/generatedManagerTypes.ts
|
||||
11
.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
15
.vscode/extensions.json
vendored
@@ -1,22 +1,25 @@
|
||||
{
|
||||
"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",
|
||||
"wix.vscode-import-cost"
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"deque-systems.vscode-axe-linter",
|
||||
"kisstkondoros.vscode-codemetrics",
|
||||
"donjayamanne.githistory",
|
||||
"wix.vscode-import-cost",
|
||||
"prograhammer.tslint-vue",
|
||||
"antfu.vite"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
- `.oxfmtrc.json`
|
||||
- `.oxlintrc.json`
|
||||
- `.prettierrc`
|
||||
- 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`: oxfmt
|
||||
- `pnpm format` / `pnpm format:check`: Prettier
|
||||
- `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 `.oxfmtrc.json`)
|
||||
- Style: (see `.prettierrc`)
|
||||
- Indent 2 spaces
|
||||
- single quotes
|
||||
- no trailing semicolons
|
||||
|
||||
@@ -79,15 +79,48 @@ 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([
|
||||
node.emptySlot.pos[0],
|
||||
node.emptySlot.pos[1]
|
||||
slotX,
|
||||
slotY
|
||||
])
|
||||
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 |
@@ -189,7 +189,9 @@ test.describe('Templates', () => {
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
)
|
||||
const nav = comfyPage.page.locator('header', { hasText: 'Templates' })
|
||||
const nav = comfyPage.page
|
||||
.locator('header')
|
||||
.filter({ hasText: 'Templates' })
|
||||
|
||||
await comfyPage.templates.waitForMinimumCardCount(1)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
@@ -199,8 +201,7 @@ test.describe('Templates', () => {
|
||||
await comfyPage.page.setViewportSize(mobileSize)
|
||||
await comfyPage.templates.waitForMinimumCardCount(1)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
// Nav header is clipped by overflow-hidden parent at mobile size
|
||||
await expect(nav).not.toBeInViewport()
|
||||
await expect(nav).not.toBeVisible() // Nav should collapse at mobile size
|
||||
|
||||
const tabletSize = { width: 1024, height: 800 }
|
||||
await comfyPage.page.setViewportSize(tabletSize)
|
||||
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
@@ -76,7 +76,6 @@ function getModuleName(id: string): string {
|
||||
export function comfyAPIPlugin(isDev: boolean): Plugin {
|
||||
return {
|
||||
name: 'comfy-api-plugin',
|
||||
apply: 'build',
|
||||
transform(code: string, id: string) {
|
||||
if (isDev) return null
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
||||
@@ -30,10 +30,6 @@ describe('MyStore', () => {
|
||||
|
||||
**Why `stubActions: false`?** By default, testing pinia stubs all actions. Set to `false` when testing actual store behavior.
|
||||
|
||||
## i18n in Component Tests
|
||||
|
||||
Use real `createI18n` with empty messages instead of mocking `vue-i18n`. See `SearchBox.test.ts` for example.
|
||||
|
||||
## Mock Patterns
|
||||
|
||||
### Reset all mocks at once
|
||||
|
||||
@@ -4,7 +4,9 @@ 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'
|
||||
// eslint-config-prettier disables ESLint rules that conflict with formatters (oxfmt)
|
||||
// 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
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
import { configs as storybookConfigs } from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
@@ -109,7 +111,7 @@ export default defineConfig([
|
||||
tseslintConfigs.recommended,
|
||||
// Difference in typecheck on CI vs Local
|
||||
pluginVue.configs['flat/recommended'],
|
||||
// Disables ESLint rules that conflict with formatters
|
||||
// Use eslint-config-prettier instead of eslint-plugin-prettier to avoid Node 24 segfault
|
||||
eslintConfigPrettier,
|
||||
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types
|
||||
storybookConfigs['flat/recommended'],
|
||||
|
||||
25
lint-staged.config.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
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,9 +1,6 @@
|
||||
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[]) => [
|
||||
@@ -17,7 +14,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 oxfmt --write ${joinedPaths}`,
|
||||
`pnpm exec prettier --cache --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.9",
|
||||
"version": "1.38.7",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -22,8 +22,10 @@
|
||||
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
|
||||
"dev": "nx serve",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"format:check": "oxfmt --check",
|
||||
"format": "oxfmt --write",
|
||||
"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",
|
||||
"json-schema": "tsx scripts/generate-json-schema.ts",
|
||||
"knip:no-cache": "knip",
|
||||
"knip": "knip --cache",
|
||||
@@ -61,12 +63,14 @@
|
||||
"@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:",
|
||||
@@ -97,11 +101,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,6 +48,9 @@ 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
|
||||
@@ -93,6 +96,9 @@ 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
|
||||
@@ -201,9 +207,6 @@ 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
|
||||
@@ -219,6 +222,9 @@ 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
|
||||
@@ -534,6 +540,9 @@ 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
|
||||
@@ -552,6 +561,9 @@ 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
|
||||
@@ -642,9 +654,6 @@ 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)
|
||||
@@ -657,6 +666,9 @@ importers:
|
||||
postcss-html:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 3.7.4
|
||||
pretty-bytes:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
@@ -2505,6 +2517,95 @@ 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}
|
||||
@@ -2512,6 +2613,9 @@ 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]
|
||||
@@ -2612,46 +2716,6 @@ 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]
|
||||
@@ -2760,6 +2824,10 @@ 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'}
|
||||
@@ -3503,6 +3571,22 @@ 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==}
|
||||
|
||||
@@ -5955,6 +6039,9 @@ 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}
|
||||
@@ -6802,14 +6889,13 @@ 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
|
||||
@@ -7710,10 +7796,6 @@ 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'}
|
||||
@@ -10595,10 +10677,59 @@ 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
|
||||
|
||||
@@ -10661,30 +10792,6 @@ 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
|
||||
|
||||
@@ -10759,6 +10866,10 @@ 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
|
||||
@@ -11474,6 +11585,20 @@ 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':
|
||||
@@ -14306,6 +14431,8 @@ 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
|
||||
@@ -15404,6 +15531,26 @@ 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
|
||||
@@ -15427,19 +15574,6 @@ 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
|
||||
@@ -15620,8 +15754,7 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.7.4:
|
||||
optional: true
|
||||
prettier@3.7.4: {}
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
|
||||
@@ -16584,8 +16717,6 @@ 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,6 +17,7 @@ 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
|
||||
@@ -32,6 +33,7 @@ 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
|
||||
@@ -68,12 +70,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
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, nextTick } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import TopMenuSection from '@/components/TopMenuSection.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import type {
|
||||
JobListItem,
|
||||
JobStatus
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { TaskItemImpl, useQueueStore } from '@/stores/queueStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const mockData = vi.hoisted(() => ({ isLoggedIn: false }))
|
||||
@@ -41,8 +36,7 @@ function createWrapper() {
|
||||
sideToolbar: {
|
||||
queueProgressOverlay: {
|
||||
viewJobHistory: 'View job history',
|
||||
expandCollapsedQueue: 'Expand collapsed queue',
|
||||
activeJobsShort: '{count} active | {count} active'
|
||||
expandCollapsedQueue: 'Expand collapsed queue'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,19 +59,6 @@ function createWrapper() {
|
||||
})
|
||||
}
|
||||
|
||||
function createJob(id: string, status: JobStatus): JobListItem {
|
||||
return {
|
||||
id,
|
||||
status,
|
||||
create_time: 0,
|
||||
priority: 0
|
||||
}
|
||||
}
|
||||
|
||||
function createTask(id: string, status: JobStatus): TaskItemImpl {
|
||||
return new TaskItemImpl(createJob(id, status))
|
||||
}
|
||||
|
||||
describe('TopMenuSection', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
@@ -119,19 +100,4 @@ describe('TopMenuSection', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the active jobs label with the current count', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const queueStore = useQueueStore()
|
||||
queueStore.pendingTasks = [createTask('pending-1', 'pending')]
|
||||
queueStore.runningTasks = [
|
||||
createTask('running-1', 'in_progress'),
|
||||
createTask('running-2', 'in_progress')
|
||||
]
|
||||
|
||||
await nextTick()
|
||||
|
||||
const queueButton = wrapper.find('[data-testid="queue-overlay-toggle"]')
|
||||
expect(queueButton.text()).toContain('3 active')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -44,17 +44,19 @@
|
||||
<Button
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
type="destructive"
|
||||
size="md"
|
||||
size="icon"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
class="px-3"
|
||||
data-testid="queue-overlay-toggle"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
"
|
||||
@click="toggleQueueOverlay"
|
||||
>
|
||||
<span class="text-sm font-normal tabular-nums">
|
||||
{{ activeJobsLabel }}
|
||||
</span>
|
||||
<span class="sr-only">
|
||||
{{ t('sideToolbar.queueProgressOverlay.expandCollapsedQueue') }}
|
||||
<i class="icon-[lucide--history] size-4" />
|
||||
<span
|
||||
v-if="queuedCount > 0"
|
||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-base-foreground"
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
</Button>
|
||||
<CurrentUserButton
|
||||
@@ -115,26 +117,18 @@ const rightSidePanelStore = useRightSidePanelStore()
|
||||
const managerState = useManagerState()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
const { t, n } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
const queueUIStore = useQueueUIStore()
|
||||
const { activeJobsCount } = storeToRefs(queueStore)
|
||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||
const releaseStore = useReleaseStore()
|
||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
const isTopMenuHovered = ref(false)
|
||||
const activeJobsLabel = computed(() => {
|
||||
const count = activeJobsCount.value
|
||||
return t(
|
||||
'sideToolbar.queueProgressOverlay.activeJobsShort',
|
||||
{ count: n(count) },
|
||||
count
|
||||
)
|
||||
})
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const isIntegratedTabBar = computed(
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('EditableText', () => {
|
||||
isEditing: true
|
||||
})
|
||||
await wrapper.findComponent(InputText).setValue('New Text')
|
||||
await wrapper.findComponent(InputText).trigger('keydown.enter')
|
||||
await wrapper.findComponent(InputText).trigger('keyup.enter')
|
||||
// Blur event should have been triggered
|
||||
expect(wrapper.findComponent(InputText).element).not.toBe(
|
||||
document.activeElement
|
||||
@@ -79,7 +79,7 @@ describe('EditableText', () => {
|
||||
await wrapper.findComponent(InputText).setValue('Modified Text')
|
||||
|
||||
// Press escape
|
||||
await wrapper.findComponent(InputText).trigger('keydown.escape')
|
||||
await wrapper.findComponent(InputText).trigger('keyup.escape')
|
||||
|
||||
// Should emit cancel event
|
||||
expect(wrapper.emitted('cancel')).toBeTruthy()
|
||||
@@ -103,7 +103,7 @@ describe('EditableText', () => {
|
||||
await wrapper.findComponent(InputText).setValue('Modified Text')
|
||||
|
||||
// Press escape (which triggers blur internally)
|
||||
await wrapper.findComponent(InputText).trigger('keydown.escape')
|
||||
await wrapper.findComponent(InputText).trigger('keyup.escape')
|
||||
|
||||
// Manually trigger blur to simulate the blur that happens after escape
|
||||
await wrapper.findComponent(InputText).trigger('blur')
|
||||
@@ -120,7 +120,7 @@ describe('EditableText', () => {
|
||||
isEditing: true
|
||||
})
|
||||
await enterWrapper.findComponent(InputText).setValue('Saved Text')
|
||||
await enterWrapper.findComponent(InputText).trigger('keydown.enter')
|
||||
await enterWrapper.findComponent(InputText).trigger('keyup.enter')
|
||||
// Trigger blur that happens after enter
|
||||
await enterWrapper.findComponent(InputText).trigger('blur')
|
||||
expect(enterWrapper.emitted('edit')).toBeTruthy()
|
||||
@@ -133,7 +133,7 @@ describe('EditableText', () => {
|
||||
isEditing: true
|
||||
})
|
||||
await escapeWrapper.findComponent(InputText).setValue('Cancelled Text')
|
||||
await escapeWrapper.findComponent(InputText).trigger('keydown.escape')
|
||||
await escapeWrapper.findComponent(InputText).trigger('keyup.escape')
|
||||
expect(escapeWrapper.emitted('cancel')).toBeTruthy()
|
||||
expect(escapeWrapper.emitted('edit')).toBeFalsy()
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<span v-if="!isEditing">
|
||||
{{ modelValue }}
|
||||
</span>
|
||||
<!-- Avoid double triggering finishEditing event when keydown.enter is triggered -->
|
||||
<!-- Avoid double triggering finishEditing event when keyup.enter is triggered -->
|
||||
<InputText
|
||||
v-else
|
||||
ref="inputRef"
|
||||
@@ -18,8 +18,8 @@
|
||||
...inputAttrs
|
||||
}
|
||||
}"
|
||||
@keydown.enter.capture.stop="blurInputElement"
|
||||
@keydown.escape.capture.stop="cancelEditing"
|
||||
@keyup.enter.capture.stop="blurInputElement"
|
||||
@keyup.escape.stop="cancelEditing"
|
||||
@click.stop
|
||||
@contextmenu.stop
|
||||
@pointerdown.stop.capture
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import StatusBadge from './StatusBadge.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Common/StatusBadge',
|
||||
component: StatusBadge,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: { control: 'text' },
|
||||
severity: {
|
||||
control: 'select',
|
||||
options: ['default', 'secondary', 'warn', 'danger', 'contrast']
|
||||
},
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['label', 'dot', 'circle']
|
||||
}
|
||||
},
|
||||
args: {
|
||||
label: 'Status',
|
||||
severity: 'default'
|
||||
}
|
||||
} satisfies Meta<typeof StatusBadge>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
label: 'Failed',
|
||||
severity: 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
export const Finished: Story = {
|
||||
args: {
|
||||
label: 'Finished',
|
||||
severity: 'contrast'
|
||||
}
|
||||
}
|
||||
|
||||
export const Dot: Story = {
|
||||
args: {
|
||||
label: undefined,
|
||||
variant: 'dot',
|
||||
severity: 'danger'
|
||||
}
|
||||
}
|
||||
|
||||
export const Circle: Story = {
|
||||
args: {
|
||||
label: '3',
|
||||
variant: 'circle'
|
||||
}
|
||||
}
|
||||
|
||||
export const AllSeverities: Story = {
|
||||
render: () => ({
|
||||
components: { StatusBadge },
|
||||
template: `
|
||||
<div class="flex items-center gap-2">
|
||||
<StatusBadge label="Default" severity="default" />
|
||||
<StatusBadge label="Secondary" severity="secondary" />
|
||||
<StatusBadge label="Warn" severity="warn" />
|
||||
<StatusBadge label="Danger" severity="danger" />
|
||||
<StatusBadge label="Contrast" severity="contrast" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { StatusBadge },
|
||||
template: `
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<StatusBadge label="Label" variant="label" />
|
||||
<span class="text-xs text-muted">label</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<StatusBadge variant="dot" severity="danger" />
|
||||
<span class="text-xs text-muted">dot</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<StatusBadge label="5" variant="circle" />
|
||||
<span class="text-xs text-muted">circle</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { statusBadgeVariants } from './statusBadge.variants'
|
||||
import type { StatusBadgeVariants } from './statusBadge.variants'
|
||||
type Severity = 'default' | 'secondary' | 'warn' | 'danger' | 'contrast'
|
||||
|
||||
const {
|
||||
label,
|
||||
severity = 'default',
|
||||
variant
|
||||
} = defineProps<{
|
||||
label?: string | number
|
||||
severity?: StatusBadgeVariants['severity']
|
||||
variant?: StatusBadgeVariants['variant']
|
||||
const { label, severity = 'default' } = defineProps<{
|
||||
label: string
|
||||
severity?: Severity
|
||||
}>()
|
||||
|
||||
function badgeClasses(sev: Severity): string {
|
||||
const baseClasses =
|
||||
'inline-flex h-3.5 items-center justify-center rounded-full px-1 text-xxxs font-semibold uppercase'
|
||||
|
||||
switch (sev) {
|
||||
case 'danger':
|
||||
return `${baseClasses} bg-destructive-background text-white`
|
||||
case 'contrast':
|
||||
return `${baseClasses} bg-base-foreground text-base-background`
|
||||
case 'warn':
|
||||
return `${baseClasses} bg-warning-background text-base-background`
|
||||
case 'secondary':
|
||||
return `${baseClasses} bg-secondary-background text-base-foreground`
|
||||
default:
|
||||
return `${baseClasses} bg-primary-background text-base-foreground`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="
|
||||
statusBadgeVariants({
|
||||
severity,
|
||||
variant: variant ?? (label == null ? 'dot' : 'label')
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<span :class="badgeClasses(severity)">{{ label }}</span>
|
||||
</template>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="h-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-(--dialog-surface)"
|
||||
>
|
||||
<div :style="topSpacerStyle" />
|
||||
<div :style="mergedGridStyle">
|
||||
<div
|
||||
v-for="item in renderedItems"
|
||||
:key="item.key"
|
||||
class="transition-[width] duration-150 ease-out"
|
||||
data-virtual-grid-item
|
||||
>
|
||||
<div ref="container" class="scroll-container">
|
||||
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
|
||||
<div :style="gridStyle">
|
||||
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
|
||||
<slot name="item" :item="item" />
|
||||
</div>
|
||||
</div>
|
||||
<div :style="bottomSpacerStyle" />
|
||||
<div
|
||||
:style="{
|
||||
height: `${((items.length - state.end) / cols) * itemHeight}px`
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,22 +28,19 @@ type GridState = {
|
||||
|
||||
const {
|
||||
items,
|
||||
gridStyle,
|
||||
bufferRows = 1,
|
||||
scrollThrottle = 64,
|
||||
resizeDebounce = 64,
|
||||
defaultItemHeight = 200,
|
||||
defaultItemWidth = 200,
|
||||
maxColumns = Infinity
|
||||
defaultItemWidth = 200
|
||||
} = defineProps<{
|
||||
items: (T & { key: string })[]
|
||||
gridStyle: CSSProperties
|
||||
gridStyle: Partial<CSSProperties>
|
||||
bufferRows?: number
|
||||
scrollThrottle?: number
|
||||
resizeDebounce?: number
|
||||
defaultItemHeight?: number
|
||||
defaultItemWidth?: number
|
||||
maxColumns?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -66,18 +59,7 @@ const { y: scrollY } = useScroll(container, {
|
||||
eventListenerOptions: { passive: true }
|
||||
})
|
||||
|
||||
const cols = computed(() =>
|
||||
Math.min(Math.floor(width.value / itemWidth.value) || 1, maxColumns)
|
||||
)
|
||||
|
||||
const mergedGridStyle = computed<CSSProperties>(() => {
|
||||
if (maxColumns === Infinity) return gridStyle
|
||||
return {
|
||||
...gridStyle,
|
||||
gridTemplateColumns: `repeat(${maxColumns}, minmax(0, 1fr))`
|
||||
}
|
||||
})
|
||||
|
||||
const cols = computed(() => Math.floor(width.value / itemWidth.value) || 1)
|
||||
const viewRows = computed(() => Math.ceil(height.value / itemHeight.value))
|
||||
const offsetRows = computed(() => Math.floor(scrollY.value / itemHeight.value))
|
||||
const isValidGrid = computed(() => height.value && width.value && items?.length)
|
||||
@@ -101,16 +83,6 @@ const renderedItems = computed(() =>
|
||||
isValidGrid.value ? items.slice(state.value.start, state.value.end) : []
|
||||
)
|
||||
|
||||
function rowsToHeight(rows: number): string {
|
||||
return `${(rows / cols.value) * itemHeight.value}px`
|
||||
}
|
||||
const topSpacerStyle = computed<CSSProperties>(() => ({
|
||||
height: rowsToHeight(state.value.start)
|
||||
}))
|
||||
const bottomSpacerStyle = computed<CSSProperties>(() => ({
|
||||
height: rowsToHeight(items.length - state.value.end)
|
||||
}))
|
||||
|
||||
whenever(
|
||||
() => state.value.isNearEnd,
|
||||
() => {
|
||||
@@ -137,6 +109,15 @@ const onResize = debounce(updateItemSize, resizeDebounce)
|
||||
watch([width, height], onResize, { flush: 'post' })
|
||||
whenever(() => items, updateItemSize, { flush: 'post' })
|
||||
onBeforeUnmount(() => {
|
||||
onResize.cancel()
|
||||
onResize.cancel() // Clear pending debounced calls
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--dialog-surface) transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { VariantProps } from 'cva'
|
||||
import { cva } from 'cva'
|
||||
|
||||
export const statusBadgeVariants = cva({
|
||||
base: 'inline-flex items-center justify-center rounded-full',
|
||||
variants: {
|
||||
severity: {
|
||||
default: 'bg-primary-background text-base-foreground',
|
||||
secondary: 'bg-secondary-background text-base-foreground',
|
||||
warn: 'bg-warning-background text-base-background',
|
||||
danger: 'bg-destructive-background text-white',
|
||||
contrast: 'bg-base-foreground text-base-background'
|
||||
},
|
||||
variant: {
|
||||
label: 'h-3.5 px-1 text-xxxs font-semibold uppercase',
|
||||
dot: 'size-2',
|
||||
circle: 'size-3.5 text-xxxs font-semibold'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
severity: 'default',
|
||||
variant: 'label'
|
||||
}
|
||||
})
|
||||
|
||||
export type StatusBadgeVariants = VariantProps<typeof statusBadgeVariants>
|
||||
@@ -200,13 +200,7 @@ const onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {
|
||||
|
||||
if (item.state === 'running' || item.state === 'initialization') {
|
||||
// Running/initializing jobs: interrupt execution
|
||||
// Cloud backend uses deleteItem, local uses interrupt
|
||||
if (isCloud) {
|
||||
await api.deleteItem('queue', promptId)
|
||||
} else {
|
||||
await api.interrupt(promptId)
|
||||
}
|
||||
executionStore.clearInitializationByPromptId(promptId)
|
||||
await api.interrupt(promptId)
|
||||
await queueStore.update()
|
||||
} else if (item.state === 'pending') {
|
||||
// Pending jobs: remove from queue
|
||||
@@ -274,15 +268,7 @@ 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 () => {
|
||||
@@ -298,14 +284,10 @@ 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 = () => {
|
||||
|
||||
@@ -5,32 +5,25 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TransitionCollapse from './TransitionCollapse.vue'
|
||||
|
||||
const {
|
||||
disabled,
|
||||
label,
|
||||
enableEmptyState,
|
||||
tooltip,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
disabled?: boolean
|
||||
label?: string
|
||||
enableEmptyState?: boolean
|
||||
tooltip?: string
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const isCollapse = defineModel<boolean>('collapse', { default: false })
|
||||
|
||||
const isExpanded = computed(() => !isCollapse.value && !disabled)
|
||||
const isExpanded = computed(() => !isCollapse.value && !props.disabled)
|
||||
|
||||
const tooltipConfig = computed(() => {
|
||||
if (!tooltip) return undefined
|
||||
return { value: tooltip, showDelay: 1000 }
|
||||
if (!props.tooltip) return undefined
|
||||
return { value: props.tooltip, showDelay: 1000 }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col bg-comfy-menu-bg', className)">
|
||||
<div class="flex flex-col bg-comfy-menu-bg">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl bg-inherit"
|
||||
>
|
||||
|
||||
@@ -25,31 +25,13 @@ const widgetsSectionDataList = computed((): NodeWidgetsListList => {
|
||||
return nodes.map((node) => {
|
||||
const { widgets = [] } = node
|
||||
const shownWidgets = widgets
|
||||
.filter(
|
||||
(w) =>
|
||||
!(w.options?.canvasOnly || w.options?.hidden || w.options?.advanced)
|
||||
)
|
||||
.filter((w) => !(w.options?.canvasOnly || w.options?.hidden))
|
||||
.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
|
||||
)
|
||||
@@ -74,12 +56,6 @@ 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>
|
||||
@@ -117,16 +93,4 @@ const advancedLabel = 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>
|
||||
|
||||
@@ -43,7 +43,7 @@ const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const isEditing = ref(false)
|
||||
|
||||
const widgetComponent = computed(() => {
|
||||
const component = getComponent(widget.type)
|
||||
const component = getComponent(widget.type, widget.name)
|
||||
return component || WidgetLegacy
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
: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"
|
||||
/>
|
||||
@@ -22,3 +21,24 @@ 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,5 +1,6 @@
|
||||
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'
|
||||
@@ -32,7 +33,8 @@ describe('SidebarIcon', () => {
|
||||
return mount(SidebarIcon, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
directives: { tooltip: Tooltip }
|
||||
directives: { tooltip: Tooltip },
|
||||
components: { OverlayBadge }
|
||||
},
|
||||
props: { ...exampleProps, ...props },
|
||||
...options
|
||||
@@ -52,9 +54,9 @@ describe('SidebarIcon', () => {
|
||||
it('creates badge when iconBadge prop is set', () => {
|
||||
const badge = '2'
|
||||
const wrapper = mountSidebarIcon({ iconBadge: badge })
|
||||
const badgeEl = wrapper.find('.sidebar-icon-badge')
|
||||
const badgeEl = wrapper.findComponent(OverlayBadge)
|
||||
expect(badgeEl.exists()).toBe(true)
|
||||
expect(badgeEl.text()).toEqual(badge)
|
||||
expect(badgeEl.find('.p-badge').text()).toEqual(badge)
|
||||
})
|
||||
|
||||
it('shows tooltip on hover', async () => {
|
||||
|
||||
@@ -17,28 +17,22 @@
|
||||
>
|
||||
<div class="side-bar-button-content">
|
||||
<slot name="icon">
|
||||
<div class="sidebar-icon-wrapper relative">
|
||||
<OverlayBadge v-if="shouldShowBadge" :value="overlayValue">
|
||||
<i
|
||||
v-if="typeof icon === 'string'"
|
||||
:class="icon + ' 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>
|
||||
<component :is="icon" v-else class="side-bar-button-icon" />
|
||||
</OverlayBadge>
|
||||
<i
|
||||
v-else-if="typeof icon === 'string'"
|
||||
:class="icon + ' side-bar-button-icon'"
|
||||
/>
|
||||
<component
|
||||
:is="icon"
|
||||
v-else-if="typeof icon === 'object'"
|
||||
class="side-bar-button-icon"
|
||||
/>
|
||||
</slot>
|
||||
<span v-if="label && !isSmall" class="side-bar-button-label">{{
|
||||
t(label)
|
||||
@@ -48,6 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import OverlayBadge from 'primevue/overlaybadge'
|
||||
import { computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -62,7 +57,6 @@ const {
|
||||
tooltip = '',
|
||||
tooltipSuffix = '',
|
||||
iconBadge = '',
|
||||
badgeClass = '',
|
||||
label = '',
|
||||
isSmall = false
|
||||
} = defineProps<{
|
||||
@@ -71,7 +65,6 @@ const {
|
||||
tooltip?: string
|
||||
tooltipSuffix?: string
|
||||
iconBadge?: string | (() => string | null)
|
||||
badgeClass?: string
|
||||
label?: string
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col">
|
||||
<!-- Active Jobs Grid -->
|
||||
<div
|
||||
v-if="activeJobItems.length"
|
||||
class="grid max-h-[50%] scrollbar-custom overflow-y-auto"
|
||||
:style="gridStyle"
|
||||
>
|
||||
<ActiveJobCard v-for="job in activeJobItems" :key="job.id" :job="job" />
|
||||
</div>
|
||||
|
||||
<!-- Assets Header -->
|
||||
<div
|
||||
v-if="assets.length"
|
||||
:class="cn('px-2 2xl:px-4', activeJobItems.length && 'mt-2')"
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
assetType === 'input'
|
||||
? 'sideToolbar.importedAssetsHeader'
|
||||
: 'sideToolbar.generatedAssetsHeader'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Grid -->
|
||||
<VirtualGrid
|
||||
class="flex-1"
|
||||
:items="assetItems"
|
||||
:grid-style="gridStyle"
|
||||
@approach-end="emit('approach-end')"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MediaAssetCard
|
||||
:asset="item.asset"
|
||||
:selected="isSelected(item.asset.id)"
|
||||
:show-output-count="showOutputCount(item.asset)"
|
||||
:output-count="getOutputCount(item.asset)"
|
||||
@click="emit('select-asset', item.asset)"
|
||||
@context-menu="emit('context-menu', $event, item.asset)"
|
||||
@zoom="emit('zoom', item.asset)"
|
||||
@output-count-click="emit('output-count-click', item.asset)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import ActiveJobCard from '@/components/sidebar/tabs/assets/ActiveJobCard.vue'
|
||||
import { useJobList } from '@/composables/queue/useJobList'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const {
|
||||
assets,
|
||||
isSelected,
|
||||
assetType = 'output',
|
||||
showOutputCount,
|
||||
getOutputCount
|
||||
} = defineProps<{
|
||||
assets: AssetItem[]
|
||||
isSelected: (assetId: string) => boolean
|
||||
assetType?: 'input' | 'output'
|
||||
showOutputCount: (asset: AssetItem) => boolean
|
||||
getOutputCount: (asset: AssetItem) => number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-asset', asset: AssetItem): void
|
||||
(e: 'context-menu', event: MouseEvent, asset: AssetItem): void
|
||||
(e: 'approach-end'): void
|
||||
(e: 'zoom', asset: AssetItem): void
|
||||
(e: 'output-count-click', asset: AssetItem): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { jobItems } = useJobList()
|
||||
|
||||
type AssetGridItem = { key: string; asset: AssetItem }
|
||||
|
||||
const activeJobItems = computed(() =>
|
||||
jobItems.value.filter((item) => isActiveJobState(item.state))
|
||||
)
|
||||
|
||||
const assetItems = computed<AssetGridItem[]>(() =>
|
||||
assets.map((asset) => ({
|
||||
key: `asset-${asset.id}`,
|
||||
asset
|
||||
}))
|
||||
)
|
||||
|
||||
const gridStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
padding: '0 0.5rem',
|
||||
gap: '0.5rem'
|
||||
}
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="flex h-full flex-col">
|
||||
<div
|
||||
v-if="activeJobItems.length"
|
||||
class="flex max-h-[50%] scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
|
||||
class="flex max-h-[50%] flex-col gap-2 overflow-y-auto px-2"
|
||||
>
|
||||
<AssetsListItem
|
||||
v-for="job in activeJobItems"
|
||||
@@ -44,15 +44,9 @@
|
||||
:class="cn('px-2', activeJobItems.length && 'mt-2')"
|
||||
>
|
||||
<div
|
||||
class="flex items-center p-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
class="flex items-center py-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
assetType === 'input'
|
||||
? 'sideToolbar.importedAssetsHeader'
|
||||
: 'sideToolbar.generatedAssetsHeader'
|
||||
)
|
||||
}}
|
||||
{{ t('sideToolbar.generatedAssetsHeader') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,7 +108,7 @@ import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
|
||||
import { isActiveJobState } from '@/utils/queueUtil'
|
||||
import type { JobState } from '@/types/queue'
|
||||
import {
|
||||
formatDuration,
|
||||
formatSize,
|
||||
@@ -124,14 +118,9 @@ import {
|
||||
import { iconForJobState } from '@/utils/queueDisplay'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const {
|
||||
assets,
|
||||
isSelected,
|
||||
assetType = 'output'
|
||||
} = defineProps<{
|
||||
const { assets, isSelected } = defineProps<{
|
||||
assets: AssetItem[]
|
||||
isSelected: (assetId: string) => boolean
|
||||
assetType?: 'input' | 'output'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -172,6 +161,12 @@ const listGridStyle = {
|
||||
gap: '0.5rem'
|
||||
}
|
||||
|
||||
function isActiveJobState(state: JobState): boolean {
|
||||
return (
|
||||
state === 'pending' || state === 'initialization' || state === 'running'
|
||||
)
|
||||
}
|
||||
|
||||
function getAssetPrimaryText(asset: AssetItem): string {
|
||||
return truncateFilename(asset.name)
|
||||
}
|
||||
|
||||
@@ -100,24 +100,34 @@
|
||||
v-if="isListView"
|
||||
:assets="displayAssets"
|
||||
:is-selected="isSelected"
|
||||
:asset-type="activeTab"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@approach-end="handleApproachEnd"
|
||||
/>
|
||||
<AssetsSidebarGridView
|
||||
<VirtualGrid
|
||||
v-else
|
||||
:assets="displayAssets"
|
||||
:is-selected="isSelected"
|
||||
:asset-type="activeTab"
|
||||
:show-output-count="shouldShowOutputCount"
|
||||
:get-output-count="getOutputCount"
|
||||
@select-asset="handleAssetSelect"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
:items="mediaAssetsWithKey"
|
||||
:grid-style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
padding: '0 0.5rem',
|
||||
gap: '0.5rem'
|
||||
}"
|
||||
@approach-end="handleApproachEnd"
|
||||
@zoom="handleZoomClick"
|
||||
@output-count-click="enterFolderView"
|
||||
/>
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MediaAssetCard
|
||||
:asset="item"
|
||||
:selected="isSelected(item.id)"
|
||||
:show-output-count="shouldShowOutputCount(item)"
|
||||
:output-count="getOutputCount(item)"
|
||||
@click="handleAssetSelect(item)"
|
||||
@context-menu="handleAssetContextMenu"
|
||||
@zoom="handleZoomClick(item)"
|
||||
@output-count-click="enterFolderView(item)"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
@@ -202,7 +212,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Divider from 'primevue/divider'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
@@ -210,14 +219,15 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import MediaAssetContextMenu from '@/platform/assets/components/MediaAssetContextMenu.vue'
|
||||
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
|
||||
import { getAssetType } from '@/platform/assets/composables/media/assetMappers'
|
||||
@@ -233,7 +243,6 @@ 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'
|
||||
@@ -247,8 +256,6 @@ interface JobOutputItem {
|
||||
const { t, n } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
const { activeJobsCount } = storeToRefs(queueStore)
|
||||
const executionStore = useExecutionStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('output')
|
||||
@@ -294,6 +301,9 @@ const formattedExecutionTime = computed(() => {
|
||||
})
|
||||
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const activeJobsCount = computed(
|
||||
() => queueStore.pendingTasks.length + queueStore.runningTasks.length
|
||||
)
|
||||
const activeJobsLabel = computed(() => {
|
||||
const count = activeJobsCount.value
|
||||
return t(
|
||||
@@ -394,14 +404,14 @@ const showLoadingState = computed(
|
||||
() =>
|
||||
loading.value &&
|
||||
displayAssets.value.length === 0 &&
|
||||
activeJobsCount.value === 0
|
||||
(!isListView.value || activeJobsCount.value === 0)
|
||||
)
|
||||
|
||||
const showEmptyState = computed(
|
||||
() =>
|
||||
!loading.value &&
|
||||
displayAssets.value.length === 0 &&
|
||||
activeJobsCount.value === 0
|
||||
(!isListView.value || activeJobsCount.value === 0)
|
||||
)
|
||||
|
||||
watch(displayAssets, (newAssets) => {
|
||||
@@ -443,6 +453,14 @@ const galleryItems = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// Add key property for VirtualGrid
|
||||
const mediaAssetsWithKey = computed(() => {
|
||||
return displayAssets.value.map((asset) => ({
|
||||
...asset,
|
||||
key: asset.id
|
||||
}))
|
||||
})
|
||||
|
||||
const refreshAssets = async () => {
|
||||
await currentAssets.value.fetchMediaList()
|
||||
if (error.value) {
|
||||
@@ -492,13 +510,7 @@ 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[]) => {
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ActiveJobCard from './ActiveJobCard.vue'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
|
||||
vi.mock('@/composables/useProgressBarBackground', () => ({
|
||||
useProgressBarBackground: () => ({
|
||||
progressBarPrimaryClass: 'bg-blue-500',
|
||||
hasProgressPercent: (val: number | undefined) => typeof val === 'number',
|
||||
progressPercentStyle: (val: number) => ({ width: `${val}%` })
|
||||
})
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
sideToolbar: {
|
||||
activeJobStatus: 'Active job: {status}'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const createJob = (overrides: Partial<JobListItem> = {}): JobListItem => ({
|
||||
id: 'test-job-1',
|
||||
title: 'Running...',
|
||||
meta: 'Step 5/10',
|
||||
state: 'running',
|
||||
progressTotalPercent: 50,
|
||||
progressCurrentPercent: 75,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const mountComponent = (job: JobListItem) =>
|
||||
mount(ActiveJobCard, {
|
||||
props: { job },
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
}
|
||||
})
|
||||
|
||||
describe('ActiveJobCard', () => {
|
||||
it('displays percentage and progress bar when job is running', () => {
|
||||
const wrapper = mountComponent(
|
||||
createJob({ state: 'running', progressTotalPercent: 65 })
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain('65%')
|
||||
const progressBar = wrapper.find('.bg-blue-500')
|
||||
expect(progressBar.exists()).toBe(true)
|
||||
expect(progressBar.attributes('style')).toContain('width: 65%')
|
||||
})
|
||||
|
||||
it('displays status text when job is pending', () => {
|
||||
const wrapper = mountComponent(
|
||||
createJob({
|
||||
state: 'pending',
|
||||
title: 'In queue...',
|
||||
progressTotalPercent: undefined
|
||||
})
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain('In queue...')
|
||||
const progressBar = wrapper.find('.bg-blue-500')
|
||||
expect(progressBar.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows spinner for pending state', () => {
|
||||
const wrapper = mountComponent(createJob({ state: 'pending' }))
|
||||
|
||||
const spinner = wrapper.find('.icon-\\[lucide--loader-circle\\]')
|
||||
expect(spinner.exists()).toBe(true)
|
||||
expect(spinner.classes()).toContain('animate-spin')
|
||||
})
|
||||
|
||||
it('shows error icon for failed state', () => {
|
||||
const wrapper = mountComponent(
|
||||
createJob({ state: 'failed', title: 'Failed' })
|
||||
)
|
||||
|
||||
const errorIcon = wrapper.find('.icon-\\[lucide--circle-alert\\]')
|
||||
expect(errorIcon.exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Failed')
|
||||
})
|
||||
|
||||
it('shows preview image when running with iconImageUrl', () => {
|
||||
const wrapper = mountComponent(
|
||||
createJob({
|
||||
state: 'running',
|
||||
iconImageUrl: 'https://example.com/preview.jpg'
|
||||
})
|
||||
)
|
||||
|
||||
const img = wrapper.find('img')
|
||||
expect(img.exists()).toBe(true)
|
||||
expect(img.attributes('src')).toBe('https://example.com/preview.jpg')
|
||||
})
|
||||
|
||||
it('has proper accessibility attributes', () => {
|
||||
const wrapper = mountComponent(createJob({ title: 'Generating...' }))
|
||||
|
||||
const container = wrapper.find('[role="status"]')
|
||||
expect(container.exists()).toBe(true)
|
||||
expect(container.attributes('aria-label')).toBe('Active job: Generating...')
|
||||
})
|
||||
})
|
||||
@@ -1,85 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
role="status"
|
||||
:aria-label="t('sideToolbar.activeJobStatus', { status: statusText })"
|
||||
class="flex flex-col gap-2 p-2 rounded-lg"
|
||||
>
|
||||
<!-- Thumbnail -->
|
||||
<div class="relative aspect-square overflow-hidden rounded-lg">
|
||||
<!-- Running state with preview image -->
|
||||
<img
|
||||
v-if="isRunning && job.iconImageUrl"
|
||||
:src="job.iconImageUrl"
|
||||
:alt="statusText"
|
||||
class="size-full object-cover"
|
||||
/>
|
||||
<!-- Placeholder for queued/failed states or running without preview -->
|
||||
<div
|
||||
v-else
|
||||
class="absolute inset-0 flex items-center justify-center bg-modal-card-placeholder-background"
|
||||
>
|
||||
<!-- Spinner for queued/initialization states -->
|
||||
<i
|
||||
v-if="isQueued"
|
||||
class="icon-[lucide--loader-circle] size-8 animate-spin text-muted-foreground"
|
||||
/>
|
||||
<!-- Error icon for failed state -->
|
||||
<i
|
||||
v-else-if="isFailed"
|
||||
class="icon-[lucide--circle-alert] size-8 text-red-500"
|
||||
/>
|
||||
<!-- Spinner for running without preview -->
|
||||
<i
|
||||
v-else
|
||||
class="icon-[lucide--loader-circle] size-8 animate-spin text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: Progress bar or status text -->
|
||||
<div class="flex gap-1.5 items-center h-5">
|
||||
<!-- Running state: percentage + progress bar -->
|
||||
<template v-if="isRunning && hasProgressPercent(progressPercent)">
|
||||
<span class="shrink-0 text-sm text-muted-foreground">
|
||||
{{ Math.round(progressPercent ?? 0) }}%
|
||||
</span>
|
||||
<div class="flex-1 relative h-1 rounded-sm bg-secondary-background">
|
||||
<div
|
||||
:class="progressBarPrimaryClass"
|
||||
:style="progressPercentStyle(progressPercent)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Non-running states: status text only -->
|
||||
<template v-else>
|
||||
<div class="w-full truncate text-center text-sm text-muted-foreground">
|
||||
{{ statusText }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { JobListItem } from '@/composables/queue/useJobList'
|
||||
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
|
||||
|
||||
const { job } = defineProps<{ job: JobListItem }>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { progressBarPrimaryClass, hasProgressPercent, progressPercentStyle } =
|
||||
useProgressBarBackground()
|
||||
|
||||
const statusText = computed(() => job.title)
|
||||
const progressPercent = computed(() => job.progressTotalPercent)
|
||||
|
||||
const isQueued = computed(
|
||||
() => job.state === 'pending' || job.state === 'initialization'
|
||||
)
|
||||
const isRunning = computed(() => job.state === 'running')
|
||||
const isFailed = computed(() => job.state === 'failed')
|
||||
</script>
|
||||
@@ -1,19 +0,0 @@
|
||||
# UI Component Guidelines
|
||||
|
||||
## Adding New Components
|
||||
|
||||
```bash
|
||||
pnpm dlx shadcn-vue@latest add <component-name> --yes
|
||||
```
|
||||
|
||||
After adding, create `ComponentName.stories.ts` with Default, Disabled, and variant stories.
|
||||
|
||||
## Reka UI Wrapper Components
|
||||
|
||||
- Use reactive props destructuring with rest: `const { class: className, ...restProps } = defineProps<Props>()`
|
||||
- Use `useForwardProps(restProps)` for prop forwarding, or `computed()` if adding defaults
|
||||
- Import siblings directly (`./Component.vue`), not from barrel (`'.'`)
|
||||
- Use `cn()` for class merging with `className`
|
||||
- Use Iconify icons: `<i class="icon-[lucide--check]" />`
|
||||
- Use design tokens: `bg-secondary-background`, `text-muted-foreground`, `border-border-default`
|
||||
- Tailwind 4 CSS variables use parentheses: `h-(--my-var)` not `h-[--my-var]`
|
||||
@@ -1,261 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Select from './Select.vue'
|
||||
import SelectContent from './SelectContent.vue'
|
||||
import SelectGroup from './SelectGroup.vue'
|
||||
import SelectItem from './SelectItem.vue'
|
||||
import SelectLabel from './SelectLabel.vue'
|
||||
import SelectSeparator from './SelectSeparator.vue'
|
||||
import SelectTrigger from './SelectTrigger.vue'
|
||||
import SelectValue from './SelectValue.vue'
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Select',
|
||||
component: Select,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
modelValue: {
|
||||
control: 'text',
|
||||
description: 'Selected value'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'When true, disables the select'
|
||||
},
|
||||
'onUpdate:modelValue': { action: 'update:modelValue' }
|
||||
}
|
||||
} satisfies Meta<typeof Select>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
},
|
||||
setup() {
|
||||
const value = ref(args.modelValue || '')
|
||||
return { value, args }
|
||||
},
|
||||
template: `
|
||||
<Select v-model="value" :disabled="args.disabled">
|
||||
<SelectTrigger class="w-56">
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="cherry">Cherry</SelectItem>
|
||||
<SelectItem value="grape">Grape</SelectItem>
|
||||
<SelectItem value="orange">Orange</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="mt-4 text-sm text-muted-foreground">
|
||||
Selected: {{ value || 'None' }}
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
export const WithPlaceholder: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
},
|
||||
setup() {
|
||||
const value = ref('')
|
||||
return { value, args }
|
||||
},
|
||||
template: `
|
||||
<Select v-model="value" :disabled="args.disabled">
|
||||
<SelectTrigger class="w-56">
|
||||
<SelectValue placeholder="Choose an option..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
},
|
||||
setup() {
|
||||
const value = ref('apple')
|
||||
return { value, args }
|
||||
},
|
||||
template: `
|
||||
<Select v-model="value" disabled>
|
||||
<SelectTrigger class="w-56">
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="apple">Apple</SelectItem>
|
||||
<SelectItem value="banana">Banana</SelectItem>
|
||||
<SelectItem value="cherry">Cherry</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const WithGroups: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
},
|
||||
setup() {
|
||||
const value = ref('')
|
||||
return { value, args }
|
||||
},
|
||||
template: `
|
||||
<Select v-model="value" :disabled="args.disabled">
|
||||
<SelectTrigger class="w-56">
|
||||
<SelectValue placeholder="Select a model type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Checkpoints</SelectLabel>
|
||||
<SelectItem value="sd15">SD 1.5</SelectItem>
|
||||
<SelectItem value="sdxl">SDXL</SelectItem>
|
||||
<SelectItem value="flux">Flux</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>LoRAs</SelectLabel>
|
||||
<SelectItem value="lora-style">Style LoRA</SelectItem>
|
||||
<SelectItem value="lora-character">Character LoRA</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectSeparator />
|
||||
<SelectGroup>
|
||||
<SelectLabel>Other</SelectLabel>
|
||||
<SelectItem value="vae">VAE</SelectItem>
|
||||
<SelectItem value="embedding">Embedding</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="mt-4 text-sm text-muted-foreground">
|
||||
Selected: {{ value || 'None' }}
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
export const Scrollable: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
},
|
||||
setup() {
|
||||
const value = ref('')
|
||||
const items = Array.from({ length: 20 }, (_, i) => ({
|
||||
value: `item-${i + 1}`,
|
||||
label: `Option ${i + 1}`
|
||||
}))
|
||||
return { value, items, args }
|
||||
},
|
||||
template: `
|
||||
<Select v-model="value" :disabled="args.disabled">
|
||||
<SelectTrigger class="w-56">
|
||||
<SelectValue placeholder="Select an option" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
|
||||
export const CustomWidth: Story = {
|
||||
render: (args) => ({
|
||||
components: {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
},
|
||||
setup() {
|
||||
const value = ref('')
|
||||
return { value, args }
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<Select v-model="value" :disabled="args.disabled">
|
||||
<SelectTrigger class="w-32">
|
||||
<SelectValue placeholder="Small" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="a">A</SelectItem>
|
||||
<SelectItem value="b">B</SelectItem>
|
||||
<SelectItem value="c">C</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select v-model="value" :disabled="args.disabled">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue placeholder="Full width select" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">Option 1</SelectItem>
|
||||
<SelectItem value="option2">Option 2</SelectItem>
|
||||
<SelectItem value="option3">Option 3</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
disabled: false
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectRootEmits, SelectRootProps } from 'reka-ui'
|
||||
import { SelectRoot, useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
// eslint-disable-next-line vue/no-unused-properties
|
||||
const props = defineProps<SelectRootProps>()
|
||||
const emits = defineEmits<SelectRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</SelectRoot>
|
||||
</template>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectContentEmits, SelectContentProps } from 'reka-ui'
|
||||
import {
|
||||
SelectContent,
|
||||
SelectPortal,
|
||||
SelectViewport,
|
||||
useForwardPropsEmits
|
||||
} from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import SelectScrollDownButton from './SelectScrollDownButton.vue'
|
||||
import SelectScrollUpButton from './SelectScrollUpButton.vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const {
|
||||
position = 'popper',
|
||||
class: className,
|
||||
...restProps
|
||||
} = defineProps<SelectContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<SelectContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => ({
|
||||
position,
|
||||
...restProps
|
||||
}))
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal>
|
||||
<SelectContent
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="
|
||||
cn(
|
||||
'relative z-3000 max-h-96 min-w-32 overflow-hidden',
|
||||
'mt-2 rounded-lg p-2',
|
||||
'bg-base-background text-base-foreground',
|
||||
'border border-solid border-border-default',
|
||||
'shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
|
||||
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectViewport
|
||||
:class="
|
||||
cn(
|
||||
'scrollbar-custom flex flex-col gap-0',
|
||||
position === 'popper' &&
|
||||
'h-(--reka-select-trigger-height) w-full min-w-(--reka-select-trigger-width)'
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectGroupProps } from 'reka-ui'
|
||||
import { SelectGroup } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectGroupProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup :class="cn('w-full', className)" v-bind="restProps">
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectItemProps } from 'reka-ui'
|
||||
import { SelectItem, SelectItemIndicator, SelectItemText } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectItemProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem
|
||||
v-bind="restProps"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-full cursor-pointer select-none items-center justify-between',
|
||||
'gap-3 rounded px-2 py-3 text-sm outline-none',
|
||||
'hover:bg-secondary-background-hover',
|
||||
'focus:bg-secondary-background-hover',
|
||||
'data-[state=checked]:bg-secondary-background-selected',
|
||||
'data-[state=checked]:hover:bg-secondary-background-selected',
|
||||
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<SelectItemText class="truncate">
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
|
||||
<SelectItemIndicator class="flex shrink-0 items-center justify-center">
|
||||
<i class="icon-[lucide--check] text-base-foreground" aria-hidden="true" />
|
||||
</SelectItemIndicator>
|
||||
</SelectItem>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectLabelProps } from 'reka-ui'
|
||||
import { SelectLabel } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectLabelProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
v-bind="restProps"
|
||||
:class="
|
||||
cn(
|
||||
'px-3 py-2 text-xs uppercase tracking-wide text-muted-foreground',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectScrollDownButtonProps } from 'reka-ui'
|
||||
import { SelectScrollDownButton } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectScrollDownButtonProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollDownButton
|
||||
v-bind="restProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex cursor-default items-center justify-center py-1 text-muted-foreground',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot>
|
||||
<i class="icon-[lucide--chevron-down]" />
|
||||
</slot>
|
||||
</SelectScrollDownButton>
|
||||
</template>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectScrollUpButtonProps } from 'reka-ui'
|
||||
import { SelectScrollUpButton } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectScrollUpButtonProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollUpButton
|
||||
v-bind="restProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex cursor-default items-center justify-center py-1 text-muted-foreground',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot>
|
||||
<i class="icon-[lucide--chevron-up]" />
|
||||
</slot>
|
||||
</SelectScrollUpButton>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectSeparatorProps } from 'reka-ui'
|
||||
import { SelectSeparator } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectSeparatorProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
v-bind="restProps"
|
||||
:class="cn('-mx-1 my-1 h-px bg-border-default', className)"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectTriggerProps } from 'reka-ui'
|
||||
import { SelectIcon, SelectTrigger } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { class: className, ...restProps } = defineProps<
|
||||
SelectTriggerProps & { class?: HTMLAttributes['class'] }
|
||||
>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectTrigger
|
||||
v-bind="restProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 w-full cursor-pointer select-none items-center justify-between',
|
||||
'rounded-lg px-4 py-2 text-sm',
|
||||
'bg-secondary-background text-base-foreground',
|
||||
'border-[2.5px] border-solid border-transparent',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'focus:border-node-component-border focus:outline-none',
|
||||
'data-[placeholder]:text-muted-foreground',
|
||||
'disabled:cursor-not-allowed disabled:opacity-60',
|
||||
'[&>span]:truncate',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<i class="icon-[lucide--chevron-down] shrink-0 text-muted-foreground" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
</template>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { SelectValueProps } from 'reka-ui'
|
||||
import { SelectValue } from 'reka-ui'
|
||||
|
||||
const props = defineProps<SelectValueProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectValue v-bind="props">
|
||||
<slot />
|
||||
</SelectValue>
|
||||
</template>
|
||||
@@ -23,11 +23,6 @@ const showInput = computed(() => isEditing.value || isEmpty)
|
||||
const { forwardRef, currentElement } = useForwardExpose()
|
||||
const registerFocus = inject(tagsInputFocusKey, undefined)
|
||||
|
||||
function handleEscape() {
|
||||
currentElement.value?.blur()
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
registerFocus?.(() => currentElement.value?.focus())
|
||||
})
|
||||
@@ -49,6 +44,5 @@ onUnmounted(() => {
|
||||
className
|
||||
)
|
||||
"
|
||||
@keydown.escape.stop="handleEscape"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,128 +1,100 @@
|
||||
<template>
|
||||
<div
|
||||
class="base-widget-layout rounded-2xl overflow-hidden relative"
|
||||
@keydown.esc.capture="handleEscape"
|
||||
>
|
||||
<div
|
||||
class="grid h-full w-full transition-[grid-template-columns] duration-300 ease-out"
|
||||
:style="gridStyle"
|
||||
<div class="base-widget-layout rounded-2xl overflow-hidden relative">
|
||||
<Button
|
||||
v-show="!isRightPanelOpen && hasRightPanel"
|
||||
size="lg"
|
||||
:class="
|
||||
cn('absolute top-4 right-18 z-10', 'transition-opacity duration-200', {
|
||||
'opacity-0 pointer-events-none': isRightPanelOpen || !hasRightPanel
|
||||
})
|
||||
"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<nav
|
||||
class="h-full overflow-hidden"
|
||||
:inert="!showLeftPanel"
|
||||
:aria-hidden="!showLeftPanel"
|
||||
>
|
||||
<div v-if="hasLeftPanel" class="h-full min-w-40 max-w-56">
|
||||
<slot name="leftPanel" />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="flex flex-col bg-base-background overflow-hidden">
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="w-full h-18 px-6 flex items-center justify-between gap-2"
|
||||
<i class="icon-[lucide--panel-right]" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
class="absolute top-4 right-6 z-10 transition-opacity duration-200 w-10"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<i class="pi pi-times" />
|
||||
</Button>
|
||||
<div class="flex h-full w-full">
|
||||
<Transition name="slide-panel">
|
||||
<nav
|
||||
v-if="$slots.leftPanel && showLeftPanel"
|
||||
:class="[
|
||||
PANEL_SIZES.width,
|
||||
PANEL_SIZES.minWidth,
|
||||
PANEL_SIZES.maxWidth
|
||||
]"
|
||||
>
|
||||
<div class="flex flex-1 shrink-0 gap-2">
|
||||
<Button
|
||||
v-if="!notMobile"
|
||||
size="icon"
|
||||
:aria-label="
|
||||
showLeftPanel ? t('g.hideLeftPanel') : t('g.showLeftPanel')
|
||||
"
|
||||
@click="toggleLeftPanel"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
showLeftPanel
|
||||
? 'icon-[lucide--panel-left]'
|
||||
: 'icon-[lucide--panel-left-close]'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<slot name="header-right-area" />
|
||||
<template v-if="!isRightPanelOpen">
|
||||
<Button
|
||||
v-if="hasRightPanel"
|
||||
size="lg"
|
||||
class="w-10 p-0"
|
||||
:aria-label="t('g.showRightPanel')"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
class="w-10"
|
||||
:aria-label="t('g.closeDialog')"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<i class="pi pi-times" />
|
||||
</Button>
|
||||
</template>
|
||||
</header>
|
||||
<slot name="leftPanel"></slot>
|
||||
</nav>
|
||||
</Transition>
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col">
|
||||
<slot name="contentFilter" />
|
||||
<h2
|
||||
v-if="!hasLeftPanel"
|
||||
class="text-xxl m-0 px-6 pt-2 pb-6 capitalize"
|
||||
>
|
||||
{{ contentTitle }}
|
||||
</h2>
|
||||
<div
|
||||
class="min-h-0 flex-1 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<aside
|
||||
v-if="hasRightPanel"
|
||||
class="overflow-hidden"
|
||||
:inert="!isRightPanelOpen"
|
||||
:aria-hidden="!isRightPanelOpen"
|
||||
>
|
||||
<div
|
||||
class="min-w-72 w-72 flex flex-col bg-modal-panel-background h-full"
|
||||
>
|
||||
<div class="flex-1 flex bg-base-background">
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<header
|
||||
data-component-id="RightPanelHeader"
|
||||
class="flex h-18 shrink-0 items-center gap-2 px-6"
|
||||
v-if="$slots.header"
|
||||
class="w-full h-18 px-6 flex items-center justify-between gap-2"
|
||||
>
|
||||
<h2 v-if="rightPanelTitle" class="flex-1 text-base font-semibold">
|
||||
{{ rightPanelTitle }}
|
||||
</h2>
|
||||
<div v-else class="flex-1">
|
||||
<slot name="rightPanelHeaderTitle" />
|
||||
<div class="flex flex-1 shrink-0 gap-2">
|
||||
<Button v-if="!notMobile" size="icon" @click="toggleLeftPanel">
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
showLeftPanel
|
||||
? 'icon-[lucide--panel-left]'
|
||||
: 'icon-[lucide--panel-left-close]'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<slot name="rightPanelHeaderActions" />
|
||||
<Button
|
||||
size="lg"
|
||||
class="w-10 p-0"
|
||||
:aria-label="t('g.hideRightPanel')"
|
||||
@click="toggleRightPanel"
|
||||
<slot name="header-right-area"></slot>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex justify-end gap-2 w-0',
|
||||
hasRightPanel && !isRightPanelOpen ? 'min-w-22' : 'min-w-10'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right-close] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
class="w-10 p-0"
|
||||
:aria-label="t('g.closeDialog')"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<i class="pi pi-times" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isRightPanelOpen && hasRightPanel"
|
||||
size="lg"
|
||||
@click="toggleRightPanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right-close]" />
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
<slot name="rightPanel" />
|
||||
</div>
|
||||
|
||||
<main class="flex min-h-0 flex-1 flex-col">
|
||||
<!-- Fallback title bar when no leftPanel is provided -->
|
||||
<slot name="contentFilter"></slot>
|
||||
<h2
|
||||
v-if="!$slots.leftPanel"
|
||||
class="text-xxl m-0 px-6 pt-2 pb-6 capitalize"
|
||||
>
|
||||
{{ contentTitle }}
|
||||
</h2>
|
||||
<div
|
||||
class="min-h-0 flex-1 px-6 pt-0 pb-10 overflow-y-auto scrollbar-custom"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</aside>
|
||||
<aside
|
||||
v-if="hasRightPanel && isRightPanelOpen"
|
||||
class="w-1/4 min-w-40 max-w-80 pt-16 pb-8"
|
||||
>
|
||||
<slot name="rightPanel"></slot>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -130,29 +102,27 @@
|
||||
<script setup lang="ts">
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
import { computed, inject, ref, useSlots, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { OnCloseKey } from '@/types/widgetTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { contentTitle, rightPanelTitle } = defineProps<{
|
||||
const { contentTitle } = defineProps<{
|
||||
contentTitle: string
|
||||
rightPanelTitle?: string
|
||||
}>()
|
||||
|
||||
const isRightPanelOpen = defineModel<boolean>('rightPanelOpen', {
|
||||
default: false
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const hasLeftPanel = computed(() => !!slots.leftPanel)
|
||||
const hasRightPanel = computed(() => !!slots.rightPanel)
|
||||
|
||||
const BREAKPOINTS = { md: 880 }
|
||||
const PANEL_SIZES = {
|
||||
width: 'w-1/3',
|
||||
minWidth: 'min-w-40',
|
||||
maxWidth: 'max-w-56'
|
||||
}
|
||||
|
||||
const slots = useSlots()
|
||||
const closeDialog = inject(OnCloseKey, () => {})
|
||||
|
||||
const breakpoints = useBreakpoints(BREAKPOINTS)
|
||||
@@ -161,6 +131,8 @@ const notMobile = breakpoints.greater('md')
|
||||
const isLeftPanelOpen = ref<boolean>(true)
|
||||
const mobileMenuOpen = ref<boolean>(false)
|
||||
|
||||
const hasRightPanel = computed(() => !!slots.rightPanel)
|
||||
|
||||
watch(notMobile, (isDesktop) => {
|
||||
if (!isDesktop) {
|
||||
mobileMenuOpen.value = false
|
||||
@@ -174,12 +146,6 @@ const showLeftPanel = computed(() => {
|
||||
return shouldShow
|
||||
})
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
gridTemplateColumns: hasRightPanel.value
|
||||
? `${hasLeftPanel.value && showLeftPanel.value ? '14rem' : '0rem'} 1fr ${isRightPanelOpen.value ? '18rem' : '0rem'}`
|
||||
: `${hasLeftPanel.value && showLeftPanel.value ? '14rem' : '0rem'} 1fr`
|
||||
}))
|
||||
|
||||
const toggleLeftPanel = () => {
|
||||
if (notMobile.value) {
|
||||
isLeftPanelOpen.value = !isLeftPanelOpen.value
|
||||
@@ -191,23 +157,6 @@ const toggleLeftPanel = () => {
|
||||
const toggleRightPanel = () => {
|
||||
isRightPanelOpen.value = !isRightPanelOpen.value
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
if (
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target instanceof HTMLSelectElement ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (isRightPanelOpen.value) {
|
||||
event.stopPropagation()
|
||||
isRightPanelOpen.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.base-widget-layout {
|
||||
@@ -222,4 +171,28 @@ function handleEscape(event: KeyboardEvent) {
|
||||
max-width: 1724px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fade transition for buttons */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Slide transition for left panel */
|
||||
.slide-panel-enter-active,
|
||||
.slide-panel-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.slide-panel-enter-from,
|
||||
.slide-panel-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
disabled: !isOverflowing,
|
||||
pt: { text: { class: 'whitespace-nowrap' } }
|
||||
}"
|
||||
class="flex cursor-pointer items-center-safe gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
|
||||
class="flex cursor-pointer items-start gap-2 rounded-md px-4 py-3 text-sm transition-colors text-base-foreground"
|
||||
:class="
|
||||
active
|
||||
? 'bg-interface-menu-component-surface-selected'
|
||||
@@ -15,32 +15,25 @@
|
||||
@mouseenter="checkOverflow"
|
||||
@click="onClick"
|
||||
>
|
||||
<NavIcon v-if="icon" :icon="icon" />
|
||||
<div v-if="icon" class="pt-0.5">
|
||||
<NavIcon :icon="icon" />
|
||||
</div>
|
||||
<i v-else class="text-neutral icon-[lucide--folder] text-xs shrink-0" />
|
||||
<span ref="textRef" class="min-w-0 truncate">
|
||||
<slot />
|
||||
<slot></slot>
|
||||
</span>
|
||||
<StatusBadge
|
||||
v-if="badge !== undefined"
|
||||
:label="String(badge)"
|
||||
severity="contrast"
|
||||
variant="circle"
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import StatusBadge from '@/components/common/StatusBadge.vue'
|
||||
import type { NavItemData } from '@/types/navTypes'
|
||||
|
||||
import NavIcon from './NavIcon.vue'
|
||||
|
||||
const { icon, badge, active, onClick } = defineProps<{
|
||||
const { icon, active, onClick } = defineProps<{
|
||||
icon: NavItemData['icon']
|
||||
badge?: NavItemData['badge']
|
||||
active?: boolean
|
||||
onClick: () => void
|
||||
}>()
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
v-for="subItem in item.items"
|
||||
:key="subItem.id"
|
||||
:icon="subItem.icon"
|
||||
:badge="subItem.badge"
|
||||
:active="activeItem === subItem.id"
|
||||
@click="activeItem = subItem.id"
|
||||
>
|
||||
@@ -33,7 +32,6 @@
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<NavItem
|
||||
:icon="item.icon"
|
||||
:badge="item.badge"
|
||||
:active="activeItem === item.id"
|
||||
@click="activeItem = item.id"
|
||||
>
|
||||
|
||||
@@ -77,7 +77,6 @@ export interface VueNodeData {
|
||||
outputs?: INodeOutputSlot[]
|
||||
resizable?: boolean
|
||||
shape?: number
|
||||
showAdvanced?: boolean
|
||||
subgraphId?: string | null
|
||||
titleMode?: TitleMode
|
||||
widgets?: SafeWidgetData[]
|
||||
@@ -315,8 +314,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
color: node.color || undefined,
|
||||
bgcolor: node.bgcolor || undefined,
|
||||
resizable: node.resizable,
|
||||
shape: node.shape,
|
||||
showAdvanced: node.showAdvanced
|
||||
shape: node.shape
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,9 +398,6 @@ 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] }
|
||||
@@ -432,7 +427,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
} else {
|
||||
// Not during workflow loading - initialize layout immediately
|
||||
// This handles individual node additions during normal operation
|
||||
requestAnimationFrame(initializeVueNodeLayout)
|
||||
initializeVueNodeLayout()
|
||||
}
|
||||
|
||||
// Call original callback if provided
|
||||
@@ -570,13 +565,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
? propertyEvent.newValue
|
||||
: undefined
|
||||
})
|
||||
break
|
||||
case 'showAdvanced':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
showAdvanced: Boolean(propertyEvent.newValue)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -305,40 +305,24 @@ describe('useJobList', () => {
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('sorts all tasks by create time', async () => {
|
||||
it('sorts all tasks by priority descending', async () => {
|
||||
queueStoreMock.pendingTasks = [
|
||||
createTask({
|
||||
promptId: 'p',
|
||||
queueIndex: 1,
|
||||
mockState: 'pending',
|
||||
createTime: 3000
|
||||
})
|
||||
createTask({ promptId: 'p', queueIndex: 1, mockState: 'pending' })
|
||||
]
|
||||
queueStoreMock.runningTasks = [
|
||||
createTask({
|
||||
promptId: 'r',
|
||||
queueIndex: 5,
|
||||
mockState: 'running',
|
||||
createTime: 2000
|
||||
})
|
||||
createTask({ promptId: 'r', queueIndex: 5, mockState: 'running' })
|
||||
]
|
||||
queueStoreMock.historyTasks = [
|
||||
createTask({
|
||||
promptId: 'h',
|
||||
queueIndex: 3,
|
||||
mockState: 'completed',
|
||||
createTime: 1000,
|
||||
executionEndTimestamp: 5000
|
||||
})
|
||||
createTask({ promptId: 'h', queueIndex: 3, mockState: 'completed' })
|
||||
]
|
||||
|
||||
const { allTasksSorted } = initComposable()
|
||||
await flush()
|
||||
|
||||
expect(allTasksSorted.value.map((task) => task.promptId)).toEqual([
|
||||
'p',
|
||||
'r',
|
||||
'h'
|
||||
'h',
|
||||
'p'
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -197,18 +197,13 @@ export function useJobList() {
|
||||
const selectedWorkflowFilter = ref<'all' | 'current'>('all')
|
||||
const selectedSortMode = ref<JobSortMode>('mostRecent')
|
||||
|
||||
const mostRecentTimestamp = (task: TaskItemImpl) => task.createTime ?? 0
|
||||
|
||||
const allTasksSorted = computed<TaskItemImpl[]>(() => {
|
||||
const all = [
|
||||
...queueStore.pendingTasks,
|
||||
...queueStore.runningTasks,
|
||||
...queueStore.historyTasks
|
||||
]
|
||||
return all.sort((a, b) => {
|
||||
const delta = mostRecentTimestamp(b) - mostRecentTimestamp(a)
|
||||
return delta === 0 ? 0 : delta
|
||||
})
|
||||
return all.sort((a, b) => b.queueIndex - a.queueIndex)
|
||||
})
|
||||
|
||||
const tasksWithJobState = computed<TaskWithState[]>(() =>
|
||||
|
||||
@@ -5,10 +5,6 @@ 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)
|
||||
@@ -59,8 +55,7 @@ const workflowStoreMock = {
|
||||
createTemporary: vi.fn()
|
||||
}
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => workflowStoreMock,
|
||||
ComfyWorkflow: class {}
|
||||
useWorkflowStore: () => workflowStoreMock
|
||||
}))
|
||||
|
||||
const interruptMock = vi.fn()
|
||||
@@ -109,13 +104,6 @@ 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,7 +6,6 @@ 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'
|
||||
@@ -16,7 +15,6 @@ 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'
|
||||
@@ -46,7 +44,6 @@ export function useJobMenu(
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const litegraphService = useLitegraphService()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
@@ -75,15 +72,10 @@ export function useJobMenu(
|
||||
const target = resolveItem(item)
|
||||
if (!target) return
|
||||
if (target.state === 'running' || target.state === 'initialization') {
|
||||
if (isCloud) {
|
||||
await api.deleteItem('queue', target.id)
|
||||
} else {
|
||||
await api.interrupt(target.id)
|
||||
}
|
||||
await api.interrupt(target.id)
|
||||
} else if (target.state === 'pending') {
|
||||
await api.deleteItem('queue', target.id)
|
||||
}
|
||||
executionStore.clearInitializationByPromptId(target.id)
|
||||
await queueStore.update()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 => {
|
||||
@@ -12,12 +11,6 @@ export const useAssetsSidebarTab = (): SidebarTabExtension => {
|
||||
tooltip: 'sideToolbar.assets',
|
||||
label: 'sideToolbar.labels.assets',
|
||||
component: markRaw(AssetsSidebarTab),
|
||||
type: 'vue',
|
||||
iconBadge: () => {
|
||||
const queueStore = useQueueStore()
|
||||
return queueStore.pendingTasks.length > 0
|
||||
? queueStore.pendingTasks.length.toString()
|
||||
: null
|
||||
}
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,8 @@ 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,6 +1,5 @@
|
||||
import { computed, reactive, readonly } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { remoteConfig } from '@/platform/remoteConfig/remoteConfig'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
@@ -96,8 +95,6 @@ export function useFeatureFlags() {
|
||||
)
|
||||
},
|
||||
get teamWorkspacesEnabled() {
|
||||
if (!isCloud) return false
|
||||
|
||||
return (
|
||||
remoteConfig.value.team_workspaces_enabled ??
|
||||
api.getServerFeature(ServerFeatureFlag.TEAM_WORKSPACES_ENABLED, false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isCloud, isNightly } from '@/platform/distribution/types'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
import './clipspace'
|
||||
import './contextMenuFilter'
|
||||
@@ -38,8 +38,3 @@ if (isCloud) {
|
||||
await import('./cloudSubscription')
|
||||
}
|
||||
}
|
||||
|
||||
// Nightly-only extensions
|
||||
if (isNightly && !isCloud) {
|
||||
await import('./nightlyBadges')
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { t } from '@/i18n'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import type { TopbarBadge } from '@/types/comfy'
|
||||
|
||||
const badges: TopbarBadge[] = [
|
||||
{
|
||||
text: t('nightly.badge.label'),
|
||||
label: t('g.nightly'),
|
||||
variant: 'warning',
|
||||
tooltip: t('nightly.badge.tooltip')
|
||||
}
|
||||
]
|
||||
|
||||
useExtensionService().registerExtension({
|
||||
name: 'Comfy.Nightly.Badges',
|
||||
topbarBadges: badges
|
||||
})
|
||||
@@ -75,9 +75,7 @@ useExtensionService().registerExtension({
|
||||
|
||||
for (const previewWidget of previewWidgets) {
|
||||
const text = message.text ?? ''
|
||||
previewWidget.value = Array.isArray(text)
|
||||
? (text?.join('\n\n') ?? '')
|
||||
: text
|
||||
previewWidget.value = Array.isArray(text) ? (text[0] ?? '') : text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recorder'
|
||||
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
@@ -24,17 +25,6 @@ import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
import { api } from '../../scripts/api'
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
function updateUIWidget(
|
||||
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
|
||||
url: string = ''
|
||||
) {
|
||||
audioUIWidget.element.src = url
|
||||
audioUIWidget.value = url
|
||||
audioUIWidget.callback?.(url)
|
||||
if (url) audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
else audioUIWidget.element.classList.add('empty-audio-widget')
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
audioWidget: IStringWidget,
|
||||
audioUIWidget: DOMWidget<HTMLAudioElement, string>,
|
||||
@@ -65,10 +55,10 @@ async function uploadFile(
|
||||
}
|
||||
|
||||
if (updateNode) {
|
||||
updateUIWidget(
|
||||
audioUIWidget,
|
||||
api.apiURL(getResourceURL(...splitFilePath(path)))
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(...splitFilePath(path))
|
||||
)
|
||||
audioWidget.value = path
|
||||
|
||||
// Manually trigger the callback to update VueNodes
|
||||
audioWidget.callback?.(path)
|
||||
@@ -128,18 +118,26 @@ app.registerExtension({
|
||||
const audios = output.audio
|
||||
if (!audios?.length) return
|
||||
const audio = audios[0]
|
||||
const resourceUrl = getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
)
|
||||
)
|
||||
updateUIWidget(audioUIWidget, api.apiURL(resourceUrl))
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
}
|
||||
|
||||
let value = ''
|
||||
audioUIWidget.options.getValue = () => value
|
||||
audioUIWidget.options.setValue = (v) => (value = v)
|
||||
audioUIWidget.onRemove = useChainCallback(
|
||||
audioUIWidget.onRemove,
|
||||
() => {
|
||||
if (!audioUIWidget.element) return
|
||||
audioUIWidget.element.pause()
|
||||
audioUIWidget.element.src = ''
|
||||
audioUIWidget.element.remove()
|
||||
}
|
||||
)
|
||||
|
||||
return { widget: audioUIWidget }
|
||||
}
|
||||
@@ -158,12 +156,10 @@ app.registerExtension({
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
const audio = output.audio[0]
|
||||
const resourceUrl = getResourceURL(
|
||||
audio.subfolder ?? '',
|
||||
audio.filename ?? '',
|
||||
audio.type
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(audio.subfolder ?? '', audio.filename ?? '', audio.type)
|
||||
)
|
||||
updateUIWidget(audioUIWidget, api.apiURL(resourceUrl))
|
||||
audioUIWidget.element.classList.remove('empty-audio-widget')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -187,18 +183,18 @@ app.registerExtension({
|
||||
const audioUIWidget = node.widgets.find(
|
||||
(w) => w.name === 'audioUI'
|
||||
) as unknown as DOMWidget<HTMLAudioElement, string>
|
||||
audioUIWidget.options.canvasOnly = true
|
||||
|
||||
const onAudioWidgetUpdate = () => {
|
||||
updateUIWidget(
|
||||
audioUIWidget,
|
||||
api.apiURL(
|
||||
getResourceURL(...splitFilePath(audioWidget.value ?? ''))
|
||||
)
|
||||
if (typeof audioWidget.value !== 'string') return
|
||||
audioUIWidget.element.src = api.apiURL(
|
||||
getResourceURL(...splitFilePath(audioWidget.value))
|
||||
)
|
||||
}
|
||||
// Initially load default audio file to audioUIWidget.
|
||||
onAudioWidgetUpdate()
|
||||
|
||||
if (audioWidget.value) {
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
audioWidget.callback = onAudioWidgetUpdate
|
||||
|
||||
// Load saved audio file widget values if restoring from workflow
|
||||
@@ -206,7 +202,9 @@ app.registerExtension({
|
||||
node.onGraphConfigured = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
onGraphConfigured?.apply(this, arguments)
|
||||
onAudioWidgetUpdate()
|
||||
if (audioWidget.value) {
|
||||
onAudioWidgetUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (files: File[]) => {
|
||||
@@ -330,7 +328,7 @@ app.registerExtension({
|
||||
URL.revokeObjectURL(audioUIWidget.element.src)
|
||||
}
|
||||
|
||||
updateUIWidget(audioUIWidget, URL.createObjectURL(audioBlob))
|
||||
audioUIWidget.element.src = URL.createObjectURL(audioBlob)
|
||||
|
||||
isRecording = false
|
||||
|
||||
|
||||
@@ -1350,12 +1350,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
})
|
||||
|
||||
function inner_clicked(
|
||||
this: ContextMenuDivElement,
|
||||
this: ContextMenu<string>,
|
||||
v?: string | IContextMenuValue<string>
|
||||
) {
|
||||
if (!node || typeof v === 'string' || !v?.value) return
|
||||
|
||||
const rect = this.getBoundingClientRect()
|
||||
const rect = this.root.getBoundingClientRect()
|
||||
canvas.showEditPropertyValue(node, v.value, {
|
||||
position: [rect.left, rect.top]
|
||||
})
|
||||
|
||||
@@ -13,9 +13,7 @@ const DEFAULT_TRACKED_PROPERTIES: string[] = [
|
||||
'flags.pinned',
|
||||
'mode',
|
||||
'color',
|
||||
'bgcolor',
|
||||
'shape',
|
||||
'showAdvanced'
|
||||
'bgcolor'
|
||||
]
|
||||
/**
|
||||
* Manages node properties with optional change tracking and instrumentation.
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"assets": "الأصول",
|
||||
"baseModels": "النماذج الأساسية",
|
||||
"browseAssets": "تصفح الأصول",
|
||||
"byType": "حسب النوع",
|
||||
"checkpoints": "نقاط التحقق",
|
||||
"civitaiLinkExample": "{example} {link}",
|
||||
"civitaiLinkExampleStrong": "مثال:",
|
||||
@@ -46,10 +45,6 @@
|
||||
"failed": "فشل التنزيل",
|
||||
"inProgress": "جاري تنزيل {assetName}..."
|
||||
},
|
||||
"emptyImported": {
|
||||
"canImport": "لا توجد نماذج مستوردة بعد. انقر على \"استيراد نموذج\" لإضافة نموذجك الخاص.",
|
||||
"restricted": "النماذج الشخصية متاحة فقط لمستوى Creator وما فوق."
|
||||
},
|
||||
"errorFileTooLarge": "الملف يتجاوز الحد الأقصى المسموح به للحجم",
|
||||
"errorFormatNotAllowed": "يسمح فقط بصيغة SafeTensor",
|
||||
"errorModelTypeNotSupported": "نوع النموذج هذا غير مدعوم",
|
||||
@@ -66,7 +61,6 @@
|
||||
"finish": "إنهاء",
|
||||
"genericLinkPlaceholder": "الصق الرابط هنا",
|
||||
"importAnother": "استيراد آخر",
|
||||
"imported": "مستوردة",
|
||||
"jobId": "معرّف المهمة",
|
||||
"loadingModels": "جارٍ تحميل {type}...",
|
||||
"maxFileSize": "الحد الأقصى لحجم الملف: {size}",
|
||||
@@ -76,29 +70,6 @@
|
||||
"threeDModelPlaceholder": "نموذج ثلاثي الأبعاد"
|
||||
},
|
||||
"modelAssociatedWithLink": "النموذج المرتبط بالرابط الذي قدمته:",
|
||||
"modelInfo": {
|
||||
"addBaseModel": "أضف نموذجًا أساسيًا...",
|
||||
"addTag": "أضف وسمًا...",
|
||||
"additionalTags": "وسوم إضافية",
|
||||
"baseModelUnknown": "النموذج الأساسي غير معروف",
|
||||
"basicInfo": "معلومات أساسية",
|
||||
"compatibleBaseModels": "نماذج أساسية متوافقة",
|
||||
"description": "الوصف",
|
||||
"descriptionNotSet": "لم يتم تعيين وصف",
|
||||
"descriptionPlaceholder": "أضف وصفًا لهذا النموذج...",
|
||||
"displayName": "اسم العرض",
|
||||
"fileName": "اسم الملف",
|
||||
"modelDescription": "وصف النموذج",
|
||||
"modelTagging": "تصنيف النموذج",
|
||||
"modelType": "نوع النموذج",
|
||||
"noAdditionalTags": "لا توجد وسوم إضافية",
|
||||
"selectModelPrompt": "اختر نموذجًا لعرض معلوماته",
|
||||
"selectModelType": "اختر نوع النموذج...",
|
||||
"source": "المصدر",
|
||||
"title": "معلومات النموذج",
|
||||
"triggerPhrases": "عبارات التفعيل",
|
||||
"viewOnSource": "عرض على {source}"
|
||||
},
|
||||
"modelName": "اسم النموذج",
|
||||
"modelNamePlaceholder": "أدخل اسمًا لهذا النموذج",
|
||||
"modelTypeSelectorLabel": "ما نوع هذا النموذج؟",
|
||||
@@ -713,7 +684,6 @@
|
||||
"clearAll": "مسح الكل",
|
||||
"clearFilters": "مسح الفلاتر",
|
||||
"close": "إغلاق",
|
||||
"closeDialog": "إغلاق الحوار",
|
||||
"color": "اللون",
|
||||
"comfy": "Comfy",
|
||||
"comfyOrgLogoAlt": "شعار ComfyOrg",
|
||||
@@ -792,8 +762,6 @@
|
||||
"goToNode": "الانتقال إلى العقدة",
|
||||
"graphNavigation": "التنقل في الرسم البياني",
|
||||
"halfSpeed": "0.5x",
|
||||
"hideLeftPanel": "إخفاء اللوحة اليسرى",
|
||||
"hideRightPanel": "إخفاء اللوحة اليمنى",
|
||||
"icon": "أيقونة",
|
||||
"imageFailedToLoad": "فشل تحميل الصورة",
|
||||
"imagePreview": "معاينة الصورة - استخدم مفاتيح الأسهم للتنقل بين الصور",
|
||||
@@ -835,7 +803,6 @@
|
||||
"name": "الاسم",
|
||||
"newFolder": "مجلد جديد",
|
||||
"next": "التالي",
|
||||
"nightly": "NIGHTLY",
|
||||
"no": "لا",
|
||||
"noAudioRecorded": "لم يتم تسجيل أي صوت",
|
||||
"noItems": "لا توجد عناصر",
|
||||
@@ -850,7 +817,6 @@
|
||||
"nodeSlotsError": "خطأ في فتحات العقدة",
|
||||
"nodeWidgetsError": "خطأ في عناصر واجهة العقدة",
|
||||
"nodes": "العُقَد",
|
||||
"nodesCount": "{count} عقدة | {count} عقدة | {count} عقدة",
|
||||
"nodesRunning": "العُقَد قيد التشغيل",
|
||||
"none": "لا شيء",
|
||||
"nothingToCopy": "لا يوجد ما يمكن نسخه",
|
||||
@@ -925,9 +891,7 @@
|
||||
"selectedFile": "الملف المحدد",
|
||||
"setAsBackground": "تعيين كخلفية",
|
||||
"settings": "الإعدادات",
|
||||
"showLeftPanel": "إظهار اللوحة اليسرى",
|
||||
"showReport": "عرض التقرير",
|
||||
"showRightPanel": "إظهار اللوحة اليمنى",
|
||||
"singleSelectDropdown": "قائمة منسدلة اختيار واحد",
|
||||
"sort": "فرز",
|
||||
"source": "المصدر",
|
||||
@@ -950,7 +914,6 @@
|
||||
"updating": "جارٍ التحديث",
|
||||
"upload": "رفع",
|
||||
"usageHint": "تلميح الاستخدام",
|
||||
"use": "استخدم",
|
||||
"user": "المستخدم",
|
||||
"versionMismatchWarning": "تحذير توافق الإصدارات",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} زر https://docs.comfy.org/installation/update_comfyui#common-update-issues للحصول على تعليمات التحديث.",
|
||||
@@ -1654,18 +1617,11 @@
|
||||
"title": "سير العمل هذا يحتوي على عقد مفقودة"
|
||||
}
|
||||
},
|
||||
"nightly": {
|
||||
"badge": {
|
||||
"label": "إصدار معاينة",
|
||||
"tooltip": "أنت تستخدم إصدارًا ليليًا من ComfyUI. يرجى استخدام زر الملاحظات لمشاركة آرائك حول هذه الميزات."
|
||||
}
|
||||
},
|
||||
"nodeCategories": {
|
||||
"": "",
|
||||
"3d": "ثلاثي الأبعاد",
|
||||
"3d_models": "نماذج ثلاثية الأبعاد",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "بايت دانس",
|
||||
"Gemini": "جيميني",
|
||||
"Ideogram": "إيديوغرام",
|
||||
@@ -1687,7 +1643,6 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "فيدو",
|
||||
"Wan": "وان",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_للاختبار",
|
||||
"advanced": "متقدم",
|
||||
"animation": "الرسوم المتحركة",
|
||||
@@ -2174,14 +2129,12 @@
|
||||
"viewControls": "عناصر تحكم العرض"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"activeJobStatus": "المهمة النشطة: {status}",
|
||||
"assets": "الأصول",
|
||||
"backToAssets": "العودة إلى جميع الأصول",
|
||||
"browseTemplates": "تصفح القوالب المثال",
|
||||
"downloads": "التنزيلات",
|
||||
"generatedAssetsHeader": "الأصول المُولدة",
|
||||
"helpCenter": "مركز المساعدة",
|
||||
"importedAssetsHeader": "الأصول المستوردة",
|
||||
"labels": {
|
||||
"assets": "الأصول",
|
||||
"console": "وحدة التحكم",
|
||||
@@ -2226,7 +2179,6 @@
|
||||
"queue": "قائمة الانتظار",
|
||||
"queueProgressOverlay": {
|
||||
"activeJobs": "{count} مهمة نشطة | {count} مهام نشطة",
|
||||
"activeJobsShort": "{count} نشط | {count} نشط",
|
||||
"activeJobsSuffix": "مهام نشطة",
|
||||
"cancelJobTooltip": "إلغاء المهمة",
|
||||
"clearHistory": "مسح سجل قائمة الانتظار",
|
||||
|
||||
@@ -328,68 +328,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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 إلى فيديو",
|
||||
@@ -13512,40 +13450,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -16233,43 +16137,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -100,11 +100,6 @@
|
||||
"no": "No",
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"closeDialog": "Close dialog",
|
||||
"showLeftPanel": "Show left panel",
|
||||
"hideLeftPanel": "Hide left panel",
|
||||
"showRightPanel": "Show right panel",
|
||||
"hideRightPanel": "Hide right panel",
|
||||
"or": "or",
|
||||
"pressKeysForNewBinding": "Press keys for new binding",
|
||||
"defaultBanner": "default banner",
|
||||
@@ -163,7 +158,6 @@
|
||||
"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",
|
||||
@@ -184,7 +178,6 @@
|
||||
"source": "Source",
|
||||
"filter": "Filter",
|
||||
"apply": "Apply",
|
||||
"use": "Use",
|
||||
"enabled": "Enabled",
|
||||
"installed": "Installed",
|
||||
"restart": "Restart",
|
||||
@@ -276,7 +269,6 @@
|
||||
"1x": "1x",
|
||||
"2x": "2x",
|
||||
"beta": "BETA",
|
||||
"nightly": "NIGHTLY",
|
||||
"profile": "Profile",
|
||||
"noItems": "No items"
|
||||
},
|
||||
@@ -710,8 +702,6 @@
|
||||
"noImportedFiles": "No imported files found",
|
||||
"noGeneratedFiles": "No generated files found",
|
||||
"generatedAssetsHeader": "Generated assets",
|
||||
"importedAssetsHeader": "Imported assets",
|
||||
"activeJobStatus": "Active job: {status}",
|
||||
"noFilesFoundMessage": "Upload files or generate content to see them here",
|
||||
"browseTemplates": "Browse example templates",
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
@@ -761,7 +751,6 @@
|
||||
"sortJobs": "Sort jobs",
|
||||
"sortBy": "Sort by",
|
||||
"activeJobs": "{count} active job | {count} active jobs",
|
||||
"activeJobsShort": "{count} active | {count} active",
|
||||
"activeJobsSuffix": "active jobs",
|
||||
"jobQueue": "Job Queue",
|
||||
"expandCollapsedQueue": "Expand job queue",
|
||||
@@ -1437,7 +1426,6 @@
|
||||
"latent": "latent",
|
||||
"mask": "mask",
|
||||
"api node": "api node",
|
||||
"Bria": "Bria",
|
||||
"video": "video",
|
||||
"ByteDance": "ByteDance",
|
||||
"preprocessors": "preprocessors",
|
||||
@@ -1518,7 +1506,6 @@
|
||||
"": "",
|
||||
"camera": "camera",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"zimage": "zimage"
|
||||
},
|
||||
"dataTypes": {
|
||||
@@ -2319,12 +2306,6 @@
|
||||
"assetBrowser": {
|
||||
"allCategory": "All {category}",
|
||||
"allModels": "All Models",
|
||||
"byType": "By type",
|
||||
"emptyImported": {
|
||||
"canImport": "No imported models yet. Click \"Import Model\" to add your own.",
|
||||
"restricted": "Personal models are only available at Creator tier and above."
|
||||
},
|
||||
"imported": "Imported",
|
||||
"assetCollection": "Asset collection",
|
||||
"assets": "Assets",
|
||||
"baseModels": "Base models",
|
||||
@@ -2415,29 +2396,6 @@
|
||||
"assetCard": "{name} - {type} asset",
|
||||
"loadingAsset": "Loading asset"
|
||||
},
|
||||
"modelInfo": {
|
||||
"title": "Model Info",
|
||||
"selectModelPrompt": "Select a model to see its information",
|
||||
"basicInfo": "Basic Info",
|
||||
"displayName": "Display Name",
|
||||
"fileName": "File Name",
|
||||
"source": "Source",
|
||||
"viewOnSource": "View on {source}",
|
||||
"modelTagging": "Model Tagging",
|
||||
"modelType": "Model Type",
|
||||
"selectModelType": "Select model type...",
|
||||
"compatibleBaseModels": "Compatible Base Models",
|
||||
"addBaseModel": "Add base model...",
|
||||
"baseModelUnknown": "Base model unknown",
|
||||
"additionalTags": "Additional Tags",
|
||||
"addTag": "Add tag...",
|
||||
"noAdditionalTags": "No additional tags",
|
||||
"modelDescription": "Model Description",
|
||||
"triggerPhrases": "Trigger Phrases",
|
||||
"description": "Description",
|
||||
"descriptionNotSet": "No description set",
|
||||
"descriptionPlaceholder": "Add a description for this model..."
|
||||
},
|
||||
"media": {
|
||||
"threeDModelPlaceholder": "3D Model",
|
||||
"audioPlaceholder": "Audio"
|
||||
@@ -2660,11 +2618,5 @@
|
||||
"workspaceNotFound": "Workspace not found",
|
||||
"tokenExchangeFailed": "Failed to authenticate with workspace: {error}"
|
||||
}
|
||||
},
|
||||
"nightly": {
|
||||
"badge": {
|
||||
"label": "Preview Version",
|
||||
"tooltip": "You are using a nightly version of ComfyUI. Please use the feedback button to share your thoughts about these features."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,68 +328,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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.",
|
||||
@@ -13531,40 +13469,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -16295,43 +16199,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"assets": "Recursos",
|
||||
"baseModels": "Modelos base",
|
||||
"browseAssets": "Explorar recursos",
|
||||
"byType": "Por tipo",
|
||||
"checkpoints": "Checkpoints",
|
||||
"civitaiLinkExample": "{example} {link}",
|
||||
"civitaiLinkExampleStrong": "Ejemplo:",
|
||||
@@ -46,10 +45,6 @@
|
||||
"failed": "La descarga falló",
|
||||
"inProgress": "Descargando {assetName}..."
|
||||
},
|
||||
"emptyImported": {
|
||||
"canImport": "Aún no hay modelos importados. Haz clic en \"Importar modelo\" para añadir el tuyo.",
|
||||
"restricted": "Los modelos personales solo están disponibles en el nivel Creador o superior."
|
||||
},
|
||||
"errorFileTooLarge": "El archivo excede el tamaño máximo permitido",
|
||||
"errorFormatNotAllowed": "Solo se permite el formato SafeTensor",
|
||||
"errorModelTypeNotSupported": "Este tipo de modelo no es compatible",
|
||||
@@ -66,7 +61,6 @@
|
||||
"finish": "Finalizar",
|
||||
"genericLinkPlaceholder": "Pega el enlace aquí",
|
||||
"importAnother": "Importar otro",
|
||||
"imported": "Importado",
|
||||
"jobId": "ID de tarea",
|
||||
"loadingModels": "Cargando {type}...",
|
||||
"maxFileSize": "Tamaño máximo de archivo: {size}",
|
||||
@@ -76,29 +70,6 @@
|
||||
"threeDModelPlaceholder": "Modelo 3D"
|
||||
},
|
||||
"modelAssociatedWithLink": "El modelo asociado con el enlace que proporcionaste:",
|
||||
"modelInfo": {
|
||||
"addBaseModel": "Agregar modelo base...",
|
||||
"addTag": "Agregar etiqueta...",
|
||||
"additionalTags": "Etiquetas adicionales",
|
||||
"baseModelUnknown": "Modelo base desconocido",
|
||||
"basicInfo": "Información básica",
|
||||
"compatibleBaseModels": "Modelos base compatibles",
|
||||
"description": "Descripción",
|
||||
"descriptionNotSet": "Sin descripción",
|
||||
"descriptionPlaceholder": "Agrega una descripción para este modelo...",
|
||||
"displayName": "Nombre para mostrar",
|
||||
"fileName": "Nombre de archivo",
|
||||
"modelDescription": "Descripción del modelo",
|
||||
"modelTagging": "Etiquetado del modelo",
|
||||
"modelType": "Tipo de modelo",
|
||||
"noAdditionalTags": "Sin etiquetas adicionales",
|
||||
"selectModelPrompt": "Selecciona un modelo para ver su información",
|
||||
"selectModelType": "Selecciona el tipo de modelo...",
|
||||
"source": "Fuente",
|
||||
"title": "Información del modelo",
|
||||
"triggerPhrases": "Frases de activación",
|
||||
"viewOnSource": "Ver en {source}"
|
||||
},
|
||||
"modelName": "Nombre del modelo",
|
||||
"modelNamePlaceholder": "Introduce un nombre para este modelo",
|
||||
"modelTypeSelectorLabel": "¿Qué tipo de modelo es este?",
|
||||
@@ -713,7 +684,6 @@
|
||||
"clearAll": "Borrar todo",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"close": "Cerrar",
|
||||
"closeDialog": "Cerrar diálogo",
|
||||
"color": "Color",
|
||||
"comfy": "Comfy",
|
||||
"comfyOrgLogoAlt": "Logo de ComfyOrg",
|
||||
@@ -792,8 +762,6 @@
|
||||
"goToNode": "Ir al nodo",
|
||||
"graphNavigation": "Navegación de gráficos",
|
||||
"halfSpeed": "0.5x",
|
||||
"hideLeftPanel": "Ocultar panel izquierdo",
|
||||
"hideRightPanel": "Ocultar panel derecho",
|
||||
"icon": "Icono",
|
||||
"imageFailedToLoad": "Falló la carga de la imagen",
|
||||
"imagePreview": "Vista previa de imagen - Usa las teclas de flecha para navegar entre imágenes",
|
||||
@@ -835,7 +803,6 @@
|
||||
"name": "Nombre",
|
||||
"newFolder": "Nueva carpeta",
|
||||
"next": "Siguiente",
|
||||
"nightly": "NIGHTLY",
|
||||
"no": "No",
|
||||
"noAudioRecorded": "No se grabó audio",
|
||||
"noItems": "Sin elementos",
|
||||
@@ -850,7 +817,6 @@
|
||||
"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",
|
||||
@@ -925,9 +891,7 @@
|
||||
"selectedFile": "Archivo seleccionado",
|
||||
"setAsBackground": "Establecer como fondo",
|
||||
"settings": "Configuraciones",
|
||||
"showLeftPanel": "Mostrar panel izquierdo",
|
||||
"showReport": "Mostrar informe",
|
||||
"showRightPanel": "Mostrar panel derecho",
|
||||
"singleSelectDropdown": "Menú desplegable de selección única",
|
||||
"sort": "Ordenar",
|
||||
"source": "Fuente",
|
||||
@@ -950,7 +914,6 @@
|
||||
"updating": "Actualizando",
|
||||
"upload": "Subir",
|
||||
"usageHint": "Sugerencia de uso",
|
||||
"use": "Usar",
|
||||
"user": "Usuario",
|
||||
"versionMismatchWarning": "Advertencia de compatibilidad de versión",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} Visita https://docs.comfy.org/installation/update_comfyui#common-update-issues para obtener instrucciones de actualización.",
|
||||
@@ -1654,18 +1617,11 @@
|
||||
"title": "Este flujo de trabajo tiene nodos faltantes"
|
||||
}
|
||||
},
|
||||
"nightly": {
|
||||
"badge": {
|
||||
"label": "Versión preliminar",
|
||||
"tooltip": "Estás usando una versión nightly de ComfyUI. Por favor, utiliza el botón de comentarios para compartir tus opiniones sobre estas funciones."
|
||||
}
|
||||
},
|
||||
"nodeCategories": {
|
||||
"": "",
|
||||
"3d": "3d",
|
||||
"3d_models": "modelos_3d",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "ByteDance",
|
||||
"Gemini": "Gemini",
|
||||
"Ideogram": "Ideogram",
|
||||
@@ -1687,7 +1643,6 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_para_pruebas",
|
||||
"advanced": "avanzado",
|
||||
"animation": "animación",
|
||||
@@ -2174,14 +2129,12 @@
|
||||
"viewControls": "Controles de vista"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"activeJobStatus": "Trabajo activo: {status}",
|
||||
"assets": "Recursos",
|
||||
"backToAssets": "Volver a todos los recursos",
|
||||
"browseTemplates": "Explorar plantillas de ejemplo",
|
||||
"downloads": "Descargas",
|
||||
"generatedAssetsHeader": "Recursos generados",
|
||||
"helpCenter": "Centro de ayuda",
|
||||
"importedAssetsHeader": "Recursos importados",
|
||||
"labels": {
|
||||
"assets": "Recursos",
|
||||
"console": "Consola",
|
||||
@@ -2226,7 +2179,6 @@
|
||||
"queue": "Cola",
|
||||
"queueProgressOverlay": {
|
||||
"activeJobs": "{count} trabajo activo | {count} trabajos activos",
|
||||
"activeJobsShort": "{count} activo(s) | {count} activo(s)",
|
||||
"activeJobsSuffix": "trabajos activos",
|
||||
"cancelJobTooltip": "Cancelar trabajo",
|
||||
"clearHistory": "Limpiar historial de la cola de trabajos",
|
||||
|
||||
@@ -328,68 +328,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -13512,40 +13450,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -16233,43 +16137,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"assets": "داراییها",
|
||||
"baseModels": "مدلهای پایه",
|
||||
"browseAssets": "مرور داراییها",
|
||||
"byType": "بر اساس نوع",
|
||||
"checkpoints": "چکپوینتها",
|
||||
"civitaiLinkExample": "{example} {link}",
|
||||
"civitaiLinkExampleStrong": "مثال:",
|
||||
@@ -46,10 +45,6 @@
|
||||
"failed": "دانلود ناموفق بود",
|
||||
"inProgress": "در حال دانلود {assetName}..."
|
||||
},
|
||||
"emptyImported": {
|
||||
"canImport": "هنوز مدلی وارد نشده است. برای افزودن مدل خود، روی «وارد کردن مدل» کلیک کنید.",
|
||||
"restricted": "مدلهای شخصی فقط برای سطح Creator و بالاتر در دسترس هستند."
|
||||
},
|
||||
"errorFileTooLarge": "فایل از حداکثر اندازه مجاز بزرگتر است",
|
||||
"errorFormatNotAllowed": "فقط فرمت SafeTensor مجاز است",
|
||||
"errorModelTypeNotSupported": "این نوع مدل پشتیبانی نمیشود",
|
||||
@@ -66,7 +61,6 @@
|
||||
"finish": "پایان",
|
||||
"genericLinkPlaceholder": "لینک را اینجا وارد کنید",
|
||||
"importAnother": "وارد کردن مورد دیگر",
|
||||
"imported": "وارد شده",
|
||||
"jobId": "شناسه کار: {jobId}",
|
||||
"loadingModels": "در حال بارگذاری {type}...",
|
||||
"maxFileSize": "حداکثر اندازه فایل: {size}",
|
||||
@@ -76,29 +70,6 @@
|
||||
"threeDModelPlaceholder": "مدل سهبعدی"
|
||||
},
|
||||
"modelAssociatedWithLink": "مدل مرتبط با لینکی که وارد کردید:",
|
||||
"modelInfo": {
|
||||
"addBaseModel": "افزودن مدل پایه...",
|
||||
"addTag": "افزودن برچسب...",
|
||||
"additionalTags": "برچسبهای اضافی",
|
||||
"baseModelUnknown": "مدل پایه نامشخص",
|
||||
"basicInfo": "اطلاعات پایه",
|
||||
"compatibleBaseModels": "مدلهای پایه سازگار",
|
||||
"description": "توضیحات",
|
||||
"descriptionNotSet": "توضیحی تنظیم نشده است",
|
||||
"descriptionPlaceholder": "یک توضیح برای این مدل اضافه کنید...",
|
||||
"displayName": "نام نمایشی",
|
||||
"fileName": "نام فایل",
|
||||
"modelDescription": "توضیحات مدل",
|
||||
"modelTagging": "برچسبگذاری مدل",
|
||||
"modelType": "نوع مدل",
|
||||
"noAdditionalTags": "برچسب اضافی وجود ندارد",
|
||||
"selectModelPrompt": "یک مدل را برای مشاهده اطلاعات آن انتخاب کنید",
|
||||
"selectModelType": "انتخاب نوع مدل...",
|
||||
"source": "منبع",
|
||||
"title": "اطلاعات مدل",
|
||||
"triggerPhrases": "عبارات فعالساز",
|
||||
"viewOnSource": "مشاهده در {source}"
|
||||
},
|
||||
"modelName": "نام مدل",
|
||||
"modelNamePlaceholder": "یک نام برای این مدل وارد کنید",
|
||||
"modelTypeSelectorLabel": "نوع مدل چیست؟",
|
||||
@@ -713,7 +684,6 @@
|
||||
"clearAll": "پاکسازی همه",
|
||||
"clearFilters": "پاکسازی فیلترها",
|
||||
"close": "بستن",
|
||||
"closeDialog": "بستن پنجره",
|
||||
"color": "رنگ",
|
||||
"comfy": "Comfy",
|
||||
"comfyOrgLogoAlt": "لوگوی ComfyOrg",
|
||||
@@ -792,8 +762,6 @@
|
||||
"goToNode": "رفتن به node",
|
||||
"graphNavigation": "ناوبری گراف",
|
||||
"halfSpeed": "۰.۵x",
|
||||
"hideLeftPanel": "پنهان کردن پنل چپ",
|
||||
"hideRightPanel": "پنهان کردن پنل راست",
|
||||
"icon": "آیکون",
|
||||
"imageFailedToLoad": "بارگذاری تصویر ناموفق بود",
|
||||
"imagePreview": "پیشنمایش تصویر - برای جابجایی بین تصاویر از کلیدهای جهتدار استفاده کنید",
|
||||
@@ -835,7 +803,6 @@
|
||||
"name": "نام",
|
||||
"newFolder": "پوشه جدید",
|
||||
"next": "بعدی",
|
||||
"nightly": "نسخه شبانه",
|
||||
"no": "خیر",
|
||||
"noAudioRecorded": "هیچ صدایی ضبط نشد",
|
||||
"noItems": "هیچ موردی وجود ندارد",
|
||||
@@ -850,7 +817,6 @@
|
||||
"nodeSlotsError": "خطا در slotهای node",
|
||||
"nodeWidgetsError": "خطا در ابزارکهای node",
|
||||
"nodes": "nodeها",
|
||||
"nodesCount": "{count} نود | {count} نود | {count} نود",
|
||||
"nodesRunning": "nodeها در حال اجرا هستند",
|
||||
"none": "هیچکدام",
|
||||
"nothingToCopy": "موردی برای کپی وجود ندارد",
|
||||
@@ -925,9 +891,7 @@
|
||||
"selectedFile": "فایل انتخابشده",
|
||||
"setAsBackground": "تنظیم به عنوان پسزمینه",
|
||||
"settings": "تنظیمات",
|
||||
"showLeftPanel": "نمایش پنل چپ",
|
||||
"showReport": "نمایش گزارش",
|
||||
"showRightPanel": "نمایش پنل راست",
|
||||
"singleSelectDropdown": "لیست کشویی تکانتخابی",
|
||||
"sort": "مرتبسازی",
|
||||
"source": "منبع",
|
||||
@@ -950,7 +914,6 @@
|
||||
"updating": "در حال بهروزرسانی {id}",
|
||||
"upload": "بارگذاری",
|
||||
"usageHint": "راهنمای استفاده",
|
||||
"use": "استفاده",
|
||||
"user": "کاربر",
|
||||
"versionMismatchWarning": "هشدار ناسازگاری نسخه",
|
||||
"versionMismatchWarningMessage": "{warning}: {detail} برای راهنمای بهروزرسانی به https://docs.comfy.org/installation/update_comfyui#common-update-issues مراجعه کنید.",
|
||||
@@ -1654,18 +1617,11 @@
|
||||
"title": "این workflow دارای nodeهای مفقود است"
|
||||
}
|
||||
},
|
||||
"nightly": {
|
||||
"badge": {
|
||||
"label": "نسخه پیشنمایش",
|
||||
"tooltip": "شما در حال استفاده از نسخه شبانه ComfyUI هستید. لطفاً با استفاده از دکمه بازخورد، نظرات خود را درباره این قابلیتها به اشتراک بگذارید."
|
||||
}
|
||||
},
|
||||
"nodeCategories": {
|
||||
"": "",
|
||||
"3d": "سهبعدی",
|
||||
"3d_models": "مدلهای سهبعدی",
|
||||
"BFL": "BFL",
|
||||
"Bria": "Bria",
|
||||
"ByteDance": "ByteDance",
|
||||
"Gemini": "Gemini",
|
||||
"Ideogram": "Ideogram",
|
||||
@@ -1687,7 +1643,6 @@
|
||||
"Veo": "Veo",
|
||||
"Vidu": "Vidu",
|
||||
"Wan": "Wan",
|
||||
"WaveSpeed": "WaveSpeed",
|
||||
"_for_testing": "_for_testing",
|
||||
"advanced": "پیشرفته",
|
||||
"animation": "انیمیشن",
|
||||
@@ -2174,14 +2129,12 @@
|
||||
"viewControls": "کنترلهای نمایش"
|
||||
},
|
||||
"sideToolbar": {
|
||||
"activeJobStatus": "وضعیت کار فعال: {status}",
|
||||
"assets": "داراییها",
|
||||
"backToAssets": "بازگشت به همه داراییها",
|
||||
"browseTemplates": "مرور قالبهای نمونه",
|
||||
"downloads": "دانلودها",
|
||||
"generatedAssetsHeader": "داراییهای تولیدشده",
|
||||
"helpCenter": "مرکز راهنما",
|
||||
"importedAssetsHeader": "داراییهای واردشده",
|
||||
"labels": {
|
||||
"assets": "داراییها",
|
||||
"console": "کنسول",
|
||||
@@ -2237,7 +2190,6 @@
|
||||
"queue": "صف",
|
||||
"queueProgressOverlay": {
|
||||
"activeJobs": "{count} کار فعال",
|
||||
"activeJobsShort": "{count} فعال | {count} فعال",
|
||||
"activeJobsSuffix": "کار فعال",
|
||||
"cancelJobTooltip": "لغو کار",
|
||||
"clearHistory": "پاکسازی تاریخچه صف کار",
|
||||
|
||||
@@ -328,68 +328,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -13539,40 +13477,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
@@ -16264,43 +16168,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
||||