Compare commits

..

14 Commits

Author SHA1 Message Date
Jin Yi
9e6939290d feature: queue tab 2026-02-06 20:02:16 +09:00
AustinMroz
8283438ee6 Fix incorrect widgetValue migration (#8625)
Under a combination of many edge cases, the `widget_values` migration
code added in #3326 would cause the progress text on a "Recraft Text to
Image" node to incorrectly deserialize into the `control_after_generate`
- widgets_values is of length 1 greater than it should be because
progress text serializes
  - It should not, there is no code to deserialize it
- negative_prompt has force_input set and skips serialization
- Migration only applies when `widgets_values` is equal to actual inputs
length. The two above edge cases cancel to make this true
- Seed is accounted for when calculating the length of widgets, but not
when applying the migration
- Migration occurs even though we track workflow version now and have an
accurate way of determining that it can not be needed

The two primary edge cases which cause the bug are both addressed
- `options.serialize` does nothing and has never done anything. I've
been guilty of making the same mistake in the ancient past, and want to
clean up the misconception where I can.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8625-Fix-incorrect-widgetValue-migration-2fe6d73d365081a683b4c675eaeebb6c)
by [Unito](https://www.unito.io)
2026-02-05 21:27:11 -08:00
Johnpaul Chiwetelu
d05e4eac58 fix: include subfolder in asset download URL for audio/video files (#8684)
## Summary

- `getAssetUrl()` was constructing `/view` URLs without the `subfolder`
query parameter, causing backend to return "file not found" for assets
stored in subfolders (common for audio/video outputs)
- Preview/playback was unaffected because it uses `preview_url` from
`ResultItemImpl.url` which correctly includes `subfolder`
- Fixed `getAssetUrl()` to include `subfolder` from
`asset.user_metadata` when present
- Simplified download functions to prefer `preview_url` (already
correct) with `getAssetUrl` as fallback

## Test plan

- [ ] Generate audio/video output (e.g. via SaveAudio node) that saves
to a subfolder
- [ ] Right-click the asset in the assets sidebar and click Download —
should download successfully
- [ ] Select multiple audio/video assets and use bulk download — should
download all
- [ ] Verify image downloads still work as before
- [ ] Verify cloud environment downloads still work (uses `preview_url`
path)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added support for organizing and downloading assets from subfolders.

* **Refactor**
* Improved asset URL generation and download handling for better
reliability and performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-06 05:06:11 +01:00
Comfy Org PR Bot
7f509cc018 1.39.8 (#8678)
Patch version increment to 1.39.8

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8678-1-39-8-2ff6d73d36508144b359cdd15822f6fb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 19:30:45 -08:00
Christian Byrne
23c8757447 fix: increase active node border weight from 2 to 3 (#8654)
## Summary

Increase stroke/outline weight for active node states to improve
visibility during workflow execution (COM-7770).

## Changes

- **What**: Increased border/stroke weight from 2 to 3 for active nodes
in both Vue Nodes and LiteGraph renderers
  - Vue Nodes: `outline-2` → `outline-3` in `LGraphNode.vue`
- LiteGraph: `lineWidth: 3` for `running` stroke (was default 1) and
`executionError` stroke (was 2) in `litegraphService.ts`
  - Updated test assertion to match

## Review Focus

Minimal visual change. The `executionError` lineWidth was also bumped
from 2 → 3 so error states remain at least as prominent as running
states.

> **Note:**
[#8603](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8603) (by
@AustinMroz) also modifies `LGraphNode.vue` with a larger restructuring
(bottom buttons, badges, resize handle). The two PRs do not conflict —
#8603 does not touch the outline/border state classes or
`litegraphService.ts`, so both changes merge cleanly.

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 17:44:19 -08:00
Johnpaul Chiwetelu
7d3d00858a feat: add download button to audio preview player (#8628)
## Summary
- Adds a download icon button to the `AudioPreviewPlayer` widget for
PreviewAudio and SaveAudio nodes
- Reuses the existing `downloadFile` utility (same as video download)
- Button appears inline next to volume/options controls, matching the
player's existing UI style

## Test plan
- [x] Add a PreviewAudio or SaveAudio node, run a workflow that produces
audio output
- [x] Verify the download icon appears in the audio player controls
- [x] Click the download button and confirm the audio file downloads
correctly
- [x] Verify the button does not appear when no audio is loaded


https://github.com/user-attachments/assets/7fb2df16-82a6-40aa-a938-aed57032e30b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8628-feat-add-download-button-to-audio-preview-player-2fe6d73d365081e3997fc45d3bb8cffc)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-06 01:35:32 +01:00
Christian Byrne
478cfc0b5e feat: replace puzzle icon with extensions-blocks icon for manager button (#8644)
## Summary

Replace the manager button puzzle icon with a custom extensions-blocks
SVG icon and add a "Manage Extensions" text label to the top bar button.

## Changes

- **What**: Swap `icon-[lucide--puzzle]` →
`icon-[comfy--extensions-blocks]` in TopMenuSection, ComfyMenuButton,
and ManagerDialog. Add visible "Manage Extensions" label (hidden below
md). Align tooltip with new label text.

## Review Focus

- Visual appearance of the new icon at different sizes
- Button layout with text label at md+ breakpoints
- Red dot notification positioning with wider button

Fixes COM-12161

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8644-feat-replace-puzzle-icon-with-extensions-blocks-icon-for-manager-button-2fe6d73d3650815c8867efc5a0842ef7)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-05 16:33:26 -08:00
Johnpaul Chiwetelu
90a701dd67 Road to No Explicit Any Part 11 (#8565)
## Summary

This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.

### Key Changes

#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing

#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks

#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)

#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture

#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods

### Files Changed

**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue

**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts

**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts

**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts

**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts

**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts

**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts

**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts

### Testing

- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499

---------

Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 16:29:28 -08:00
Johnpaul Chiwetelu
7f81e1afac ci: filter snapshot update job to only run @screenshot tagged tests (#8629)
## Summary
- Adds `--grep @screenshot` to the Playwright command in the
update-snapshots CI workflow
- Skips ~146 non-screenshot tests that don't produce any snapshot files,
reducing CI time and resource usage

## Details
All tests that call `toHaveScreenshot` are already tagged with
`@screenshot` (either at the `test.describe` or individual `test`
level). The snapshot update job was previously running every test
unnecessarily.

The `--grep` CLI flag is ANDed with each project's existing
`grep`/`grepInvert` settings, so all projects continue to work
correctly:
- `chromium`: `@screenshot` AND NOT `@mobile`
- `chromium-2x`: `@screenshot` AND `@2x`
- `mobile-chrome`: `@screenshot` AND `@mobile`

## Test plan
- [x] Trigger the update-snapshots workflow on a PR with the "New
Browser Test Expectations" label and verify only screenshot-tagged tests
run
- [x] Verify snapshot files are still correctly updated
2026-02-05 15:18:21 -08:00
AustinMroz
e26283e754 Revert delay of layout initialization (#8619)
#7591 added a one tick delay to layout initialization in an attempt to
resolve some layouting discrepancies. However, it appears to have
reintroduced node scaling issues and introduced a new bug that prevents
cloning nodes with alt+drag in vue.

Alternatives methods of resolving the original issue are being
investigated, but this change was causing more harm than good.

The prior PR included other changes (like a testing fix). Those changes
remain beneficial and do not need to be reverted.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8619-Revert-delay-of-layout-initialization-2fe6d73d365081fc9111c9457ea9752d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 09:17:41 -08:00
Jin Yi
1ca6e57ac4 fix: skip node replacement API call when feature is disabled (#8618) 2026-02-05 16:37:18 +09:00
Johnpaul Chiwetelu
3adecc4ded fix: prevent duplicate context menu items by using content-based comparison (#8602)
## Summary
- Switches from reference-based to content-based duplicate detection for
context menu items
- Fixes cases where extensions create duplicate menu items (different
objects with same content)
- Improves removal detection accuracy by comparing content strings
instead of object references

## Details
The previous implementation compared menu items by object reference,
which would miss duplicates when extensions added new objects with the
same content. This change:
- Creates Sets of content strings from menu items for efficient
duplicate detection
- Filters additions by checking if their content already exists
- Provides accurate count of removed items through content comparison

## Test plan
- [x] Unit tests pass (`pnpm test:unit
src/lib/litegraph/src/contextMenuCompat.test.ts`)
- [x] TypeScript compilation succeeds (`pnpm typecheck`)
- [x] Linting passes (`pnpm lint`)
- [x] Pre-commit hooks pass

## Before
<img width="633" height="1316" alt="Screenshot 2026-02-04 045422"
src="https://github.com/user-attachments/assets/972871e2-1fd6-45a4-bb6c-9ce73ce7aed7"
/>


## After
<img width="531" height="854" alt="Screenshot 2026-02-04 045918"
src="https://github.com/user-attachments/assets/977bed37-dfb8-41d7-b659-88c477ff8a02"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved context menu compatibility: more accurate detection of
added/removed menu items and clearer warnings when items are removed.
* **Refactor**
* Updated numeric input formatting to use locale-aware formatting logic,
preserving grouping and precision behavior.
* **Tests**
* Added a test ensuring legacy menu extraction handles items with
undefined content correctly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8602-fix-prevent-duplicate-context-menu-items-by-using-content-based-comparison-2fd6d73d36508197aa74c3409c7425fa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-02-04 20:06:58 -08:00
Comfy Org PR Bot
7404756b6d 1.39.7 (#8614)
Patch version increment to 1.39.7

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8614-1-39-7-2fe6d73d365081449fa7cc9143adf0fa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-04 18:38:15 -08:00
Christian Byrne
7c6e2d2c7a fix: disable CodeRabbit high_level_summary to stop PR description auto-updates (#8615)
## Summary

Disables the `high_level_summary` feature in CodeRabbit's configuration.
This feature automatically modifies the PR description every time a
commit is pushed, which was causing frustration for developers.

## Changes

- Set `high_level_summary: false` in `.coderabbit.yaml`

## Context

Discussed in #frontend-code-reviews Slack channel. The team agreed that
having PR descriptions automatically modified with every commit push is
not desirable behavior.

---

Fixes
https://www.notion.so/comfy-org/Ops-Disable-high_level_summary-in-ComfyUI_frontend-coderabbit-yaml-2fd6d73d3650812b9eb8d6680fa11932

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8615-fix-disable-CodeRabbit-high_level_summary-to-stop-PR-description-auto-updates-2fe6d73d36508112ac16f5df51845fcd)
by [Unito](https://www.unito.io)
2026-02-04 18:37:44 -08:00
124 changed files with 1509 additions and 675 deletions

View File

@@ -2,5 +2,6 @@ issue_enrichment:
auto_enrich:
enabled: true
reviews:
high_level_summary: false
auto_review:
drafts: true

View File

@@ -109,7 +109,7 @@ jobs:
# Run sharded tests with snapshot updates (browsers pre-installed in container)
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright-tests
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run: pnpm exec playwright test --update-snapshots --grep @screenshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
continue-on-error: true
- name: Stage changed snapshot files

View File

@@ -1,6 +1,8 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import { nextTick, provide } from 'vue'
import type { ElectronWindow } from '@/utils/envUtil'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
@@ -42,16 +44,21 @@ const meta: Meta<typeof InstallView> = {
const router = createMockRouter()
// Mock electron API
;(window as any).electronAPI = {
;(window as ElectronWindow).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (_eventName: string, _data?: any) => {}
trackEvent: (
_eventName: string,
_data?: Record<string, unknown>
) => {}
},
installComfyUI: (_options: any) => {},
changeTheme: (_theme: any) => {},
installComfyUI: (
_options: Parameters<ElectronAPI['installComfyUI']>[0]
) => {},
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
@@ -240,8 +247,8 @@ export const DesktopSettings: Story = {
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as any).electronAPI.getPlatform = () => 'win32'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
@@ -259,8 +266,8 @@ export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as any).electronAPI.getPlatform = () => 'darwin'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
@@ -327,7 +334,7 @@ export const ManualInstall: Story = {
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as any).electronAPI.validateInstallPath = () =>
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
@@ -375,7 +382,7 @@ export const ErrorState: Story = {
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as any).electronAPI.validateInstallPath = () =>
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,

View File

@@ -1,5 +1,9 @@
import { test as base } from '@playwright/test'
interface TestWindow extends Window {
__ws__?: Record<string, WebSocket>
}
export const webSocketFixture = base.extend<{
ws: { trigger(data: unknown, url?: string): Promise<void> }
}>({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.39.6",
"version": "1.39.8",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path d="M6.66667 14V5.33333C6.66667 5.15652 6.59643 4.98695 6.4714 4.86193C6.34638 4.7369 6.17681 4.66667 6 4.66667H2.66667C2.48986 4.66667 2.32029 4.7369 2.19526 4.86193C2.07024 4.98695 2 5.15652 2 5.33333V13.3333C2 13.5101 2.07024 13.6797 2.19526 13.8047C2.32029 13.9298 2.48986 14 2.66667 14H10.6667C10.8435 14 11.013 13.9298 11.1381 13.8047C11.2631 13.6797 11.3333 13.5101 11.3333 13.3333V10C11.3333 9.82319 11.2631 9.65362 11.1381 9.5286C11.013 9.40357 10.8435 9.33333 10.6667 9.33333H2M10 2H13.3333C13.7015 2 14 2.29848 14 2.66667V6C14 6.36819 13.7015 6.66667 13.3333 6.66667H10C9.63181 6.66667 9.33333 6.36819 9.33333 6V2.66667C9.33333 2.29848 9.63181 2 10 2Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 837 B

View File

@@ -2,10 +2,7 @@
import { execSync } from 'child_process'
import * as fs from 'fs'
import { globSync } from 'glob'
interface LocaleData {
[key: string]: any
}
import type { LocaleData } from './i18n-types'
// Configuration
const SOURCE_PATTERNS = ['src/**/*.{js,ts,vue}', '!src/locales/**/*']
@@ -45,7 +42,7 @@ function getStagedLocaleFiles(): string[] {
}
// Extract all keys from a nested object
function extractKeys(obj: any, prefix = ''): string[] {
function extractKeys(obj: LocaleData, prefix = ''): string[] {
const keys: string[] = []
for (const [key, value] of Object.entries(obj)) {
@@ -166,17 +163,17 @@ async function checkNewUnusedKeys() {
// Report results
if (unusedNewKeys.length > 0) {
console.log('\n⚠ Warning: Found unused NEW i18n keys:\n')
console.warn('\n⚠ Warning: Found unused NEW i18n keys:\n')
for (const key of unusedNewKeys.sort()) {
console.log(` - ${key}`)
console.warn(` - ${key}`)
}
console.log(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
console.log(
console.warn(`\n✨ Total unused new keys: ${unusedNewKeys.length}`)
console.warn(
'\nThese keys were added but are not used anywhere in the codebase.'
)
console.log('Consider using them or removing them in a future update.')
console.warn('Consider using them or removing them in a future update.')
// Changed from process.exit(1) to process.exit(0) for warning only
process.exit(0)

View File

@@ -7,6 +7,7 @@ import {
writeFileSync
} from 'fs'
import { dirname, join } from 'path'
import type { LocaleData } from './i18n-types'
// Ensure directories exist
function ensureDir(dir: string) {
@@ -41,8 +42,8 @@ function getAllJsonFiles(dir: string): string[] {
}
// Find additions in new object compared to base
function findAdditions(base: any, updated: any): Record<string, any> {
const additions: Record<string, any> = {}
function findAdditions(base: LocaleData, updated: LocaleData): LocaleData {
const additions: LocaleData = {}
for (const key in updated) {
if (!(key in base)) {
@@ -74,7 +75,7 @@ function capture(srcLocaleDir: string, tempBaseDir: string) {
ensureDir(dirname(targetPath))
writeFileSync(targetPath, readFileSync(file, 'utf8'))
}
console.log('Captured current locale files to temp/base/')
console.warn('Captured current locale files to temp/base/')
}
// Diff command
@@ -94,7 +95,7 @@ function diff(srcLocaleDir: string, tempBaseDir: string, tempDiffDir: string) {
if (Object.keys(additions).length > 0) {
ensureDir(dirname(diffPath))
writeFileSync(diffPath, JSON.stringify(additions, null, 2))
console.log(`Wrote diff to ${diffPath}`)
console.warn(`Wrote diff to ${diffPath}`)
}
}
}
@@ -116,9 +117,9 @@ switch (command) {
// Remove temp directory recursively
if (existsSync('temp')) {
rmSync('temp', { recursive: true, force: true })
console.log('Removed temp directory')
console.warn('Removed temp directory')
}
break
default:
console.log('Please specify either "capture" or "diff" command')
console.error('Please specify either "capture" or "diff" command')
}

5
scripts/i18n-types.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Shared types for i18n-related scripts
*/
export type LocaleData = { [key: string]: string | LocaleData }

View File

@@ -19,12 +19,14 @@
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('menu.customNodesManager')"
:aria-label="t('menu.manageExtensions')"
class="relative"
@click="openCustomNodeManager"
>
<i class="icon-[lucide--puzzle] size-4" />
<i class="icon-[comfy--extensions-blocks] size-4" />
<span class="not-md:hidden">
{{ t('menu.manageExtensions') }}
</span>
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
@@ -220,7 +222,7 @@ const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
)
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.customNodesManager'))
buildTooltipConfig(t('menu.manageExtensions'))
)
const queueContextMenu = ref<InstanceType<typeof ContextMenu> | null>(null)
const queueContextMenuItems = computed<MenuItem[]>(() => [

View File

@@ -38,6 +38,7 @@
<script setup lang="ts">
import Skeleton from 'primevue/skeleton'
import { computed, onUnmounted, ref, watch } from 'vue'
import type { StyleValue } from 'vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
import { useMediaCache } from '@/services/mediaCacheService'
@@ -55,7 +56,7 @@ const {
alt?: string
containerClass?: ClassValue
imageClass?: ClassValue
imageStyle?: Record<string, any>
imageStyle?: StyleValue
rootMargin?: string
}>()

View File

@@ -1,8 +1,5 @@
<template>
<div
ref="container"
class="h-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-(--dialog-surface)"
>
<div ref="container" class="h-full scrollbar-custom">
<div :style="topSpacerStyle" />
<div :style="mergedGridStyle">
<div

View File

@@ -131,7 +131,7 @@ const { isDragging, dragMessage, handleDragOver, handleDragLeave, handleDrop } =
onModelDrop: async (file) => {
await viewer.handleModelDrop(file)
},
disabled: viewer.isPreview.value || isStandaloneMode
disabled: viewer.isPreview.value || !!isStandaloneMode
})
onMounted(async () => {

View File

@@ -158,7 +158,7 @@ export const Queued: Story = {
prompt_id: 'p1'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -217,7 +217,7 @@ export const QueuedParallel: Story = {
prompt_id: 'p2'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -258,7 +258,7 @@ export const Running: Story = {
prompt_id: 'p1'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -303,7 +303,7 @@ export const QueuedZeroAheadSingleRunning: Story = {
prompt_id: 'p1'
}
}
} as any
}
return { args: { ...args, jobId } }
},
@@ -360,7 +360,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
prompt_id: 'p2'
}
}
} as any
}
return { args: { ...args, jobId } }
},

View File

@@ -138,7 +138,6 @@ describe('flatAndCategorizeSelectedItems', () => {
expect(result.nodes).toEqual([testNode1])
expect(result.groups).toEqual([testGroup1, testGroup2])
expect(result.nodeToParentGroup.get(testNode1)).toBe(testGroup2)
expect(result.nodeToParentGroup.has(testGroup2 as any)).toBe(false)
})
it('should handle mixed selection of nodes and groups', () => {

View File

@@ -230,7 +230,7 @@ const extraMenuItems = computed(() => [
{
key: 'manage-extensions',
label: t('menu.manageExtensions'),
icon: 'icon-[lucide--puzzle]',
icon: 'icon-[comfy--extensions-blocks]',
command: showManageExtensions
}
])

View File

@@ -1,23 +1,7 @@
<template>
<div class="flex h-full flex-col">
<!-- Active Jobs Grid -->
<div
v-if="!isInFolderView && isQueuePanelV2Enabled && activeJobItems.length"
class="grid max-h-[50%] scrollbar-custom overflow-y-auto"
:style="gridStyle"
>
<ActiveMediaAssetCard
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 v-if="assets.length" class="px-2 2xl:px-4">
<div
class="flex items-center py-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
>
@@ -59,25 +43,18 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import ActiveMediaAssetCard from '@/platform/assets/components/ActiveMediaAssetCard.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'
import { useSettingStore } from '@/platform/settings/settingStore'
const {
assets,
isSelected,
isInFolderView = false,
assetType = 'output',
showOutputCount,
getOutputCount
} = defineProps<{
assets: AssetItem[]
isSelected: (assetId: string) => boolean
isInFolderView?: boolean
assetType?: 'input' | 'output'
showOutputCount: (asset: AssetItem) => boolean
getOutputCount: (asset: AssetItem) => number
@@ -92,19 +69,9 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const { jobItems } = useJobList()
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
type AssetGridItem = { key: string; asset: AssetItem }
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
)
const assetItems = computed<AssetGridItem[]>(() =>
assets.map((asset) => ({
key: `asset-${asset.id}`,

View File

@@ -1,6 +1,5 @@
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import AssetsSidebarListView from './AssetsSidebarListView.vue'
@@ -10,51 +9,12 @@ vi.mock('vue-i18n', () => ({
})
}))
vi.mock('@/composables/queue/useJobActions', () => ({
useJobActions: () => ({
cancelAction: { variant: 'ghost', label: 'Cancel', icon: 'pi pi-times' },
canCancelJob: ref(false),
runCancelJob: vi.fn()
})
}))
const mockJobItems = ref<
Array<{
id: string
title: string
meta: string
state: string
createTime?: number
}>
>([])
vi.mock('@/composables/queue/useJobList', () => ({
useJobList: () => ({
jobItems: mockJobItems
})
}))
vi.mock('@/stores/assetsStore', () => ({
useAssetsStore: () => ({
isAssetDeleting: () => false
})
}))
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: (key: string) => key === 'Comfy.Queue.QPOV2'
})
}))
vi.mock('@/utils/queueUtil', () => ({
isActiveJobState: (state: string) =>
state === 'pending' || state === 'running'
}))
vi.mock('@/utils/queueDisplay', () => ({
iconForJobState: () => 'pi pi-spinner'
}))
vi.mock('@/platform/assets/schemas/assetMetadataSchema', () => ({
getOutputAssetMetadata: () => undefined
}))
@@ -73,7 +33,6 @@ vi.mock('@/utils/formatUtil', () => ({
describe('AssetsSidebarListView', () => {
beforeEach(() => {
vi.clearAllMocks()
mockJobItems.value = []
})
const defaultProps = {
@@ -84,67 +43,14 @@ describe('AssetsSidebarListView', () => {
toggleStack: async () => {}
}
it('displays active jobs in oldest-first order (FIFO)', () => {
mockJobItems.value = [
{
id: 'newest',
title: 'Newest Job',
meta: '',
state: 'pending',
createTime: 3000
},
{
id: 'middle',
title: 'Middle Job',
meta: '',
state: 'running',
createTime: 2000
},
{
id: 'oldest',
title: 'Oldest Job',
meta: '',
state: 'pending',
createTime: 1000
}
]
it('renders without errors with empty assets', () => {
const wrapper = mount(AssetsSidebarListView, {
props: defaultProps,
shallow: true
})
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
expect(jobListItems).toHaveLength(3)
const displayedTitles = jobListItems.map((item) =>
item.props('primaryText')
)
expect(displayedTitles).toEqual(['Oldest Job', 'Middle Job', 'Newest Job'])
})
it('excludes completed and failed jobs from active jobs section', () => {
mockJobItems.value = [
{ id: 'pending', title: 'Pending', meta: '', state: 'pending' },
{ id: 'completed', title: 'Completed', meta: '', state: 'completed' },
{ id: 'failed', title: 'Failed', meta: '', state: 'failed' },
{ id: 'running', title: 'Running', meta: '', state: 'running' }
]
const wrapper = mount(AssetsSidebarListView, {
props: defaultProps,
shallow: true
})
const jobListItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
expect(jobListItems).toHaveLength(2)
const displayedTitles = jobListItems.map((item) =>
item.props('primaryText')
)
expect(displayedTitles).toContain('Running')
expect(displayedTitles).toContain('Pending')
expect(displayedTitles).not.toContain('Completed')
expect(displayedTitles).not.toContain('Failed')
expect(wrapper.exists()).toBe(true)
const listItems = wrapper.findAllComponents({ name: 'AssetsListItem' })
expect(listItems).toHaveLength(0)
})
})

View File

@@ -1,48 +1,6 @@
<template>
<div class="flex h-full flex-col">
<div
v-if="isQueuePanelV2Enabled && activeJobItems.length"
class="flex max-h-[50%] scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
>
<AssetsListItem
v-for="job in activeJobItems"
:key="job.id"
:class="
cn(
'w-full shrink-0 text-text-primary transition-colors hover:bg-secondary-background-hover',
'cursor-default'
)
"
:preview-url="job.iconImageUrl"
:preview-alt="job.title"
:icon-name="job.iconName"
:icon-class="getJobIconClass(job)"
:primary-text="job.title"
:secondary-text="job.meta"
:progress-total-percent="job.progressTotalPercent"
:progress-current-percent="job.progressCurrentPercent"
@mouseenter="onJobEnter(job.id)"
@mouseleave="onJobLeave(job.id)"
@click.stop
>
<template v-if="hoveredJobId === job.id" #actions>
<Button
v-if="canCancelJob"
:variant="cancelAction.variant"
size="icon"
:aria-label="cancelAction.label"
@click.stop="runCancelJob()"
>
<i :class="cancelAction.icon" class="size-4" />
</Button>
</template>
</AssetsListItem>
</div>
<div
v-if="assetItems.length"
:class="cn('px-2', activeJobItems.length && 'mt-2')"
>
<div v-if="assetItems.length" class="px-2">
<div
class="flex items-center p-2 text-sm font-normal leading-normal text-muted-foreground font-inter"
>
@@ -119,31 +77,25 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Button from '@/components/ui/button/Button.vue'
import { useJobActions } from '@/composables/queue/useJobActions'
import type { JobListItem } from '@/composables/queue/useJobList'
import { useJobList } from '@/composables/queue/useJobList'
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
import type { OutputStackListItem } from '@/platform/assets/composables/useOutputStacks'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { iconForMediaType } from '@/platform/assets/utils/mediaIconUtil'
import { useAssetsStore } from '@/stores/assetsStore'
import { isActiveJobState } from '@/utils/queueUtil'
import {
formatDuration,
formatSize,
getMediaTypeFromFilename,
truncateFilename
} from '@/utils/formatUtil'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
import { useSettingStore } from '@/platform/settings/settingStore'
const {
assetItems,
@@ -170,24 +122,8 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const { jobItems } = useJobList()
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const hoveredJobId = ref<string | null>(null)
const hoveredAssetId = ref<string | null>(null)
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
)
const hoveredJob = computed(() =>
hoveredJobId.value
? (activeJobItems.value.find((job) => job.id === hoveredJobId.value) ??
null)
: null
)
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
const listGridStyle = {
display: 'grid',
@@ -240,16 +176,6 @@ function getAssetCardClass(selected: boolean): string {
)
}
function onJobEnter(jobId: string) {
hoveredJobId.value = jobId
}
function onJobLeave(jobId: string) {
if (hoveredJobId.value === jobId) {
hoveredJobId.value = null
}
}
function onAssetEnter(assetId: string) {
hoveredAssetId.value = assetId
}
@@ -259,13 +185,4 @@ function onAssetLeave(assetId: string) {
hoveredAssetId.value = null
}
}
function getJobIconClass(job: JobListItem): string | undefined {
const classes = []
const iconName = job.iconName ?? iconForJobState(job.state)
if (!job.iconImageUrl && iconName === iconForJobState('pending')) {
classes.push('animate-spin')
}
return classes.length ? classes.join(' ') : undefined
}
</script>

View File

@@ -26,6 +26,15 @@
<template #tool-buttons>
<!-- Normal Tab View -->
<TabList v-if="!isInFolderView" v-model="activeTab">
<Tab v-if="isQueuePanelV2Enabled" class="font-inter" value="queue">
{{ $t('sideToolbar.labels.queue') }}
<span
v-if="activeJobsCount > 0"
class="ml-1 inline-flex items-center justify-center rounded-full bg-primary px-1.5 text-xs text-base-foreground font-medium h-5"
>
{{ activeJobsCount }}
</span>
</Tab>
<Tab class="font-inter" value="output">{{
$t('sideToolbar.labels.generated')
}}</Tab>
@@ -43,8 +52,9 @@
</Button>
</div>
<!-- Filter Bar -->
<!-- Filter Bar (hidden on queue tab) -->
<MediaAssetFilterBar
v-if="!isQueueTab"
v-model:search-query="searchQuery"
v-model:sort-by="sortBy"
v-model:view-mode="viewMode"
@@ -53,13 +63,14 @@
:show-generation-time-sort="activeTab === 'output'"
/>
<div
v-if="isQueuePanelV2Enabled && !isInFolderView"
class="flex items-center justify-between px-2 py-2 2xl:px-4"
v-if="isQueueTab && !isInFolderView"
class="flex items-center justify-between px-4 2xl:px-6"
>
<span class="text-sm text-muted-foreground">
{{ activeJobsLabel }}
</span>
<div class="flex items-center gap-2">
<MediaAssetViewModeToggle v-model:view-mode="viewMode" />
<span class="text-sm text-base-foreground">
{{ t('sideToolbar.queueProgressOverlay.clearQueueTooltip') }}
</span>
@@ -76,7 +87,7 @@
</Button>
</div>
</div>
<Divider v-else type="dashed" class="my-2" />
<Divider v-else-if="!isQueueTab" type="dashed" class="my-2" />
</template>
<template #body>
<div v-if="showLoadingState">
@@ -87,23 +98,32 @@
icon="pi pi-info-circle"
:title="
$t(
activeTab === 'input'
? 'sideToolbar.noImportedFiles'
: 'sideToolbar.noGeneratedFiles'
isQueueTab
? 'sideToolbar.noQueueItems'
: activeTab === 'input'
? 'sideToolbar.noImportedFiles'
: 'sideToolbar.noGeneratedFiles'
)
"
:message="
$t(
isQueueTab
? 'sideToolbar.noQueueItemsMessage'
: 'sideToolbar.noFilesFoundMessage'
)
"
:message="$t('sideToolbar.noFilesFoundMessage')"
/>
</div>
<div v-else class="relative size-full" @click="handleEmptySpaceClick">
<QueueAssetView v-if="isQueueTab" :view-mode="viewMode" />
<AssetsSidebarListView
v-if="isListView"
v-else-if="isListView"
:asset-items="listViewAssetItems"
:is-selected="isSelected"
:selectable-assets="listViewSelectableAssets"
:is-stack-expanded="isListViewStackExpanded"
:toggle-stack="toggleListViewStack"
:asset-type="activeTab"
:asset-type="assetTabType"
@select-asset="handleAssetSelect"
@context-menu="handleAssetContextMenu"
@approach-end="handleApproachEnd"
@@ -112,8 +132,7 @@
v-else
:assets="displayAssets"
:is-selected="isSelected"
:is-in-folder-view="isInFolderView"
:asset-type="activeTab"
:asset-type="assetTabType"
:show-output-count="shouldShowOutputCount"
:get-output-count="getOutputCount"
@select-asset="handleAssetSelect"
@@ -224,6 +243,7 @@ const Load3dViewerContent = () =>
import('@/components/load3d/Load3dViewerContent.vue')
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
import QueueAssetView from '@/components/sidebar/tabs/QueueAssetView.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'
@@ -231,6 +251,7 @@ import TabList from '@/components/tab/TabList.vue'
import Button from '@/components/ui/button/Button.vue'
import MediaAssetContextMenu from '@/platform/assets/components/MediaAssetContextMenu.vue'
import MediaAssetFilterBar from '@/platform/assets/components/MediaAssetFilterBar.vue'
import MediaAssetViewModeToggle from '@/platform/assets/components/MediaAssetViewModeToggle.vue'
import { getAssetType } from '@/platform/assets/composables/media/assetMappers'
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
@@ -257,7 +278,7 @@ const { activeJobsCount } = storeToRefs(queueStore)
const executionStore = useExecutionStore()
const settingStore = useSettingStore()
const activeTab = ref<'input' | 'output'>('output')
const activeTab = ref<'input' | 'output' | 'queue'>('output')
const folderPromptId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined)
const isInFolderView = computed(() => folderPromptId.value !== null)
@@ -268,6 +289,10 @@ const viewMode = useStorage<'list' | 'grid'>(
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const isQueueTab = computed(() => activeTab.value === 'queue')
const assetTabType = computed<'input' | 'output'>(() =>
activeTab.value === 'input' ? 'input' : 'output'
)
const isListView = computed(
() => isQueuePanelV2Enabled.value && viewMode.value === 'list'
)
@@ -415,18 +440,15 @@ const isBulkMode = computed(
)
const showLoadingState = computed(
() =>
loading.value &&
displayAssets.value.length === 0 &&
activeJobsCount.value === 0
() => !isQueueTab.value && loading.value && displayAssets.value.length === 0
)
const showEmptyState = computed(
() =>
!loading.value &&
displayAssets.value.length === 0 &&
activeJobsCount.value === 0
)
const showEmptyState = computed(() => {
if (isQueueTab.value) {
return activeJobsCount.value === 0
}
return !loading.value && displayAssets.value.length === 0
})
watch(visibleAssets, (newAssets) => {
// Alternative: keep hidden selections and surface them in UI; for now prune
@@ -483,12 +505,21 @@ watch(
clearSelection()
// Clear search when switching tabs
searchQuery.value = ''
// Reset pagination state when tab changes
void refreshAssets()
// Skip asset fetch for queue tab
if (activeTab.value !== 'queue') {
void refreshAssets()
}
},
{ immediate: true }
)
// Reset to output tab if QPOV2 is disabled while on queue tab
watch(isQueuePanelV2Enabled, (enabled) => {
if (!enabled && activeTab.value === 'queue') {
activeTab.value = 'output'
}
})
function handleAssetSelect(asset: AssetItem, assets?: AssetItem[]) {
const assetList = assets ?? visibleAssets.value
const index = assetList.findIndex((a) => a.id === asset.id)

View File

@@ -0,0 +1,123 @@
<template>
<div class="flex h-full flex-col">
<!-- Grid View -->
<VirtualGrid
v-if="viewMode === 'grid'"
class="flex-1"
:items="gridItems"
:grid-style="gridStyle"
>
<template #item="{ item }">
<ActiveMediaAssetCard :job="item.job" />
</template>
</VirtualGrid>
<!-- List View -->
<div
v-else
class="flex flex-1 scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
>
<AssetsListItem
v-for="job in activeJobItems"
:key="job.id"
:class="
cn(
'w-full shrink-0 text-text-primary transition-colors hover:bg-secondary-background-hover',
'cursor-default'
)
"
:preview-url="job.iconImageUrl"
:preview-alt="job.title"
:icon-name="job.iconName"
:icon-class="getJobIconClass(job)"
:primary-text="job.title"
:secondary-text="job.meta"
:progress-total-percent="job.progressTotalPercent"
:progress-current-percent="job.progressCurrentPercent"
@mouseenter="onJobEnter(job.id)"
@mouseleave="onJobLeave(job.id)"
@click.stop
>
<template v-if="hoveredJobId === job.id" #actions>
<Button
v-if="canCancelJob"
:variant="cancelAction.variant"
size="icon"
:aria-label="cancelAction.label"
@click.stop="runCancelJob()"
>
<i :class="cancelAction.icon" class="size-4" />
</Button>
</template>
</AssetsListItem>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Button from '@/components/ui/button/Button.vue'
import { useJobActions } from '@/composables/queue/useJobActions'
import type { JobListItem } from '@/composables/queue/useJobList'
import { useJobList } from '@/composables/queue/useJobList'
import ActiveMediaAssetCard from '@/platform/assets/components/ActiveMediaAssetCard.vue'
import AssetsListItem from '@/platform/assets/components/AssetsListItem.vue'
import { isActiveJobState } from '@/utils/queueUtil'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
const { viewMode = 'grid' } = defineProps<{
viewMode?: 'list' | 'grid'
}>()
const { jobItems } = useJobList()
const activeJobItems = computed(() =>
jobItems.value.filter((item) => isActiveJobState(item.state)).toReversed()
)
const gridItems = computed(() =>
activeJobItems.value.map((job) => ({
key: `queue-${job.id}`,
job
}))
)
const gridStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
padding: '0 0.5rem',
gap: '0.5rem'
}
// List view hover & cancel logic
const hoveredJobId = ref<string | null>(null)
const hoveredJob = computed(() =>
hoveredJobId.value
? (activeJobItems.value.find((job) => job.id === hoveredJobId.value) ??
null)
: null
)
const { cancelAction, canCancelJob, runCancelJob } = useJobActions(hoveredJob)
function onJobEnter(jobId: string) {
hoveredJobId.value = jobId
}
function onJobLeave(jobId: string) {
if (hoveredJobId.value === jobId) {
hoveredJobId.value = null
}
}
function getJobIconClass(job: JobListItem): string | undefined {
const classes = []
const iconName = job.iconName ?? iconForJobState(job.state)
if (!job.iconImageUrl && iconName === iconForJobState('pending')) {
classes.push('animate-spin')
}
return classes.length ? classes.join(' ') : undefined
}
</script>

View File

@@ -52,7 +52,7 @@ export interface SafeWidgetData {
isDOMWidget?: boolean
label?: string
nodeType?: string
options?: IWidgetOptions<unknown>
options?: IWidgetOptions
spec?: InputSpec
slotMetadata?: WidgetSlotMetadata
}
@@ -145,7 +145,7 @@ interface SharedWidgetEnhancements {
/** Widget label */
label?: string
/** Widget options */
options?: Record<string, any>
options?: IWidgetOptions
}
/**
@@ -170,7 +170,7 @@ export function getSharedWidgetEnhancements(
? 'ring ring-component-node-widget-advanced'
: undefined,
label: widget.label,
options: widget.options
options: widget.options as IWidgetOptions
}
}
@@ -432,7 +432,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

View File

@@ -47,7 +47,7 @@ export const useComputedWithWidgetWatch = (
const { widgetNames, triggerCanvasRedraw = false } = options
// Create a reactive trigger based on widget values
const widgetValues = ref<Record<string, any>>({})
const widgetValues = ref<Record<string, unknown>>({})
// Initialize widget observers
if (node.widgets) {
@@ -56,7 +56,7 @@ export const useComputedWithWidgetWatch = (
: node.widgets
// Initialize current values
const currentValues: Record<string, any> = {}
const currentValues: Record<string, unknown> = {}
widgetsToObserve.forEach((widget) => {
currentValues[widget.name] = widget.value
})

View File

@@ -2,7 +2,7 @@ import type { Ref } from 'vue'
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, Point } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
@@ -33,7 +33,7 @@ export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
if (node.data instanceof ComfyNodeDefImpl) {
const nodeDef = node.data
const pos = [...basePos]
const pos: Point = [...basePos]
// Add an offset on y to make sure after adding the node, the cursor
// is on the node (top left corner)
pos[1] += LiteGraph.NODE_TITLE_HEIGHT

View File

@@ -195,6 +195,42 @@ describe('contextMenuCompat', () => {
expect.any(Error)
)
})
it('should handle multiple items with undefined content correctly', () => {
// Setup base method with items that have undefined content
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
return [
{ content: undefined, title: 'Separator 1' },
{ content: undefined, title: 'Separator 2' },
{ content: 'Item 1', callback: () => {} }
]
}
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')
// Monkey-patch to add an item with undefined content
const original = LGraphCanvas.prototype.getCanvasMenuOptions
LGraphCanvas.prototype.getCanvasMenuOptions =
function (): (IContextMenuValue | null)[] {
const items = original.apply(this)
items.push({ content: undefined, title: 'Separator 3' })
return items
}
// Extract legacy items
const legacyItems = legacyMenuCompat.extractLegacyItems(
'getCanvasMenuOptions',
mockCanvas
)
// Should extract only the newly added item with undefined content
// (not collapse with existing undefined content items)
expect(legacyItems).toHaveLength(1)
expect(legacyItems[0]).toMatchObject({
content: undefined,
title: 'Separator 3'
})
})
})
describe('integration', () => {

View File

@@ -152,19 +152,51 @@ class LegacyMenuCompat {
const patchedItems = methodToCall.apply(context, args) as
| (IContextMenuValue | null)[]
| undefined
if (!patchedItems) return []
if (!patchedItems) {
return []
}
// Use content-based diff to detect additions (not reference-based)
// Create composite keys from multiple properties to handle undefined content
const createItemKey = (item: IContextMenuValue): string => {
const parts = [
item.content ?? '',
item.title ?? '',
item.className ?? '',
item.property ?? '',
item.type ?? ''
]
return parts.join('|')
}
// Use set-based diff to detect additions by reference
const originalSet = new Set<IContextMenuValue | null>(originalItems)
const addedItems = patchedItems.filter((item) => !originalSet.has(item))
const originalKeys = new Set(
originalItems
.filter(
(item): item is IContextMenuValue =>
item !== null && typeof item === 'object' && 'content' in item
)
.map(createItemKey)
)
const addedItems = patchedItems.filter((item) => {
if (item === null) return false
if (typeof item !== 'object' || !('content' in item)) return false
return !originalKeys.has(createItemKey(item))
})
// Warn if items were removed (patched has fewer original items than expected)
const retainedOriginalCount = patchedItems.filter((item) =>
originalSet.has(item)
const patchedKeys = new Set(
patchedItems
.filter(
(item): item is IContextMenuValue =>
item !== null && typeof item === 'object' && 'content' in item
)
.map(createItemKey)
)
const removedCount = [...originalKeys].filter(
(key) => !patchedKeys.has(key)
).length
if (retainedOriginalCount < originalItems.length) {
if (removedCount > 0) {
console.warn(
`[Context Menu Compat] Monkey patch for ${methodName} removed ${originalItems.length - retainedOriginalCount} original menu item(s). ` +
`[Context Menu Compat] Monkey patch for ${methodName} removed ${removedCount} original menu item(s). ` +
`This may cause unexpected behavior.`
)
}

View File

@@ -2,7 +2,7 @@ import type { NeverNever, PickNevers } from '@/lib/litegraph/src/types/utility'
type EventListeners<T> = {
readonly [K in keyof T]:
| ((this: EventTarget, ev: CustomEvent<T[K]>) => any)
| ((this: EventTarget, ev: CustomEvent<T[K]>) => unknown)
| EventListenerObject
| null
}

View File

@@ -68,7 +68,7 @@ describe.skip('subgraphUtils', () => {
describe.skip('findUsedSubgraphIds', () => {
it('should handle graph with no subgraphs', () => {
const graph = new LGraph()
const registry = new Map<UUID, any>()
const registry = new Map<UUID, LGraph>()
const result = findUsedSubgraphIds(graph, registry)
expect(result.size).toBe(0)
@@ -87,7 +87,7 @@ describe.skip('subgraphUtils', () => {
const node2 = createTestSubgraphNode(subgraph2)
subgraph1.add(node2)
const registry = new Map<UUID, any>([
const registry = new Map<UUID, LGraph>([
[subgraph1.id, subgraph1],
[subgraph2.id, subgraph2]
])
@@ -115,7 +115,7 @@ describe.skip('subgraphUtils', () => {
const node3 = createTestSubgraphNode(subgraph1, { id: 3 })
subgraph2.add(node3)
const registry = new Map<UUID, any>([
const registry = new Map<UUID, LGraph>([
[subgraph1.id, subgraph1],
[subgraph2.id, subgraph2]
])
@@ -139,7 +139,7 @@ describe.skip('subgraphUtils', () => {
rootGraph.add(node2)
// Only register subgraph1
const registry = new Map<UUID, any>([[subgraph1.id, subgraph1]])
const registry = new Map<UUID, LGraph>([[subgraph1.id, subgraph1]])
const result = findUsedSubgraphIds(rootGraph, registry)
expect(result.size).toBe(2)

View File

@@ -37,6 +37,14 @@ export interface IWidgetOptions<TValues = unknown[]> {
getOptionLabel?: (value?: string | null) => string
callback?: IWidget['callback']
iconClass?: string
// Vue widget options
disabled?: boolean
useGrouping?: boolean
placeholder?: string
showThumbnails?: boolean
showItemNavigators?: boolean
hidden?: boolean
}
interface IWidgetSliderOptions extends IWidgetOptions<number[]> {

View File

@@ -35,11 +35,10 @@ export class ComboWidget
override get _displayValue() {
if (this.computedDisabled) return ''
if (this.options.getOptionLabel) {
const getOptionLabel = this.options.getOptionLabel
if (getOptionLabel) {
try {
return this.options.getOptionLabel(
this.value ? String(this.value) : null
)
return getOptionLabel(this.value ? String(this.value) : null)
} catch (e) {
console.error('Failed to map value:', e)
return this.value ? String(this.value) : ''
@@ -155,9 +154,12 @@ export class ComboWidget
}
const menu = new LiteGraph.ContextMenu([], menuOptions)
const getOptionLabel = this.options.getOptionLabel
for (const value of values_list) {
try {
const label = this.options.getOptionLabel(String(value))
const label = getOptionLabel
? getOptionLabel(String(value))
: String(value)
menu.addItem(label, value, menuOptions)
} catch (err) {
console.error('Failed to map value:', err)

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "إعادة التشغيل"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة"
},
"Comfy_BrowseModelAssets": {
"label": "تجريبي: تصفح أصول النماذج"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "عرض نافذة الإعدادات"
},
"Comfy_Subgraph_SetDescription": {
"label": "تعيين وصف الرسم البياني الفرعي"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "تعيين الأسماء المستعارة للبحث في الرسم البياني الفرعي"
},
"Comfy_ToggleAssetAPI": {
"label": "تجريبي: تمكين AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "تبديل اللوحة السفلية"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "تبديل لوحة الطرفية السفلية"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "تبديل لوحة السجلات السفلية"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "تبديل اللوحة السفلية الأساسية"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "تدوير لليمين في محرر القناع",
"Save": "حفظ",
"Save As": "حفظ باسم",
"Set Subgraph Description": "تعيين وصف المخطط الفرعي",
"Set Subgraph Search Aliases": "تعيين الأسماء المستعارة للبحث في المخطط الفرعي",
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
"Show Model Selector (Dev)": "إظهار منتقي النماذج (للمطورين)",
"Show Settings Dialog": "عرض نافذة الإعدادات",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "لا يمكن حذف المخططات المثبتة",
"confirmDelete": "سيؤدي هذا الإجراء إلى إزالة المخطط نهائيًا من مكتبتك",
"confirmDeleteTitle": "حذف المخطط؟",
"enterDescription": "أدخل وصفًا",
"enterSearchAliases": "أدخل الأسماء المستعارة للبحث (مفصولة بفواصل)",
"hidden": "معاملات مخفية / متداخلة",
"hideAll": "إخفاء الكل",
"loadFailure": "فشل تحميل مخططات الرسم البياني الفرعي",
@@ -2434,6 +2438,7 @@
"publishSuccess": "تم الحفظ في مكتبة العقد",
"publishSuccessMessage": "يمكنك العثور على مخطط الرسم البياني الفرعي الخاص بك في مكتبة العقد ضمن \"مخططات الرسم البياني الفرعي\"",
"saveBlueprint": "احفظ المخطط الفرعي في المكتبة",
"searchAliases": "بحث عن الأسماء المستعارة",
"showAll": "إظهار الكل",
"showRecommended": "إظهار العناصر الموصى بها",
"shown": "معروض على العقدة"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "إنشاء قائمة",
"inputs": {
"inputs": {
"name": "المدخلات"
}
},
"outputs": {
"0": {
"name": "قائمة",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "إنشاء فيديو من الصور.",
"display_name": "إنشاء فيديو",
@@ -13940,6 +13954,9 @@
"bpm": {
"name": "الإيقاع (BPM)"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13949,6 +13966,10 @@
"duration": {
"name": "المدة"
},
"generate_audio_codes": {
"name": "توليد رموز الصوت",
"tooltip": "تفعيل نموذج اللغة الكبير (LLM) الذي يولد رموز الصوت. قد يكون بطيئًا لكنه سيزيد من جودة الصوت الناتج. قم بإيقاف هذا الخيار إذا كنت تقدم مرجعًا صوتيًا للنموذج."
},
"keyscale": {
"name": "المقام الموسيقي"
},
@@ -13964,8 +13985,17 @@
"tags": {
"name": "الوسوم"
},
"temperature": {
"name": "درجة الحرارة"
},
"timesignature": {
"name": "توقيع الإيقاع"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14900,6 +14930,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "فك ترميز الصوت بواسطة VAE (مجزأ)",
"inputs": {
"overlap": {
"name": "تداخل"
},
"samples": {
"name": "عينات"
},
"tile_size": {
"name": "حجم التجزئة"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "فك ترميز VAE Hunyuan3D",
"inputs": {

View File

@@ -263,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "Show Settings Dialog"
},
"Comfy_Subgraph_SetDescription": {
"label": "Set Subgraph Description"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "Set Subgraph Search Aliases"
},
"Comfy_ToggleAssetAPI": {
"label": "Experimental: Enable AssetAPI"
},

View File

@@ -10,6 +10,7 @@
"download": "Download",
"downloadImage": "Download image",
"downloadVideo": "Download video",
"downloadAudio": "Download audio",
"editOrMaskImage": "Edit or mask image",
"editImage": "Edit image",
"decrement": "Decrement",
@@ -754,6 +755,8 @@
"noFilesFound": "No files found",
"noImportedFiles": "No imported files found",
"noGeneratedFiles": "No generated files found",
"noQueueItems": "No active jobs",
"noQueueItemsMessage": "Queue a prompt to see active jobs here",
"generatedAssetsHeader": "Generated assets",
"importedAssetsHeader": "Imported assets",
"activeJobStatus": "Active job: {status}",
@@ -1250,6 +1253,8 @@
"Save": "Save",
"Save As": "Save As",
"Show Settings Dialog": "Show Settings Dialog",
"Set Subgraph Description": "Set Subgraph Description",
"Set Subgraph Search Aliases": "Set Subgraph Search Aliases",
"Experimental: Enable AssetAPI": "Experimental: Enable AssetAPI",
"Canvas Performance": "Canvas Performance",
"Help Center": "Help Center",

View File

@@ -2020,6 +2020,20 @@
}
}
},
"CreateList": {
"display_name": "Create List",
"inputs": {
"inputs": {
"name": "inputs"
}
},
"outputs": {
"0": {
"name": "list",
"tooltip": null
}
}
},
"CreateVideo": {
"display_name": "Create Video",
"description": "Create a video from images.",
@@ -6104,6 +6118,10 @@
"5": {
"name": "recording_video",
"tooltip": null
},
"6": {
"name": "model_3d",
"tooltip": null
}
}
},
@@ -11488,8 +11506,8 @@
}
},
"ReferenceTimbreAudio": {
"display_name": "ReferenceTimbreAudio",
"description": "This node sets the reference audio for timbre (for ace step 1.5)",
"display_name": "Reference Audio",
"description": "This node sets the reference audio for ace step 1.5",
"inputs": {
"conditioning": {
"name": "conditioning"
@@ -12541,11 +12559,11 @@
}
},
"SaveGLB": {
"display_name": "SaveGLB",
"display_name": "Save 3D Model",
"inputs": {
"mesh": {
"name": "mesh",
"tooltip": "Mesh or GLB file to save"
"tooltip": "Mesh or 3D file to save"
},
"filename_prefix": {
"name": "filename_prefix"
@@ -14053,6 +14071,22 @@
"keyscale": {
"name": "keyscale"
},
"generate_audio_codes": {
"name": "generate_audio_codes",
"tooltip": "Enable the LLM that generates audio codes. This can be slow but will increase the quality of the generated audio. Turn this off if you are giving the model an audio reference."
},
"cfg_scale": {
"name": "cfg_scale"
},
"temperature": {
"name": "temperature"
},
"top_p": {
"name": "top_p"
},
"top_k": {
"name": "top_k"
},
"control_after_generate": {
"name": "control after generate"
}
@@ -15056,6 +15090,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE Decode Audio (Tiled)",
"inputs": {
"samples": {
"name": "samples"
},
"vae": {
"name": "vae"
},
"tile_size": {
"name": "tile_size"
},
"overlap": {
"name": "overlap"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "Reiniciar"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Abrir visor 3D (Beta) para el nodo seleccionado"
},
"Comfy_BrowseModelAssets": {
"label": "Experimental: Explorar recursos de modelos"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "Mostrar Diálogo de Configuraciones"
},
"Comfy_Subgraph_SetDescription": {
"label": "Establecer descripción del subgrafo"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "Establecer alias de búsqueda del subgrafo"
},
"Comfy_ToggleAssetAPI": {
"label": "Experimental: Habilitar AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "Alternar Panel Inferior"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Alternar Panel Inferior de Terminal"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Alternar Panel Inferior de Registros"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Alternar panel inferior esencial"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "Girar a la derecha en el editor de máscaras",
"Save": "Guardar",
"Save As": "Guardar como",
"Set Subgraph Description": "Establecer descripción del subgrafo",
"Set Subgraph Search Aliases": "Establecer alias de búsqueda del subgrafo",
"Show Keybindings Dialog": "Mostrar diálogo de combinaciones de teclas",
"Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)",
"Show Settings Dialog": "Mostrar diálogo de configuración",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "No se pueden eliminar los blueprints instalados",
"confirmDelete": "Esta acción eliminará permanentemente el subgrafo de tu biblioteca",
"confirmDeleteTitle": "¿Eliminar subgrafo?",
"enterDescription": "Introduce una descripción",
"enterSearchAliases": "Introduce alias de búsqueda (separados por comas)",
"hidden": "Parámetros ocultos/anidados",
"hideAll": "Ocultar todo",
"loadFailure": "No se pudieron cargar los subgrafos",
@@ -2434,6 +2438,7 @@
"publishSuccess": "Guardado en la biblioteca de nodos",
"publishSuccessMessage": "Puedes encontrar tu subgrafo en la biblioteca de nodos bajo \"Subgraph Blueprints\"",
"saveBlueprint": "Guardar subgrafo en la biblioteca",
"searchAliases": "Buscar alias",
"showAll": "Mostrar todo",
"showRecommended": "Mostrar widgets recomendados",
"shown": "Mostrado en el nodo"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "Crear lista",
"inputs": {
"inputs": {
"name": "entradas"
}
},
"outputs": {
"0": {
"name": "lista",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "Crea un video a partir de imágenes.",
"display_name": "Crear video",
@@ -6418,9 +6432,7 @@
"Load3D": {
"display_name": "Cargar 3D",
"inputs": {
"clear": {
"": "limpiar"
},
"clear": {},
"height": {
"name": "alto"
},
@@ -6431,10 +6443,10 @@
"name": "archivo_modelo"
},
"upload 3d model": {
"": "subir modelo 3D"
"es": "Subir modelo 3D"
},
"upload extra resources": {
"": "subir recursos adicionales"
"es": "Subir recursos adicionales"
},
"width": {
"name": "ancho"
@@ -13946,6 +13958,9 @@
"bpm": {
"name": "bpm"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13955,6 +13970,10 @@
"duration": {
"name": "duración"
},
"generate_audio_codes": {
"name": "generar_códigos_de_audio",
"tooltip": "Activa el LLM que genera códigos de audio. Esto puede ser lento, pero aumentará la calidad del audio generado. Desactívalo si proporcionas una referencia de audio al modelo."
},
"keyscale": {
"name": "escala tonal"
},
@@ -13970,8 +13989,17 @@
"tags": {
"name": "etiquetas"
},
"temperature": {
"name": "temperatura"
},
"timesignature": {
"name": "compás"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14906,6 +14934,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE Decodificar audio (en mosaico)",
"inputs": {
"overlap": {
"name": "superposición"
},
"samples": {
"name": "muestras"
},
"tile_size": {
"name": "tamaño_de_mosaico"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "راه‌اندازی مجدد"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "باز کردن ۳D Viewer (بتا) برای node انتخاب‌شده"
},
"Comfy_BrowseModelAssets": {
"label": "آزمایشی: مرور Model Assets"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "نمایش پنجره تنظیمات"
},
"Comfy_Subgraph_SetDescription": {
"label": "تنظیم توضیحات زیرگراف"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "تنظیم نام‌های جایگزین جستجوی زیرگراف"
},
"Comfy_ToggleAssetAPI": {
"label": "آزمایشی: فعال‌سازی AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "تغییر پنل پایین"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "تغییر پنل ترمینال پایین"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "تغییر پنل گزارش‌ها پایین"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "تغییر پنل ضروریات پایین"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "چرخش به راست در MaskEditor",
"Save": "ذخیره",
"Save As": "ذخیره به عنوان",
"Set Subgraph Description": "تنظیم توضیح زیرگراف",
"Set Subgraph Search Aliases": "تنظیم نام‌های مستعار جستجوی زیرگراف",
"Show Keybindings Dialog": "نمایش پنجره کلیدهای میانبر",
"Show Model Selector (Dev)": "نمایش انتخاب‌گر مدل (توسعه‌دهنده)",
"Show Settings Dialog": "نمایش پنجره تنظیمات",
@@ -2435,6 +2437,8 @@
"cannotDeleteGlobal": "امکان حذف blueprints نصب‌شده وجود ندارد",
"confirmDelete": "این عمل باعث حذف دائمی بلوپرینت از کتابخانه شما می‌شود",
"confirmDeleteTitle": "حذف بلوپرینت؟",
"enterDescription": "توضیحی وارد کنید",
"enterSearchAliases": "نام‌های مستعار جستجو را وارد کنید (با ویرگول جدا کنید)",
"hidden": "پارامترهای مخفی / تو در تو",
"hideAll": "مخفی‌سازی همه",
"loadFailure": "بارگذاری بلوپرینت‌های زیرگراف ناموفق بود",
@@ -2445,6 +2449,7 @@
"publishSuccess": "در کتابخانه گره‌ها ذخیره شد",
"publishSuccessMessage": "می‌توانید بلوپرینت زیرگراف خود را در کتابخانه گره‌ها در بخش \"بلوپرینت‌های زیرگراف\" پیدا کنید",
"saveBlueprint": "ذخیره زیرگراف در کتابخانه",
"searchAliases": "جستجوی نام‌های مستعار",
"showAll": "نمایش همه",
"showRecommended": "نمایش ویجت‌های پیشنهادی",
"shown": "نمایش روی گره"

View File

@@ -2020,6 +2020,20 @@
}
}
},
"CreateList": {
"display_name": "ایجاد لیست",
"inputs": {
"inputs": {
"name": "ورودی‌ها"
}
},
"outputs": {
"0": {
"name": "لیست",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "ایجاد ویدیو از تصاویر.",
"display_name": "ایجاد ویدیو",
@@ -6449,32 +6463,18 @@
"name": "عرض"
}
},
"outputs": {
"0": {
"name": "تصویر",
"tooltip": null
},
"1": {
"name": "ماسک",
"tooltip": null
},
"2": {
"name": "مسیر مش",
"tooltip": null
},
"3": {
"name": "نرمال",
"tooltip": null
},
"4": {
"name": "اطلاعات دوربین",
"tooltip": null
},
"5": {
"name": "ویدئوی ضبط‌شده",
"outputs": [
null,
null,
null,
null,
null,
null,
{
"name": "مدل_۳بعدی",
"tooltip": null
}
}
]
},
"LoadAudio": {
"display_name": "بارگذاری صوت",
@@ -13973,6 +13973,9 @@
"bpm": {
"name": "ضرب در دقیقه (BPM)"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13982,6 +13985,10 @@
"duration": {
"name": "مدت زمان"
},
"generate_audio_codes": {
"name": "تولید کدهای صوتی",
"tooltip": "فعال‌سازی LLM برای تولید کدهای صوتی. این کار ممکن است کند باشد اما کیفیت صدای تولیدشده را افزایش می‌دهد. اگر به مدل یک مرجع صوتی می‌دهید، این گزینه را غیرفعال کنید."
},
"keyscale": {
"name": "گام"
},
@@ -13997,8 +14004,17 @@
"tags": {
"name": "برچسب‌ها"
},
"temperature": {
"name": "temperature"
},
"timesignature": {
"name": "امضای زمانی"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14933,6 +14949,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "رمزگشایی VAE صوتی (کاشی‌بندی‌شده)",
"inputs": {
"overlap": {
"name": "همپوشانی"
},
"samples": {
"name": "نمونه‌ها"
},
"tile_size": {
"name": "اندازه کاشی"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "Redémarrer"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Ouvrir le visualiseur 3D (bêta) pour le nœud sélectionné"
},
"Comfy_BrowseModelAssets": {
"label": "Expérimental : Parcourir les ressources de modèles"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "Afficher la boîte de dialogue des paramètres"
},
"Comfy_Subgraph_SetDescription": {
"label": "Définir la description du sous-graphe"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "Définir les alias de recherche du sous-graphe"
},
"Comfy_ToggleAssetAPI": {
"label": "Expérimental : Activer AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "Basculer le panneau inférieur"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Basculer le panneau inférieur du terminal"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Basculer le panneau inférieur des journaux"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Afficher/Masquer le panneau inférieur essentiel"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "Tourner à droite dans l'éditeur de masque",
"Save": "Enregistrer",
"Save As": "Enregistrer sous",
"Set Subgraph Description": "Définir la description du sous-graphe",
"Set Subgraph Search Aliases": "Définir les alias de recherche du sous-graphe",
"Show Keybindings Dialog": "Afficher la boîte de dialogue des raccourcis clavier",
"Show Model Selector (Dev)": "Afficher le sélecteur de modèle (Dev)",
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "Impossible de supprimer les blueprints installés",
"confirmDelete": "Cette action supprimera définitivement le plan de votre bibliothèque",
"confirmDeleteTitle": "Supprimer le plan ?",
"enterDescription": "Saisissez une description",
"enterSearchAliases": "Saisissez des alias de recherche (séparés par des virgules)",
"hidden": "Paramètres cachés / imbriqués",
"hideAll": "Tout masquer",
"loadFailure": "Échec du chargement des plans de sous-graphes",
@@ -2434,6 +2438,7 @@
"publishSuccess": "Enregistré dans la bibliothèque de nœuds",
"publishSuccessMessage": "Vous pouvez trouver votre plan de sous-graphe dans la bibliothèque de nœuds sous \"Plans de sous-graphes\"",
"saveBlueprint": "Enregistrer le sous-graphe dans la bibliothèque",
"searchAliases": "Rechercher des alias",
"showAll": "Tout afficher",
"showRecommended": "Afficher les widgets recommandés",
"shown": "Affiché sur le nœud"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "Créer une liste",
"inputs": {
"inputs": {
"name": "entrées"
}
},
"outputs": {
"0": {
"name": "liste",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "Créer une vidéo à partir dimages.",
"display_name": "Créer une vidéo",
@@ -13946,6 +13960,9 @@
"bpm": {
"name": "bpm"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13955,6 +13972,10 @@
"duration": {
"name": "durée"
},
"generate_audio_codes": {
"name": "générer des codes audio",
"tooltip": "Activez le LLM qui génère des codes audio. Cela peut être lent mais augmentera la qualité de laudio généré. Désactivez cette option si vous fournissez une référence audio au modèle."
},
"keyscale": {
"name": "tonalité"
},
@@ -13970,8 +13991,17 @@
"tags": {
"name": "tags"
},
"temperature": {
"name": "température"
},
"timesignature": {
"name": "signature rythmique"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14906,6 +14936,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "Décoder Audio VAE (par tuiles)",
"inputs": {
"overlap": {
"name": "chevauchement"
},
"samples": {
"name": "échantillons"
},
"tile_size": {
"name": "taille de tuile"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "再起動"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "選択したードの3Dビューアーベータを開く"
},
"Comfy_BrowseModelAssets": {
"label": "実験的: モデルアセットを参照"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "設定ダイアログを表示"
},
"Comfy_Subgraph_SetDescription": {
"label": "サブグラフの説明を設定"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "サブグラフの検索エイリアスを設定"
},
"Comfy_ToggleAssetAPI": {
"label": "実験的: AssetAPIを有効化"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "パネル下部の切り替え"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "ターミナルパネル下部の切り替え"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "ログパネル下部の切り替え"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "必須な下部パネルを切り替え"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "マスクエディタで右に回転",
"Save": "保存",
"Save As": "名前を付けて保存",
"Set Subgraph Description": "サブグラフの説明を設定",
"Set Subgraph Search Aliases": "サブグラフの検索エイリアスを設定",
"Show Keybindings Dialog": "キーバインドダイアログを表示",
"Show Model Selector (Dev)": "モデルセレクターを表示(開発用)",
"Show Settings Dialog": "設定ダイアログを表示",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "インストール済みのブループリントは削除できません",
"confirmDelete": "この操作により、ライブラリからサブグラフが完全に削除されます",
"confirmDeleteTitle": "サブグラフを削除しますか?",
"enterDescription": "説明を入力してください",
"enterSearchAliases": "検索用エイリアスを入力(カンマ区切り)",
"hidden": "非表示/ネストされたパラメータ",
"hideAll": "すべて非表示",
"loadFailure": "サブグラフの読み込みに失敗しました",
@@ -2434,6 +2438,7 @@
"publishSuccess": "ノードライブラリに保存されました",
"publishSuccessMessage": "サブグラフはノードライブラリの「サブグラフブループリント」で見つけることができます",
"saveBlueprint": "サブグラフをライブラリに保存",
"searchAliases": "エイリアスを検索",
"showAll": "すべて表示",
"showRecommended": "おすすめウィジェットを表示",
"shown": "ノード上で表示"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "リストを作成",
"inputs": {
"inputs": {
"name": "入力"
}
},
"outputs": {
"0": {
"name": "リスト",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "画像から動画を作成します。",
"display_name": "動画を作成",
@@ -6418,9 +6432,7 @@
"Load3D": {
"display_name": "3Dを読み込む",
"inputs": {
"clear": {
"": "クリア"
},
"clear": "クリア",
"height": {
"name": "高さ"
},
@@ -6430,12 +6442,8 @@
"model_file": {
"name": "モデルファイル"
},
"upload 3d model": {
"": "3Dモデルをアップロード"
},
"upload extra resources": {
"": "追加リソースをアップロード"
},
"upload 3d model": "3Dモデルをアップロード",
"upload extra resources": "追加リソースをアップロード",
"width": {
"name": "幅"
}
@@ -13946,6 +13954,9 @@
"bpm": {
"name": "BPM"
},
"cfg_scale": {
"name": "cfgスケール"
},
"clip": {
"name": "clip"
},
@@ -13955,6 +13966,10 @@
"duration": {
"name": "長さ"
},
"generate_audio_codes": {
"name": "オーディオコードを生成",
"tooltip": "オーディオコードを生成するLLMを有効にします。これにより生成されるオーディオの品質が向上しますが、処理が遅くなる場合があります。モデルにオーディオリファレンスを与える場合はオフにしてください。"
},
"keyscale": {
"name": "キー"
},
@@ -13970,8 +13985,17 @@
"tags": {
"name": "タグ"
},
"temperature": {
"name": "温度"
},
"timesignature": {
"name": "拍子"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14906,6 +14930,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAEデコードオーディオタイル",
"inputs": {
"overlap": {
"name": "オーバーラップ"
},
"samples": {
"name": "サンプル"
},
"tile_size": {
"name": "タイルサイズ"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "재시작"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "선택한 노드에 대해 3D 뷰어(베타) 열기"
},
"Comfy_BrowseModelAssets": {
"label": "실험적: 모델 에셋 탐색"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "설정 대화상자 보기"
},
"Comfy_Subgraph_SetDescription": {
"label": "서브그래프 설명 설정"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "서브그래프 검색 별칭 설정"
},
"Comfy_ToggleAssetAPI": {
"label": "실험적: AssetAPI 활성화"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "하단 패널 토글"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "터미널 하단 패널 토글"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "로그 하단 패널 토글"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "필수 하단 패널 전환"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "마스크 편집기에서 오른쪽으로 회전",
"Save": "저장",
"Save As": "다른 이름으로 저장",
"Set Subgraph Description": "서브그래프 설명 설정",
"Set Subgraph Search Aliases": "서브그래프 검색 별칭 설정",
"Show Keybindings Dialog": "단축키 대화상자 표시",
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
"Show Settings Dialog": "설정 대화상자 표시",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "설치된 블루프린트는 삭제할 수 없습니다",
"confirmDelete": "이 작업은 라이브러리에서 블루프린트를 영구적으로 제거합니다",
"confirmDeleteTitle": "블루프린트를 삭제하시겠습니까?",
"enterDescription": "설명을 입력하세요",
"enterSearchAliases": "검색 별칭을 입력하세요 (쉼표로 구분)",
"hidden": "숨김 / 중첩 매개변수",
"hideAll": "모두 숨김",
"loadFailure": "서브그래프 블루프린트 로드 실패",
@@ -2434,6 +2438,7 @@
"publishSuccess": "노드 라이브러리에 저장됨",
"publishSuccessMessage": "노드 라이브러리의 \"서브그래프 블루프린트\" 아래에서 서브그래프 블루프린트를 찾을 수 있습니다",
"saveBlueprint": "서브그래프를 라이브러리에 저장",
"searchAliases": "별칭 검색",
"showAll": "모두 표시",
"showRecommended": "권장 위젯 표시",
"shown": "노드에 표시됨"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "목록 만들기",
"inputs": {
"inputs": {
"name": "입력값"
}
},
"outputs": {
"0": {
"name": "목록",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "이미지로부터 비디오를 생성합니다.",
"display_name": "비디오 생성",
@@ -6418,7 +6432,7 @@
"Load3D": {
"display_name": "3D 불러오기",
"inputs": {
"clear": {},
"clear": "지우기",
"height": {
"name": "높이"
},
@@ -6428,12 +6442,8 @@
"model_file": {
"name": "모델 파일"
},
"upload 3d model": {
"ko": "3D 모델 업로드"
},
"upload extra resources": {
"ko": "추가 리소스 업로드"
},
"upload 3d model": "3D 모델 업로드",
"upload extra resources": "추가 리소스 업로드",
"width": {
"name": "너비"
}
@@ -13944,6 +13954,9 @@
"bpm": {
"name": "BPM"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13953,6 +13966,10 @@
"duration": {
"name": "길이"
},
"generate_audio_codes": {
"name": "오디오 코드 생성",
"tooltip": "오디오 코드를 생성하는 LLM을 활성화합니다. 느릴 수 있지만 생성된 오디오의 품질이 향상됩니다. 모델에 오디오 참조를 제공하는 경우 이 옵션을 끄세요."
},
"keyscale": {
"name": "조성"
},
@@ -13968,8 +13985,17 @@
"tags": {
"name": "태그"
},
"temperature": {
"name": "temperature"
},
"timesignature": {
"name": "박자"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14904,6 +14930,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE 오디오 디코드 (타일)",
"inputs": {
"overlap": {
"name": "오버랩"
},
"samples": {
"name": "샘플"
},
"tile_size": {
"name": "타일 크기"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "Reiniciar"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Abrir visualizador 3D (Beta) para o nó selecionado"
},
"Comfy_BrowseModelAssets": {
"label": "Experimental: Navegar pelos ativos de modelo"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "Mostrar Diálogo de Configurações"
},
"Comfy_Subgraph_SetDescription": {
"label": "Definir descrição do subgrafo"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "Definir aliases de pesquisa do subgrafo"
},
"Comfy_ToggleAssetAPI": {
"label": "Experimental: Ativar AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "Alternar painel inferior"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Alternar painel inferior do terminal"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Alternar painel inferior de logs"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Alternar painel inferior essencial"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "Girar para a direita no MaskEditor",
"Save": "Salvar",
"Save As": "Salvar como",
"Set Subgraph Description": "Definir Descrição do Subgrafo",
"Set Subgraph Search Aliases": "Definir Apelidos de Busca do Subgrafo",
"Show Keybindings Dialog": "Mostrar diálogo de atalhos",
"Show Model Selector (Dev)": "Mostrar seletor de modelo (Dev)",
"Show Settings Dialog": "Mostrar diálogo de configurações",
@@ -2435,6 +2437,8 @@
"cannotDeleteGlobal": "Não é possível excluir blueprints instalados",
"confirmDelete": "Esta ação removerá permanentemente o blueprint da sua biblioteca",
"confirmDeleteTitle": "Excluir blueprint?",
"enterDescription": "Insira uma descrição",
"enterSearchAliases": "Insira apelidos de busca (separados por vírgula)",
"hidden": "Parâmetros ocultos/aninhados",
"hideAll": "Ocultar tudo",
"loadFailure": "Falha ao carregar blueprints de subgrafo",
@@ -2445,6 +2449,7 @@
"publishSuccess": "Salvo na Biblioteca de Nós",
"publishSuccessMessage": "Você pode encontrar seu blueprint de subgrafo na biblioteca de nós em \"Blueprints de Subgrafo\"",
"saveBlueprint": "Salvar Subgrafo na Biblioteca",
"searchAliases": "Buscar Apelidos",
"showAll": "Mostrar tudo",
"showRecommended": "Mostrar widgets recomendados",
"shown": "Exibido no nó"

View File

@@ -2020,6 +2020,20 @@
}
}
},
"CreateList": {
"display_name": "Criar Lista",
"inputs": {
"inputs": {
"name": "entradas"
}
},
"outputs": {
"0": {
"name": "lista",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "Crie um vídeo a partir de imagens.",
"display_name": "Criar Vídeo",
@@ -6449,32 +6463,18 @@
"name": "largura"
}
},
"outputs": {
"0": {
"name": "imagem",
"tooltip": null
},
"1": {
"name": "mask",
"tooltip": null
},
"2": {
"name": "caminho_malha",
"tooltip": null
},
"3": {
"name": "normal",
"tooltip": null
},
"4": {
"name": "info_câmera",
"tooltip": null
},
"5": {
"name": "vídeo_gravado",
"outputs": [
null,
null,
null,
null,
null,
null,
{
"name": "model_3d",
"tooltip": null
}
}
]
},
"LoadAudio": {
"display_name": "Carregar Áudio",
@@ -13973,6 +13973,9 @@
"bpm": {
"name": "bpm"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13982,6 +13985,10 @@
"duration": {
"name": "duração"
},
"generate_audio_codes": {
"name": "gerar_códigos_de_áudio",
"tooltip": "Ativa o LLM que gera códigos de áudio. Isso pode ser lento, mas aumentará a qualidade do áudio gerado. Desative se você fornecer uma referência de áudio ao modelo."
},
"keyscale": {
"name": "escala tonal"
},
@@ -13997,8 +14004,17 @@
"tags": {
"name": "tags"
},
"temperature": {
"name": "temperatura"
},
"timesignature": {
"name": "fórmula de compasso"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14933,6 +14949,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE Decodificar Áudio (Em Blocos)",
"inputs": {
"overlap": {
"name": "sobreposição"
},
"samples": {
"name": "amostras"
},
"tile_size": {
"name": "tamanho_do_bloco"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "Перезапустить"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Открыть 3D-просмотрщик (бета) для выбранного узла"
},
"Comfy_BrowseModelAssets": {
"label": "Экспериментально: Просмотр ресурсов моделей"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "Показать диалог настроек"
},
"Comfy_Subgraph_SetDescription": {
"label": "Установить описание подграфа"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "Установить поисковые псевдонимы подграфа"
},
"Comfy_ToggleAssetAPI": {
"label": "Экспериментально: Включить AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "Переключить нижнюю панель"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Переключить нижнюю панель терминала"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Переключить нижнюю панель логов"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Показать/скрыть основную нижнюю панель"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "Повернуть вправо в MaskEditor",
"Save": "Сохранить",
"Save As": "Сохранить как",
"Set Subgraph Description": "Установить описание подграфа",
"Set Subgraph Search Aliases": "Установить псевдонимы поиска подграфа",
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
"Show Model Selector (Dev)": "Показать выбор модели (Dev)",
"Show Settings Dialog": "Показать диалог настроек",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "Невозможно удалить установленные blueprints",
"confirmDelete": "Это действие навсегда удалит подграф из вашей библиотеки",
"confirmDeleteTitle": "Удалить подграф?",
"enterDescription": "Введите описание",
"enterSearchAliases": "Введите псевдонимы для поиска (через запятую)",
"hidden": "Скрытые / вложенные параметры",
"hideAll": "Скрыть всё",
"loadFailure": "Не удалось загрузить схемы подграфов",
@@ -2434,6 +2438,7 @@
"publishSuccess": "Сохранено в библиотеку узлов",
"publishSuccessMessage": "Вы можете найти свой подграф в библиотеке узлов в разделе «Subgraph Blueprints»",
"saveBlueprint": "Сохранить подграф в библиотеку",
"searchAliases": "Поиск по псевдонимам",
"showAll": "Показать всё",
"showRecommended": "Показать рекомендуемые виджеты",
"shown": "Показано на узле"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "Создать список",
"inputs": {
"inputs": {
"name": "входные данные"
}
},
"outputs": {
"0": {
"name": "список",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "Создайте видео из изображений.",
"display_name": "Создать видео",
@@ -6418,7 +6432,9 @@
"Load3D": {
"display_name": "Загрузить 3D",
"inputs": {
"clear": "Очистить",
"clear": {
"": "Очистить"
},
"height": {
"name": "высота"
},
@@ -6428,8 +6444,12 @@
"model_file": {
"name": "файл модели"
},
"upload 3d model": "Загрузить 3D-модель",
"upload extra resources": "Загрузить дополнительные ресурсы",
"upload 3d model": {
"": "Загрузить 3D-модель"
},
"upload extra resources": {
"": "Загрузить дополнительные ресурсы"
},
"width": {
"name": "ширина"
}
@@ -13940,6 +13960,9 @@
"bpm": {
"name": "bpm"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13949,6 +13972,10 @@
"duration": {
"name": "длительность"
},
"generate_audio_codes": {
"name": "генерировать аудиокоды",
"tooltip": "Включить LLM, который генерирует аудиокоды. Это может быть медленно, но повысит качество сгенерированного аудио. Отключите, если вы предоставляете модели аудиореференс."
},
"keyscale": {
"name": "тональность"
},
@@ -13964,8 +13991,17 @@
"tags": {
"name": "теги"
},
"temperature": {
"name": "температура"
},
"timesignature": {
"name": "размер"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14900,6 +14936,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE декодирование аудио (плитками)",
"inputs": {
"overlap": {
"name": "перекрытие"
},
"samples": {
"name": "образцы"
},
"tile_size": {
"name": "размер плитки"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEDecodeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "Yeniden Başlat"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "Seçili Düğüm için 3D Görüntüleyiciyi (Beta) Aç"
},
"Comfy_BrowseModelAssets": {
"label": "Deneysel: Model Varlıklarını Gözat"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "Ayarlar İletişim Kutusunu Göster"
},
"Comfy_Subgraph_SetDescription": {
"label": "Alt Grafik Açıklamasını Ayarla"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "Alt Grafik Arama Takma Adlarını Ayarla"
},
"Comfy_ToggleAssetAPI": {
"label": "Deneysel: AssetAPI'yi Etkinleştir"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "Alt Paneli Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "Terminal Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "Kayıtlar Alt Panelini Aç/Kapat"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "Temel Alt Paneli Aç/Kapat"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "MaskEditor'da sağa döndür",
"Save": "Kaydet",
"Save As": "Farklı Kaydet",
"Set Subgraph Description": "Alt Grafik Açıklamasını Ayarla",
"Set Subgraph Search Aliases": "Alt Grafik Arama Takma Adlarını Ayarla",
"Show Keybindings Dialog": "Tuş Atamaları İletişim Kutusunu Göster",
"Show Model Selector (Dev)": "Model Seçiciyi Göster (Geliştirici)",
"Show Settings Dialog": "Ayarlar İletişim Kutusunu Göster",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "Yüklü şablonlar silinemez",
"confirmDelete": "Bu işlem taslağı kütüphanenizden kalıcı olarak kaldıracaktır",
"confirmDeleteTitle": "Taslak silinsin mi?",
"enterDescription": "Bir açıklama girin",
"enterSearchAliases": "Arama takma adlarını girin (virgülle ayrılmış)",
"hidden": "Gizli / iç içe parametreler",
"hideAll": "Tümünü gizle",
"loadFailure": "Alt grafik taslakları yüklenemedi",
@@ -2434,6 +2438,7 @@
"publishSuccess": "Düğüm Kütüphanesine Kaydedildi",
"publishSuccessMessage": "Alt grafik taslağınızı düğüm kütüphanesinde \"Alt Grafik Taslakları\" altında bulabilirsiniz",
"saveBlueprint": "Alt Grafiği Kütüphaneye Kaydet",
"searchAliases": "Takma Adlarda Ara",
"showAll": "Tümünü göster",
"showRecommended": "Önerilen widget'ları göster",
"shown": "Düğümde gösterilen"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "Liste Oluştur",
"inputs": {
"inputs": {
"name": "girdiler"
}
},
"outputs": {
"0": {
"name": "liste",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "Görüntülerden bir video oluşturun.",
"display_name": "Video Oluştur",
@@ -13946,6 +13960,9 @@
"bpm": {
"name": "bpm"
},
"cfg_scale": {
"name": "cfg ölçeği"
},
"clip": {
"name": "clip"
},
@@ -13955,6 +13972,10 @@
"duration": {
"name": "süre"
},
"generate_audio_codes": {
"name": "ses kodlarını üret",
"tooltip": "Ses kodlarını üreten LLM'i etkinleştir. Bu işlem yavaş olabilir ancak üretilen sesin kalitesini artırır. Eğer modele bir ses referansı veriyorsanız bunu kapatın."
},
"keyscale": {
"name": "ton anahtarı"
},
@@ -13970,8 +13991,17 @@
"tags": {
"name": "etiketler"
},
"temperature": {
"name": "sıcaklık"
},
"timesignature": {
"name": "zaman imzası"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14906,6 +14936,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE Sesini Çöz (Döşemeli)",
"inputs": {
"overlap": {
"name": "örtüşme"
},
"samples": {
"name": "örnekler"
},
"tile_size": {
"name": "döşeme boyutu"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAEKodÇözmeHunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "重新啟動"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "為選取的節點開啟 3D 檢視器Beta"
},
"Comfy_BrowseModelAssets": {
"label": "實驗性:瀏覽模型資源"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "顯示設定對話框"
},
"Comfy_Subgraph_SetDescription": {
"label": "設定子圖描述"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "設定子圖搜尋別名"
},
"Comfy_ToggleAssetAPI": {
"label": "實驗性:啟用 AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "切換下方面板"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "切換終端機底部面板"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "切換日誌底部面板"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "切換基本下方面板"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "在遮罩編輯器中向右旋轉",
"Save": "儲存",
"Save As": "另存新檔",
"Set Subgraph Description": "設定子圖描述",
"Set Subgraph Search Aliases": "設定子圖搜尋別名",
"Show Keybindings Dialog": "顯示快捷鍵對話框",
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
"Show Settings Dialog": "顯示設定對話框",
@@ -2424,6 +2426,8 @@
"cannotDeleteGlobal": "無法刪除已安裝的藍圖",
"confirmDelete": "此操作將永久從您的程式庫中移除藍圖",
"confirmDeleteTitle": "刪除藍圖?",
"enterDescription": "輸入描述",
"enterSearchAliases": "輸入搜尋別名(以逗號分隔)",
"hidden": "隱藏 / 巢狀參數",
"hideAll": "全部隱藏",
"loadFailure": "載入子圖藍圖失敗",
@@ -2434,6 +2438,7 @@
"publishSuccess": "已儲存至節點庫",
"publishSuccessMessage": "您可以在節點庫的「子圖藍圖」中找到您的子圖藍圖",
"saveBlueprint": "將子圖儲存到資料庫",
"searchAliases": "搜尋別名",
"showAll": "顯示全部",
"showRecommended": "顯示建議的小工具",
"shown": "在節點上顯示"

View File

@@ -2018,6 +2018,20 @@
}
}
},
"CreateList": {
"display_name": "建立清單",
"inputs": {
"inputs": {
"name": "輸入"
}
},
"outputs": {
"0": {
"name": "清單",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "從圖片建立影片。",
"display_name": "建立影片",
@@ -13944,6 +13958,9 @@
"bpm": {
"name": "每分鐘節拍數"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13953,6 +13970,10 @@
"duration": {
"name": "時長"
},
"generate_audio_codes": {
"name": "產生音訊編碼",
"tooltip": "啟用產生音訊編碼的 LLM。這可能會較慢但能提升產生音訊的品質。如果你已提供模型音訊參考請關閉此選項。"
},
"keyscale": {
"name": "調性"
},
@@ -13968,8 +13989,17 @@
"tags": {
"name": "標籤"
},
"temperature": {
"name": "temperature"
},
"timesignature": {
"name": "拍號"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14904,6 +14934,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE 解碼音訊(分塊)",
"inputs": {
"overlap": {
"name": "重疊"
},
"samples": {
"name": "樣本"
},
"tile_size": {
"name": "分塊大小"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAE 解碼 Hunyuan3D",
"inputs": {

View File

@@ -35,9 +35,6 @@
"Comfy-Desktop_Restart": {
"label": "重启"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "为所选节点开启 3D 浏览器Beta 版)"
},
"Comfy_BrowseModelAssets": {
"label": "实验性:浏览模型资源"
},
@@ -266,6 +263,12 @@
"Comfy_ShowSettingsDialog": {
"label": "显示设置对话框"
},
"Comfy_Subgraph_SetDescription": {
"label": "设置子图描述"
},
"Comfy_Subgraph_SetSearchAliases": {
"label": "设置子图搜索别名"
},
"Comfy_ToggleAssetAPI": {
"label": "实验性:启用 AssetAPI"
},
@@ -311,12 +314,6 @@
"Workspace_ToggleBottomPanel": {
"label": "切换底部面板"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "切换终端底部面板"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "切换日志底部面板"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "切换基本下方面板"
},

View File

@@ -1686,6 +1686,8 @@
"Rotate Right in MaskEditor": "在蒙版编辑器中向右旋转",
"Save": "保存",
"Save As": "另存为",
"Set Subgraph Description": "设置子图描述",
"Set Subgraph Search Aliases": "设置子图搜索别名",
"Show Keybindings Dialog": "显示快捷键对话框",
"Show Model Selector (Dev)": "显示模型选择器(开发用)",
"Show Settings Dialog": "显示设置对话框",
@@ -2435,6 +2437,8 @@
"cannotDeleteGlobal": "无法删除已安装的蓝图",
"confirmDelete": "此操作将永久从您的库中移除该子工作流",
"confirmDeleteTitle": "删除子工作流?",
"enterDescription": "输入描述",
"enterSearchAliases": "输入搜索别名(用逗号分隔)",
"hidden": "隐藏/嵌套参数",
"hideAll": "全部隐藏",
"loadFailure": "加载子工作流蓝图失败",
@@ -2445,6 +2449,7 @@
"publishSuccess": "已保存到节点库",
"publishSuccessMessage": "您可以在节点库的“子工作流蓝图”下找到您的子工作流蓝图",
"saveBlueprint": "保存子工作流到节点库",
"searchAliases": "搜索别名",
"showAll": "全部显示",
"showRecommended": "显示推荐控件",
"shown": "节点上显示"

View File

@@ -2020,6 +2020,20 @@
}
}
},
"CreateList": {
"display_name": "创建列表",
"inputs": {
"inputs": {
"name": "输入"
}
},
"outputs": {
"0": {
"name": "列表",
"tooltip": null
}
}
},
"CreateVideo": {
"description": "从图像创建视频。",
"display_name": "创建视频",
@@ -6443,32 +6457,18 @@
"name": "宽度"
}
},
"outputs": {
"0": {
"name": "图像",
"tooltip": null
},
"1": {
"name": "遮罩",
"tooltip": null
},
"2": {
"name": "网格路径",
"tooltip": null
},
"3": {
"name": "法向",
"tooltip": null
},
"4": {
"name": "线条",
"tooltip": null
},
"5": {
"name": "相机信息",
"outputs": [
null,
null,
null,
null,
null,
null,
{
"name": "model_3d",
"tooltip": null
}
}
]
},
"LoadAudio": {
"display_name": "加载音频",
@@ -13967,6 +13967,9 @@
"bpm": {
"name": "每分钟节拍数"
},
"cfg_scale": {
"name": "cfg_scale"
},
"clip": {
"name": "clip"
},
@@ -13976,6 +13979,10 @@
"duration": {
"name": "时长"
},
"generate_audio_codes": {
"name": "生成音频代码",
"tooltip": "启用生成音频代码的LLM。这可能较慢但会提升生成音频的质量。如果你为模型提供了音频参考请关闭此选项。"
},
"keyscale": {
"name": "调式"
},
@@ -13991,8 +13998,17 @@
"tags": {
"name": "标签"
},
"temperature": {
"name": "temperature"
},
"timesignature": {
"name": "拍号"
},
"top_k": {
"name": "top_k"
},
"top_p": {
"name": "top_p"
}
},
"outputs": {
@@ -14927,6 +14943,28 @@
}
}
},
"VAEDecodeAudioTiled": {
"display_name": "VAE解码音频分块",
"inputs": {
"overlap": {
"name": "重叠"
},
"samples": {
"name": "样本"
},
"tile_size": {
"name": "分块大小"
},
"vae": {
"name": "vae"
}
},
"outputs": {
"0": {
"tooltip": null
}
}
},
"VAEDecodeHunyuan3D": {
"display_name": "VAE解码Hunyuan3D",
"inputs": {

View File

@@ -65,15 +65,8 @@ export function useMediaAssetActions() {
try {
const filename = targetAsset.name
let downloadUrl: string
// In cloud, use preview_url directly (from cloud storage)
// In OSS/localhost, use the /view endpoint
if (isCloud && targetAsset.preview_url) {
downloadUrl = targetAsset.preview_url
} else {
downloadUrl = getAssetUrl(targetAsset)
}
// Prefer preview_url (already includes subfolder) with getAssetUrl as fallback
const downloadUrl = targetAsset.preview_url || getAssetUrl(targetAsset)
downloadFile(downloadUrl, filename)
@@ -103,15 +96,8 @@ export function useMediaAssetActions() {
try {
assets.forEach((asset) => {
const filename = asset.name
let downloadUrl: string
// In cloud, use preview_url directly (from GCS or other cloud storage)
// In OSS/localhost, use the /view endpoint
if (isCloud && asset.preview_url) {
downloadUrl = asset.preview_url
} else {
downloadUrl = getAssetUrl(asset)
}
// Prefer preview_url (already includes subfolder) with getAssetUrl as fallback
const downloadUrl = asset.preview_url || getAssetUrl(asset)
downloadFile(downloadUrl, filename)
})

View File

@@ -487,7 +487,7 @@ function createAssetService() {
url: string
name: string
tags?: string[]
user_metadata?: Record<string, any>
user_metadata?: Record<string, unknown>
preview_id?: string
}): Promise<AssetItem & { created_new: boolean }> {
const res = await api.fetchApi(ASSETS_ENDPOINT, {
@@ -525,7 +525,7 @@ function createAssetService() {
data: string
name: string
tags?: string[]
user_metadata?: Record<string, any>
user_metadata?: Record<string, unknown>
}): Promise<AssetItem & { created_new: boolean }> {
// Validate that data is a data URL
if (!params.data || !params.data.startsWith('data:')) {

View File

@@ -8,7 +8,7 @@ import { getAssetType } from './assetTypeUtil'
/**
* Get the download/view URL for an asset
* Constructs the proper URL with filename encoding and type parameter
* Constructs the proper URL with filename encoding, type, and subfolder parameters
*
* @param asset The asset to get URL for
* @param defaultType Default type if asset doesn't have tags (default: 'output')
@@ -23,7 +23,12 @@ export function getAssetUrl(
defaultType: 'input' | 'output' = 'output'
): string {
const assetType = getAssetType(asset, defaultType)
return api.apiURL(
`/view?filename=${encodeURIComponent(asset.name)}&type=${assetType}`
)
const subfolder = asset.user_metadata?.subfolder
const params = new URLSearchParams()
params.set('filename', asset.name)
params.set('type', assetType)
if (typeof subfolder === 'string' && subfolder) {
params.set('subfolder', subfolder)
}
return api.apiURL(`/view?${params}`)
}

View File

@@ -38,7 +38,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
useErrorHandling: vi.fn(() => ({
wrapWithErrorHandlingAsync: vi.fn(
(fn, errorHandler) =>
async (...args: any[]) => {
async (...args: Parameters<typeof fn>) => {
try {
return await fn(...args)
} catch (error) {

View File

@@ -22,7 +22,8 @@ function mockSettingStore(enabled: boolean) {
return enabled
}
return false
})
}),
load: vi.fn().mockResolvedValue(undefined)
})
}
@@ -227,6 +228,16 @@ describe('useNodeReplacementStore', () => {
consoleErrorSpy.mockRestore()
})
it('should not fetch when feature is disabled', async () => {
vi.mocked(fetchNodeReplacements).mockResolvedValue({})
store = createStore(false)
await store.load()
expect(fetchNodeReplacements).not.toHaveBeenCalled()
expect(store.isLoaded).toBe(false)
})
it('should not re-fetch when called twice', async () => {
vi.mocked(fetchNodeReplacements).mockResolvedValue(mockReplacements)
store = createStore()

View File

@@ -15,7 +15,7 @@ export const useNodeReplacementStore = defineStore('nodeReplacement', () => {
)
async function load() {
if (isLoaded.value) return
if (isLoaded.value || !isEnabled.value) return
try {
replacements.value = await fetchNodeReplacements()

View File

@@ -1198,7 +1198,7 @@ export const CORE_SETTINGS: SettingParams[] = [
tooltip:
'When enabled, missing nodes can be automatically replaced with their newer equivalents if a replacement mapping exists.',
type: 'boolean',
defaultValue: true,
defaultValue: false,
experimental: true,
versionAdded: '1.40.0'
}

View File

@@ -46,7 +46,7 @@ function onChange(
}
export const useSettingStore = defineStore('setting', () => {
const settingValues = ref<Record<string, any>>({})
const settingValues = ref<Partial<Settings>>({})
const settingsById = ref<Record<string, SettingParams>>({})
const {
@@ -87,7 +87,7 @@ export const useSettingStore = defineStore('setting', () => {
* @param key - The key of the setting to check.
* @returns Whether the setting exists.
*/
function exists(key: string) {
function exists<K extends keyof Settings>(key: K) {
return settingValues.value[key] !== undefined
}
@@ -118,7 +118,7 @@ export const useSettingStore = defineStore('setting', () => {
*/
function get<K extends keyof Settings>(key: K): Settings[K] {
// Clone the value when returning to prevent external mutations
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key))
return _.cloneDeep(settingValues.value[key] ?? getDefaultValue(key)!)
}
/**

View File

@@ -222,7 +222,7 @@ interface WorkflowStore {
activeSubgraph: Subgraph | undefined
/** Updates the {@link subgraphNamePath} and {@link isSubgraphActive} values. */
updateActiveGraph: () => void
executionIdToCurrentId: (id: string) => any
executionIdToCurrentId: (id: string) => string | undefined
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
nodeToNodeLocatorId: (node: LGraphNode) => NodeLocatorId
nodeExecutionIdToNodeLocatorId: (
@@ -718,7 +718,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
//FIXME: use existing util function
const executionIdToCurrentId = (id: string) => {
const executionIdToCurrentId = (id: string): string | undefined => {
const subgraph = activeSubgraph.value
// Short-circuit: ID belongs to the parent workflow / no active subgraph

View File

@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as I18n from 'vue-i18n'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import type { WorkflowDraftSnapshot } from '@/platform/workflow/persistence/base/draftCache'
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useWorkflowDraftStore } from '@/platform/workflow/persistence/stores/workflowDraftStore'
import { defaultGraphJSON } from '@/scripts/defaultGraph'
@@ -191,7 +192,7 @@ describe('useWorkflowPersistence', () => {
const drafts = JSON.parse(
localStorage.getItem('Comfy.Workflow.Drafts') ?? '{}'
) as Record<string, any>
) as Record<string, WorkflowDraftSnapshot>
expect(Object.keys(drafts).length).toBe(32)
expect(drafts['workflows/Draft0.json']).toBeUndefined()

View File

@@ -184,7 +184,7 @@ describe('LGraphNode', () => {
const wrapper = mountLGraphNode({ nodeData: mockNodeData })
expect(wrapper.classes()).toContain('outline-2')
expect(wrapper.classes()).toContain('outline-3')
expect(wrapper.classes()).toContain('outline-node-component-outline')
})

View File

@@ -17,7 +17,7 @@
// hover (only when node should handle events)
shouldHandleNodePointerEvents &&
'hover:ring-7 ring-node-component-ring',
'outline-transparent outline-2 focus-visible:outline-node-component-outline',
'outline-transparent outline-3 focus-visible:outline-node-component-outline',
borderClass,
outlineClass,
cursorClass,

View File

@@ -36,6 +36,7 @@ import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/interfaces'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import { RenderShape } from '@/lib/litegraph/src/litegraph'
import NodeHeader from '@/renderer/extensions/vueNodes/components/NodeHeader.vue'
import NodeSlots from '@/renderer/extensions/vueNodes/components/NodeSlots.vue'
@@ -69,8 +70,11 @@ const nodeData = computed<VueNodeData>(() => {
options: {
hidden: input.hidden,
advanced: input.advanced,
values: input.type === 'COMBO' ? input.options : undefined // For combo widgets
}
values:
input.type === 'COMBO' && Array.isArray(input.options)
? input.options
: undefined
} satisfies IWidgetOptions
}))
const inputs: INodeInputSlot[] = Object.entries(nodeDef.inputs || {})

View File

@@ -16,10 +16,12 @@ import type { ChartData } from 'chart.js'
import Chart from 'primevue/chart'
import { computed } from 'vue'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import type { ChartInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']>
type ChartWidgetOptions = NonNullable<ChartInputSpec['options']> &
IWidgetOptions
const value = defineModel<ChartData>({ required: true })

View File

@@ -38,10 +38,12 @@ import {
filterWidgetProps
} from '@/utils/widgetPropFilter'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import { WidgetInputBaseClass } from './layout'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
type WidgetOptions = { format?: ColorFormat } & Record<string, unknown>
type WidgetOptions = IWidgetOptions & { format?: ColorFormat }
const props = defineProps<{
widget: SimplifiedWidget<string, WidgetOptions>

View File

@@ -67,30 +67,40 @@ function updateValue(e: UIEvent) {
const { target } = e
if (!(target instanceof HTMLInputElement)) return
const parsed = evaluateInput(unformatValue(target.value))
if (parsed !== undefined)
modelValue.value = Math.min(
filteredProps.value.max,
Math.max(filteredProps.value.min, parsed)
)
else target.value = formattedValue.value
if (parsed !== undefined) {
const max = filteredProps.value.max ?? Number.MAX_VALUE
const min = filteredProps.value.min ?? -Number.MAX_VALUE
modelValue.value = Math.min(max, Math.max(min, parsed))
} else target.value = formattedValue.value
textEdit.value = false
}
const canDecrement = computed(
() =>
modelValue.value > filteredProps.value.min &&
!props.widget.options?.disabled
)
const canIncrement = computed(
() =>
modelValue.value < filteredProps.value.max &&
!props.widget.options?.disabled
)
interface NumericWidgetOptions {
min: number
max: number
step?: number
step2?: number
precision?: number
disabled?: boolean
useGrouping?: boolean
}
const filteredProps = computed(() =>
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
)
const filteredProps = computed(() => {
const filtered = filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
return filtered as Partial<NumericWidgetOptions>
})
const isDisabled = computed(() => props.widget.options?.disabled ?? false)
const canDecrement = computed(() => {
const min = filteredProps.value.min ?? -Number.MAX_VALUE
return modelValue.value > min && !isDisabled.value
})
const canIncrement = computed(() => {
const max = filteredProps.value.max ?? Number.MAX_VALUE
return modelValue.value < max && !isDisabled.value
})
// Get the precision value for proper number formatting
const precision = computed(() => {
@@ -108,7 +118,7 @@ const stepValue = computed(() => {
// Use step / 10 for custom large step values (> 10) to match litegraph behavior
// This is important for extensions like Impact Pack that use custom step values (e.g., 640)
// We skip default step values (1, 10) to avoid affecting normal widgets
const step = props.widget.options?.step
const step = props.widget.options?.step as number | undefined
if (step !== undefined && step > 10) {
return Number(step) / 10
}
@@ -140,17 +150,16 @@ const buttonsDisabled = computed(() => {
})
function updateValueBy(delta: number) {
modelValue.value = Math.min(
filteredProps.value.max,
Math.max(filteredProps.value.min, modelValue.value + delta)
)
const max = filteredProps.value.max ?? Number.MAX_VALUE
const min = filteredProps.value.min ?? -Number.MAX_VALUE
modelValue.value = Math.min(max, Math.max(min, modelValue.value + delta))
}
const dragValue = ref<number>()
const dragDelta = ref(0)
function handleMouseDown(e: PointerEvent) {
if (e.button > 0) return
if (props.widget.options?.disabled) return
if (isDisabled.value) return
const { target } = e
if (!(target instanceof HTMLElement)) return
target.setPointerCapture(e.pointerId)
@@ -163,10 +172,9 @@ function handleMouseMove(e: PointerEvent) {
const unclippedValue =
dragValue.value + ((dragDelta.value / 10) | 0) * stepValue.value
dragDelta.value %= 10
dragValue.value = Math.min(
filteredProps.value.max,
Math.max(filteredProps.value.min, unclippedValue)
)
const max = filteredProps.value.max ?? Number.MAX_VALUE
const min = filteredProps.value.min ?? -Number.MAX_VALUE
dragValue.value = Math.min(max, Math.max(min, unclippedValue))
}
function handleMouseUp() {
const newValue = dragValue.value
@@ -248,7 +256,7 @@ const sliderWidth = computed(() => {
:value="formattedValue"
role="spinbutton"
tabindex="0"
:disabled="widget.options?.disabled"
:disabled="isDisabled"
autocomplete="off"
autocorrect="off"
spellcheck="false"

View File

@@ -5,6 +5,7 @@ import type { InputTextProps } from 'primevue/inputtext'
import Textarea from 'primevue/textarea'
import { describe, expect, it } from 'vitest'
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetInputText from './WidgetInputText.vue'
@@ -18,7 +19,7 @@ describe('WidgetInputText Value Binding', () => {
name: 'test_input',
type: 'string',
value,
options,
options: options as IWidgetOptions,
callback
})

View File

@@ -53,7 +53,7 @@ const props = defineProps<Props>()
const modelValue = defineModel<string | undefined>({
default(props: Props) {
return props.widget.options?.values?.[0] || ''
return props.widget.options?.values?.[0] ?? ''
}
})

View File

@@ -31,7 +31,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
value: string = 'img_001.png',
options: {
values?: string[]
getOptionLabel?: (value: string | null) => string
getOptionLabel?: (value?: string | null) => string
} = {},
spec?: ComboInputSpec
): SimplifiedWidget<string | undefined> => ({
@@ -82,7 +82,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
describe('when custom labels are provided via getOptionLabel', () => {
it('displays custom labels while preserving original values', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (!value) return 'No file'
const mapping: Record<string, string> = {
'img_001.png': 'Vacation Photo',
@@ -112,7 +112,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('emits original values when items with custom labels are selected', async () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (!value) return 'No file'
return `Custom: ${value}`
})
@@ -134,7 +134,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('falls back to original value when label mapping fails', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (value === 'photo_abc.jpg') {
throw new Error('Mapping failed')
}
@@ -163,7 +163,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('falls back to original value when label mapping returns empty string', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (value === 'photo_abc.jpg') {
return ''
}
@@ -185,7 +185,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
})
it('falls back to original value when label mapping returns undefined', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (value === 'hash789.png') {
return undefined as unknown as string
}
@@ -209,7 +209,7 @@ describe('WidgetSelectDropdown custom label mapping', () => {
describe('output items with custom label mapping', () => {
it('applies custom label mapping to output items from queue history', () => {
const getOptionLabel = vi.fn((value: string | null) => {
const getOptionLabel = vi.fn((value?: string | null) => {
if (!value) return 'No file'
return `Output: ${value}`
})

View File

@@ -57,7 +57,7 @@ provide(
const modelValue = defineModel<string | undefined>({
default(props: Props) {
return props.widget.options?.values?.[0] || ''
return props.widget.options?.values?.[0] ?? ''
}
})
@@ -73,7 +73,8 @@ const combinedProps = computed(() => ({
}))
const getAssetData = () => {
const nodeType = props.widget.options?.nodeType ?? props.nodeType
const nodeType: string | undefined =
props.widget.options?.nodeType ?? props.nodeType
if (props.isAssetMode && nodeType) {
return useAssetWidgetData(toRef(nodeType))
}
@@ -134,11 +135,11 @@ const inputItems = computed<FormDropdownItem[]>(() => {
return []
}
return values.map((value: string, index: number) => ({
return values.map((value, index) => ({
id: `input-${index}`,
preview_url: getMediaUrl(value, 'input'),
name: value,
label: getDisplayLabel(value)
preview_url: getMediaUrl(String(value), 'input'),
name: String(value),
label: getDisplayLabel(String(value))
}))
})
const outputItems = computed<FormDropdownItem[]>(() => {

View File

@@ -15,8 +15,8 @@
v-model="modelValue"
:class="cn(WidgetInputBaseClass, 'size-full text-xs resize-none')"
:placeholder
:readonly="widget.options?.read_only"
:disabled="widget.options?.read_only"
:readonly="isReadOnly"
:disabled="isReadOnly"
fluid
data-capture-wheel="true"
@pointerdown.capture.stop
@@ -58,4 +58,6 @@ const filteredProps = computed(() =>
const displayName = computed(() => widget.label || widget.name)
const id = useId()
const isReadOnly = computed(() => widget.options?.read_only ?? false)
</script>

Some files were not shown because too many files have changed in this diff Show More