Compare commits

...

13 Commits

Author SHA1 Message Date
bymyself
6fcf5c8348 feat(selection): Conditionally hide "Convert to Subgraph" button
Implements the feature from issue #4866.

The "Convert to Subgraph" button in the selection toolbox is now hidden under the following conditions:
- The selection consists only of group nodes.
- A single SubgraphNode is selected.

This prevents users from attempting to convert invalid selections into subgraphs.
2025-08-12 20:30:46 -07:00
Alexander Piskun
ef1852d551 gemini-2.5-pro and flash models; corrected prices (#4945) 2025-08-12 14:52:44 -07:00
Alexander Piskun
983ebb2ba7 pricing update for MinimaxHailuoVideo node and Kling "kling-v2-1" model (#4938) 2025-08-12 14:15:06 -07:00
AustinMroz
db71365768 Implement subgraph unpacking (#4840) 2025-08-12 13:45:29 -07:00
Christian Byrne
17d7ba8bcb [ci] Add lint:fix:no-cache script for consistency (#4948)
Co-authored-by: github-actions <github-actions@github.com>
2025-08-12 13:25:22 -07:00
Christian Byrne
24a386c766 fix: add cleanup for graphChanged listener in useWorkflowPersistence (#4947) 2025-08-12 12:27:04 -07:00
Christian Byrne
c42c9315f4 [refactor] Replace lodash with es-toolkit (#4935) 2025-08-12 12:22:09 -07:00
Sidharth
d068b8351e Feat: add duplicate workflow on sidebar (#4895) 2025-08-12 12:04:29 -07:00
Christian Byrne
1cf8087be0 [ci] Optimize ESLint performance with caching and generated file excl… (#4926)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2025-08-12 13:31:54 -04:00
filtered
9c31d708a2 Add automatic trackpad / mouse sensing (#4913) 2025-08-12 10:15:32 -07:00
Comfy Org PR Bot
9a70e927aa 1.26.2 (#4939)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-08-12 00:52:23 -07:00
arab-future-academy
dc444faa75 Feature/arabic translation (#4916) 2025-08-11 23:29:01 -07:00
Jennifer Weber
a055ec2dff [bugfix] Fix queue not updating with completed task images (#4936)
Co-authored-by: Jennifer Weber <weberjc@MacBookPro.lan>
2025-08-11 22:36:43 -07:00
85 changed files with 11904 additions and 124 deletions

View File

@@ -49,7 +49,7 @@ DO NOT use deprecated PrimeVue components. Use these replacements instead:
## Development Guidelines
1. Leverage VueUse functions for performance-enhancing styles
2. Use lodash for utility functions
2. Use es-toolkit for utility functions
3. Use TypeScript for type safety
4. Implement proper props and emits definitions
5. Utilize Vue 3's Teleport component when needed

View File

@@ -18,7 +18,7 @@ Use Tailwind CSS for styling
Leverage VueUse functions for performance-enhancing styles
Use lodash for utility functions
Use es-toolkit for utility functions
Use TypeScript for type safety

View File

@@ -61,6 +61,11 @@ jobs:
exit 1
fi
- name: Lint generated types
run: |
echo "Linting generated ComfyUI-Manager API types..."
npm run lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
- name: Check for changes
id: check-changes
run: |

View File

@@ -61,6 +61,11 @@ jobs:
exit 1
fi
- name: Lint generated types
run: |
echo "Linting generated Comfy Registry API types..."
npm run lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
- name: Check for changes
id: check-changes
run: |

3
.gitignore vendored
View File

@@ -7,6 +7,9 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# ESLint cache
.eslintcache
node_modules
dist
dist-ssr

View File

@@ -9,7 +9,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es'],
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

View File

@@ -75,7 +75,7 @@ The development of successive minor versions overlaps. For example, while versio
<summary>v1.5: Native translation (i18n)</summary>
ComfyUI now includes built-in translation support, replacing the need for third-party translation extensions. Select your language
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, or Korean. This native
in `Comfy > Locale > Language` to translate the interface into English, Chinese (Simplified), Russian, Japanese, Korean, or Arabic. This native
implementation offers better performance, reliability, and maintainability compared to previous solutions.<br>
More details available here: https://blog.comfy.org/p/native-localization-support-i18n

View File

@@ -1,5 +1,5 @@
import _ from 'es-toolkit/compat'
import fs from 'fs'
import _ from 'lodash'
import path from 'path'
import type { Request, Route } from 'playwright'
import { v4 as uuidv4 } from 'uuid'
@@ -75,7 +75,9 @@ export default class TaskHistory {
private async handleGetView(route: Route) {
const fileName = getFilenameParam(route.request())
if (!this.outputContentTypes.has(fileName)) route.continue()
if (!this.outputContentTypes.has(fileName)) {
return route.continue()
}
const asset = this.loadAsset(fileName)
return route.fulfill({

View File

@@ -317,6 +317,25 @@ test.describe('Workflows sidebar', () => {
])
})
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
const { workflowsTab } = comfyPage.menu
await workflowsTab.open()
await workflowsTab
.getPersistedItem('workflow1.json')
.click({ button: 'right' })
await comfyPage.clickContextMenuItem('Duplicate')
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'*workflow1 (Copy).json'
])
})
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'default.json'

View File

@@ -14,7 +14,10 @@ export default [
ignores: [
'src/scripts/*',
'src/extensions/core/*',
'src/types/vue-shim.d.ts'
'src/types/vue-shim.d.ts',
// Generated files that don't need linting
'src/types/comfyRegistryTypes.ts',
'src/types/generatedManagerTypes.ts'
]
},
{

25
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.26.1",
"version": "1.26.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.26.1",
"version": "1.26.2",
"license": "GPL-3.0-only",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -30,12 +30,12 @@
"axios": "^1.8.2",
"dompurify": "^3.2.5",
"dotenv": "^16.4.5",
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"jsondiffpatch": "^0.6.0",
"lodash": "^4.17.21",
"loglevel": "^1.9.2",
"marked": "^15.0.11",
"pinia": "^2.1.7",
@@ -62,7 +62,6 @@
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
@@ -4887,12 +4886,6 @@
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
"integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==",
"dev": true
},
"node_modules/@types/markdown-it": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
@@ -7998,6 +7991,15 @@
"node": ">= 0.4"
}
},
"node_modules/es-toolkit": {
"version": "1.39.9",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz",
"integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -11450,7 +11452,8 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
"node_modules/lodash-es": {
"version": "4.17.21",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.26.1",
"version": "1.26.2",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -21,8 +21,10 @@
"test:component": "vitest run src/components/",
"prepare": "husky || true",
"preview": "vite preview",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix",
"lint:no-cache": "eslint src",
"lint:fix:no-cache": "eslint src --fix",
"knip": "knip",
"locale": "lobe-i18n locale",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
@@ -39,7 +41,6 @@
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.6",
"@types/node": "^20.14.8",
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
@@ -98,12 +99,12 @@
"axios": "^1.8.2",
"dompurify": "^3.2.5",
"dotenv": "^16.4.5",
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"jsondiffpatch": "^0.6.0",
"lodash": "^4.17.21",
"loglevel": "^1.9.2",
"marked": "^15.0.11",
"pinia": "^2.1.7",

View File

@@ -51,7 +51,7 @@ const template = await fetch('/templates/default.json')
## General Guidelines
- Use lodash for utility functions
- Use es-toolkit for utility functions
- Implement proper TypeScript types
- Follow Vue 3 composition API style guide
- Use vue-i18n for ALL user-facing strings in `src/locales/en/main.json`

View File

@@ -20,7 +20,7 @@ import {
useLocalStorage,
watchDebounced
} from '@vueuse/core'
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import Panel from 'primevue/panel'
import { Ref, computed, inject, nextTick, onMounted, ref, watch } from 'vue'

View File

@@ -42,7 +42,7 @@
</template>
<script setup lang="ts" generic="TFilter extends SearchFilter">
import { debounce } from 'lodash'
import { debounce } from 'es-toolkit/compat'
import Button from 'primevue/button'
import IconField from 'primevue/iconfield'
import InputIcon from 'primevue/inputicon'

View File

@@ -16,7 +16,7 @@
<script setup lang="ts" generic="T">
import { useElementSize, useScroll, whenever } from '@vueuse/core'
import { clamp, debounce } from 'lodash'
import { clamp, debounce } from 'es-toolkit/compat'
import { type CSSProperties, computed, onBeforeUnmount, ref, watch } from 'vue'
type GridState = {

View File

@@ -169,8 +169,8 @@ import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import type { CaptureContext, User } from '@sentry/core'
import { captureMessage } from '@sentry/core'
import _ from 'lodash'
import cloneDeep from 'lodash/cloneDeep'
import _ from 'es-toolkit/compat'
import { cloneDeep } from 'es-toolkit/compat'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Dropdown from 'primevue/dropdown'

View File

@@ -93,7 +93,7 @@
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { merge } from 'lodash'
import { merge } from 'es-toolkit/compat'
import Button from 'primevue/button'
import {
computed,

View File

@@ -12,7 +12,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import PackEnableToggle from './PackEnableToggle.vue'
// Mock debounce to execute immediately
vi.mock('lodash', () => ({
vi.mock('es-toolkit/compat', () => ({
debounce: <T extends (...args: any[]) => any>(fn: T) => fn
}))

View File

@@ -10,7 +10,7 @@
</template>
<script setup lang="ts">
import { debounce } from 'lodash'
import { debounce } from 'es-toolkit/compat'
import ToggleSwitch from 'primevue/toggleswitch'
import { computed, ref } from 'vue'

View File

@@ -57,7 +57,7 @@
</template>
<script setup lang="ts">
import { stubTrue } from 'lodash'
import { stubTrue } from 'es-toolkit/compat'
import AutoComplete, {
AutoCompleteOptionSelectEvent
} from 'primevue/autocomplete'

View File

@@ -1,6 +1,20 @@
<template>
<Button
v-show="isVisible"
v-if="isUnpackVisible"
v-tooltip.top="{
value: t('commands.Comfy_Graph_UnpackSubgraph.label'),
showDelay: 1000
}"
severity="secondary"
text
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
>
<template #icon>
<i-lucide:expand />
</template>
</Button>
<Button
v-else-if="isConvertVisible"
v-tooltip.top="{
value: t('commands.Comfy_Graph_ConvertToSubgraph.label'),
showDelay: 1000
@@ -20,6 +34,7 @@ import Button from 'primevue/button'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { LGraphGroup, SubgraphNode } from '@/lib/litegraph/src/litegraph'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
@@ -27,7 +42,31 @@ const { t } = useI18n()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const isVisible = computed(() => {
const isUnpackVisible = computed(() => {
return (
canvasStore.selectedItems?.length === 1 &&
canvasStore.selectedItems[0] instanceof SubgraphNode
)
})
const isConvertVisible = computed(() => {
const items = canvasStore.selectedItems
if (!items || items.length === 0) {
return false
}
// Unpack button takes precedence for single subgraph node
if (items.length === 1 && items[0] instanceof SubgraphNode) {
return false
}
// Hide if ALL selected items are groups
const allAreGroups = items.every((item) => item instanceof LGraphGroup)
if (allAreGroups) {
return false
}
// Otherwise, show it, assuming there's some selection
return (
canvasStore.groupSelected ||
canvasStore.rerouteSelected ||

View File

@@ -82,7 +82,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
</template>
<script setup lang="ts">
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { computed } from 'vue'
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'

View File

@@ -265,6 +265,14 @@ const renderTreeNode = (
const workflow = node.data
await workflowService.insertWorkflow(workflow)
}
},
{
label: t('g.duplicate'),
icon: 'pi pi-file-export',
command: async () => {
const workflow = node.data
await workflowService.duplicateWorkflow(workflow)
}
}
]
},

View File

@@ -1,7 +1,7 @@
import { FitAddon } from '@xterm/addon-fit'
import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css'
import { debounce } from 'lodash'
import { debounce } from 'es-toolkit/compat'
import { Ref, markRaw, onMounted, onUnmounted } from 'vue'
export function useTerminal(element: Ref<HTMLElement | undefined>) {

View File

@@ -1,5 +1,5 @@
import { useMutationObserver, useResizeObserver } from '@vueuse/core'
import { debounce } from 'lodash'
import { debounce } from 'es-toolkit/compat'
import { readonly, ref } from 'vue'
/**

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { computed, onMounted, watch } from 'vue'
import { useNodePricing } from '@/composables/node/useNodePricing'

View File

@@ -418,7 +418,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const modeValue = String(modeWidget.value)
// Pricing matrix from CSV data based on mode string content
if (modeValue.includes('v2-master')) {
if (modeValue.includes('v2-1-master')) {
if (modeValue.includes('10s')) {
return '$2.80/Run' // price is the same as for v2-master model
}
return '$1.40/Run' // price is the same as for v2-master model
} else if (modeValue.includes('v2-master')) {
if (modeValue.includes('10s')) {
return '$2.80/Run'
}
@@ -558,6 +563,32 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
MinimaxTextToVideoNode: {
displayPrice: '$0.43/Run'
},
MinimaxHailuoVideoNode: {
displayPrice: (node: LGraphNode): string => {
const resolutionWidget = node.widgets?.find(
(w) => w.name === 'resolution'
) as IComboWidget
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!resolutionWidget || !durationWidget) {
return '$0.28-0.56/Run (varies with resolution & duration)'
}
const resolution = String(resolutionWidget.value)
const duration = String(durationWidget.value)
if (resolution.includes('768P')) {
if (duration.includes('6')) return '$0.28/Run'
if (duration.includes('10')) return '$0.56/Run'
} else if (resolution.includes('1080P')) {
if (duration.includes('6')) return '$0.49/Run'
}
return '$0.43/Run' // default median
}
},
OpenAIDalle2: {
displayPrice: (node: LGraphNode): string => {
const sizeWidget = node.widgets?.find(
@@ -1278,9 +1309,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
// Google Veo video generation
if (model.includes('veo-2.0')) {
return '$0.5/second'
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
return '$0.00016/$0.0006 per 1K tokens'
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
return '$0.0003/$0.0025 per 1K tokens'
} else if (model.includes('gemini-2.5-flash')) {
return '$0.0003/$0.0025 per 1K tokens'
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
return '$0.00125/$0.01 per 1K tokens'
} else if (model.includes('gemini-2.5-pro')) {
return '$0.00125/$0.01 per 1K tokens'
}
// For other Gemini models, show token-based pricing info
@@ -1358,6 +1393,7 @@ export const useNodePricing = () => {
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
KlingSingleImageVideoEffectNode: ['effect_scene'],
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIGPTImage1: ['quality', 'n'],

View File

@@ -1,4 +1,4 @@
import { groupBy } from 'lodash'
import { groupBy } from 'es-toolkit/compat'
import { computed, onMounted } from 'vue'
import { useWorkflowPacks } from '@/composables/nodePack/useWorkflowPacks'

View File

@@ -21,6 +21,7 @@ import { useWorkflowService } from '@/services/workflowService'
import type { ComfyCommand } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useCanvasStore, useTitleEditorStore } from '@/stores/graphStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { useQueueSettingsStore, useQueueStore } from '@/stores/queueStore'
import { useSettingStore } from '@/stores/settingStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
@@ -797,6 +798,22 @@ export function useCoreCommands(): ComfyCommand[] {
canvas.select(node)
}
},
{
id: 'Comfy.Graph.UnpackSubgraph',
icon: 'pi pi-sitemap',
label: 'Unpack the selected Subgraph',
versionAdded: '1.20.1',
category: 'essentials' as const,
function: () => {
const canvas = canvasStore.getCanvas()
const graph = canvas.subgraph ?? canvas.graph
if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')
const subgraphNode = app.canvas.selectedItems.values().next().value
useNodeOutputStore().revokeSubgraphPreviews(subgraphNode)
graph.unpackSubgraph(subgraphNode)
}
},
{
id: 'Workspace.ToggleBottomPanel.Shortcuts',
icon: 'pi pi-key',

View File

@@ -1,5 +1,5 @@
import { watchDebounced } from '@vueuse/core'
import { orderBy } from 'lodash'
import { orderBy } from 'es-toolkit/compat'
import { computed, ref, watch } from 'vue'
import { DEFAULT_PAGE_SIZE } from '@/constants/searchConstants'

View File

@@ -1,3 +1,4 @@
import { tryOnScopeDispose } from '@vueuse/core'
import { computed, watch } from 'vue'
import { api } from '@/scripts/api'
@@ -88,6 +89,11 @@ export function useWorkflowPersistence() {
)
api.addEventListener('graphChanged', persistCurrentWorkflow)
// Clean up event listener when component unmounts
tryOnScopeDispose(() => {
api.removeEventListener('graphChanged', persistCurrentWorkflow)
})
// Restore workflow tabs states
const openWorkflows = computed(() => workflowStore.openWorkflows)
const activeWorkflow = computed(() => workflowStore.activeWorkflow)

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'

View File

@@ -300,7 +300,8 @@ export const CORE_SETTINGS: SettingParams[] = [
{ value: 'ja', text: '日本語' },
{ value: 'ko', text: '한국어' },
{ value: 'fr', text: 'Français' },
{ value: 'es', text: 'Español' }
{ value: 'es', text: 'Español' },
{ value: 'ar', text: 'عربي' }
],
defaultValue: () => navigator.language.split('-')[0] || 'en'
},

View File

@@ -1,5 +1,5 @@
import { debounce } from 'lodash'
import _ from 'lodash'
import { debounce } from 'es-toolkit/compat'
import _ from 'es-toolkit/compat'
import { t } from '@/i18n'

View File

@@ -1,5 +1,9 @@
import { createI18n } from 'vue-i18n'
import arCommands from './locales/ar/commands.json'
import ar from './locales/ar/main.json'
import arNodes from './locales/ar/nodeDefs.json'
import arSettings from './locales/ar/settings.json'
import enCommands from './locales/en/commands.json'
import en from './locales/en/main.json'
import enNodes from './locales/en/nodeDefs.json'
@@ -50,7 +54,8 @@ const messages = {
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings)
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings)
}
export const i18n = createI18n({

View File

@@ -43,6 +43,19 @@ export class CanvasPointer {
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
static #maxClickDrift2 = this.#maxClickDrift ** 2
/** Assume that "wheel" events with both deltaX and deltaY less than this value are trackpad gestures. */
static trackpadThreshold = 60
/**
* The minimum time between "wheel" events to allow switching between trackpad
* and mouse modes.
*
* This prevents trackpad "flick" panning from registering as regular mouse wheel.
* After a flick gesture is complete, the automatic wheel events are sent with
* reduced frequency, but much higher deltaX and deltaY values.
*/
static trackpadMaxGap = 200
/** The element this PointerState should capture input against when dragging. */
element: Element
/** Pointer ID used by drag capture. */
@@ -77,6 +90,9 @@ export class CanvasPointer {
/** The last pointerup event for the primary button */
eUp?: CanvasPointerEvent
/** The last pointermove event that was treated as a trackpad gesture. */
lastTrackpadEvent?: WheelEvent
/**
* If set, as soon as the mouse moves outside the click drift threshold, this action is run once.
* @param pointer [DEPRECATED] This parameter will be removed in a future release.
@@ -257,6 +273,35 @@ export class CanvasPointer {
delete this.onDragStart
}
/**
* Checks if the given wheel event is part of a continued trackpad gesture.
* @param e The wheel event to check
* @returns `true` if the event is part of a continued trackpad gesture, otherwise `false`
*/
#isContinuationOfGesture(e: WheelEvent): boolean {
const { lastTrackpadEvent } = this
if (!lastTrackpadEvent) return false
return (
e.timeStamp - lastTrackpadEvent.timeStamp < CanvasPointer.trackpadMaxGap
)
}
/**
* Checks if the given wheel event is part of a trackpad gesture.
* @param e The wheel event to check
* @returns `true` if the event is part of a trackpad gesture, otherwise `false`
*/
isTrackpadGesture(e: WheelEvent): boolean {
if (this.#isContinuationOfGesture(e)) {
this.lastTrackpadEvent = e
return true
}
const threshold = CanvasPointer.trackpadThreshold
return Math.abs(e.deltaX) < threshold && Math.abs(e.deltaY) < threshold
}
/**
* Resets the state of this {@link CanvasPointer} instance.
*

View File

@@ -1,4 +1,4 @@
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import type { Point, Rect } from './interfaces'
import { LGraphCanvas } from './litegraph'

View File

@@ -1,4 +1,4 @@
import { toString } from 'lodash'
import { toString } from 'es-toolkit/compat'
import {
SUBGRAPH_INPUT_ID,
@@ -1664,6 +1664,254 @@ export class LGraph
return { subgraph, node: subgraphNode as SubgraphNode }
}
unpackSubgraph(subgraphNode: SubgraphNode) {
if (!(subgraphNode instanceof SubgraphNode))
throw new Error('Can only unpack Subgraph Nodes')
this.beforeChange()
const center = [0, 0]
for (const node of subgraphNode.subgraph.nodes) {
center[0] += node.pos[0] + node.size[0] / 2
center[1] += node.pos[1] + node.size[1] / 2
}
center[0] /= subgraphNode.subgraph.nodes.length
center[1] /= subgraphNode.subgraph.nodes.length
const offsetX = subgraphNode.pos[0] - center[0] + subgraphNode.size[0] / 2
const offsetY = subgraphNode.pos[1] - center[1] + subgraphNode.size[1] / 2
const movedNodes = multiClone(subgraphNode.subgraph.nodes)
const nodeIdMap = new Map<NodeId, NodeId>()
for (const n_info of movedNodes) {
const node = LiteGraph.createNode(String(n_info.type), n_info.title)
if (!node) {
throw new Error('Node not found')
}
nodeIdMap.set(n_info.id, ++this.last_node_id)
node.id = this.last_node_id
n_info.id = this.last_node_id
this.add(node, true)
node.configure(n_info)
node.pos[0] += offsetX
node.pos[1] += offsetY
for (const input of node.inputs) {
input.link = null
}
}
//cleanup reoute.linkIds now, but leave link.parentIds dangling
for (const islot of subgraphNode.inputs) {
if (!islot.link) continue
const link = this.links.get(islot.link)
if (!link) {
console.warn('Broken link', islot, islot.link)
continue
}
for (const reroute of LLink.getReroutes(this, link)) {
reroute.linkIds.delete(link.id)
}
}
for (const oslot of subgraphNode.outputs) {
for (const linkId of oslot.links ?? []) {
const link = this.links.get(linkId)
if (!link) {
console.warn('Broken link', oslot, linkId)
continue
}
for (const reroute of LLink.getReroutes(this, link)) {
reroute.linkIds.delete(link.id)
}
}
}
const newLinks: [
NodeId,
number,
NodeId,
number,
LinkId,
RerouteId | undefined,
RerouteId | undefined,
boolean
][] = []
for (const [, link] of subgraphNode.subgraph._links) {
let externalParentId: RerouteId | undefined
if (link.origin_id === SUBGRAPH_INPUT_ID) {
const outerLinkId = subgraphNode.inputs[link.origin_slot].link
if (!outerLinkId) {
console.error('Missing Link ID when unpacking')
continue
}
const outerLink = this.links[outerLinkId]
link.origin_id = outerLink.origin_id
link.origin_slot = outerLink.origin_slot
externalParentId = outerLink.parentId
} else {
const origin_id = nodeIdMap.get(link.origin_id)
if (!origin_id) {
console.error('Missing Link ID when unpacking')
continue
}
link.origin_id = origin_id
}
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
[]) {
const sublink = this.links[linkId]
newLinks.push([
link.origin_id,
link.origin_slot,
sublink.target_id,
sublink.target_slot,
link.id,
link.parentId,
sublink.parentId,
true
])
sublink.parentId = undefined
}
continue
} else {
const target_id = nodeIdMap.get(link.target_id)
if (!target_id) {
console.error('Missing Link ID when unpacking')
continue
}
link.target_id = target_id
}
newLinks.push([
link.origin_id,
link.origin_slot,
link.target_id,
link.target_slot,
link.id,
link.parentId,
externalParentId,
false
])
}
this.remove(subgraphNode)
this.subgraphs.delete(subgraphNode.subgraph.id)
const linkIdMap = new Map<LinkId, LinkId[]>()
for (const newLink of newLinks) {
let created: LLink | null | undefined
if (newLink[0] == SUBGRAPH_INPUT_ID) {
if (!(this instanceof Subgraph)) {
console.error('Ignoring link to subgraph outside subgraph')
continue
}
const tnode = this._nodes_by_id[newLink[2]]
created = this.inputNode.slots[newLink[1]].connect(
tnode.inputs[newLink[3]],
tnode
)
} else if (newLink[2] == SUBGRAPH_OUTPUT_ID) {
if (!(this instanceof Subgraph)) {
console.error('Ignoring link to subgraph outside subgraph')
continue
}
const tnode = this._nodes_by_id[newLink[0]]
created = this.outputNode.slots[newLink[3]].connect(
tnode.outputs[newLink[1]],
tnode
)
} else {
created = this._nodes_by_id[newLink[0]].connect(
newLink[1],
this._nodes_by_id[newLink[2]],
newLink[3]
)
}
if (!created) {
console.error('Failed to create link')
continue
}
//This is a little unwieldy since Map.has isn't a type guard
const linkIds = linkIdMap.get(newLink[4]) ?? []
linkIds.push(created.id)
if (!linkIdMap.has(newLink[4])) {
linkIdMap.set(newLink[4], linkIds)
}
newLink[4] = created.id
}
const rerouteIdMap = new Map<RerouteId, RerouteId>()
for (const reroute of subgraphNode.subgraph.reroutes.values()) {
if (
reroute.parentId !== undefined &&
rerouteIdMap.get(reroute.parentId) === undefined
) {
console.error('Missing Parent ID')
}
const migratedReroute = new Reroute(++this.state.lastRerouteId, this, [
reroute.pos[0] + offsetX,
reroute.pos[1] + offsetY
])
rerouteIdMap.set(reroute.id, migratedReroute.id)
this.reroutes.set(migratedReroute.id, migratedReroute)
}
//iterate over newly created links to update reroute parentIds
for (const newLink of newLinks) {
const linkInstance = this.links.get(newLink[4])
if (!linkInstance) {
continue
}
let instance: Reroute | LLink | undefined = linkInstance
let parentId: RerouteId | undefined = newLink[6]
if (newLink[7]) {
parentId = newLink[6]
//TODO: recursion check/helper method? Probably exists, but wouldn't mesh with the reference tracking used by this implementation
while (parentId) {
instance.parentId = parentId
instance = this.reroutes.get(parentId)
if (!instance) throw new Error('Broken Id link when unpacking')
if (instance.linkIds.has(linkInstance.id))
throw new Error('Infinite parentId loop')
instance.linkIds.add(linkInstance.id)
parentId = instance.parentId
}
}
parentId = newLink[5]
while (parentId) {
const migratedId = rerouteIdMap.get(parentId)
if (!migratedId) throw new Error('Broken Id link when unpacking')
instance.parentId = migratedId
instance = this.reroutes.get(migratedId)
if (!instance) throw new Error('Broken Id link when unpacking')
if (instance.linkIds.has(linkInstance.id))
throw new Error('Infinite parentId loop')
instance.linkIds.add(linkInstance.id)
const oldReroute = subgraphNode.subgraph.reroutes.get(parentId)
if (!oldReroute) throw new Error('Broken Id link when unpacking')
parentId = oldReroute.parentId
}
if (!newLink[7]) {
parentId = newLink[6]
while (parentId) {
instance.parentId = parentId
instance = this.reroutes.get(parentId)
if (!instance) throw new Error('Broken Id link when unpacking')
if (instance.linkIds.has(linkInstance.id))
throw new Error('Infinite parentId loop')
instance.linkIds.add(linkInstance.id)
parentId = instance.parentId
}
}
}
const nodes: LGraphNode[] = []
for (const nodeId of nodeIdMap.values()) {
const node = this._nodes_by_id[nodeId]
nodes.push(node)
node._setConcreteSlots()
node.arrange()
}
const reroutes = [...rerouteIdMap.values()]
.map((i) => this.reroutes.get(i))
.filter((x): x is Reroute => !!x)
this.canvasAction((c) => c.selectItems([...nodes, ...reroutes]))
this.afterChange()
}
/**
* Resolve a path of subgraph node IDs into a list of subgraph nodes.
* Not intended to be run from subgraphs.

View File

@@ -1,4 +1,4 @@
import { toString } from 'lodash'
import { toString } from 'es-toolkit/compat'
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
@@ -3456,10 +3456,6 @@ export class LGraphCanvas
processMouseWheel(e: WheelEvent): void {
if (!this.graph || !this.allow_dragcanvas) return
// TODO: Mouse wheel zoom rewrite
// @ts-expect-error wheelDeltaY is non-standard property on WheelEvent
const delta = e.wheelDeltaY ?? e.detail * -60
this.adjustMouseEvent(e)
const pos: Point = [e.clientX, e.clientY]
@@ -3467,35 +3463,34 @@ export class LGraphCanvas
let { scale } = this.ds
if (
LiteGraph.canvasNavigationMode === 'legacy' ||
(LiteGraph.canvasNavigationMode === 'standard' && e.ctrlKey)
) {
if (delta > 0) {
scale *= this.zoom_speed
} else if (delta < 0) {
scale *= 1 / this.zoom_speed
}
this.ds.changeScale(scale, [e.clientX, e.clientY])
} else if (
LiteGraph.macTrackpadGestures &&
(!LiteGraph.macGesturesRequireMac || navigator.userAgent.includes('Mac'))
) {
if (e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey) {
if (e.deltaY > 0) {
scale *= 1 / this.zoom_speed
} else if (e.deltaY < 0) {
scale *= this.zoom_speed
}
this.ds.changeScale(scale, [e.clientX, e.clientY])
} else if (e.ctrlKey) {
// Detect if this is a trackpad gesture or mouse wheel
const isTrackpad = this.pointer.isTrackpadGesture(e)
if (e.ctrlKey || LiteGraph.canvasNavigationMode === 'legacy') {
// Legacy mode or standard mode with ctrl - use wheel for zoom
if (isTrackpad) {
// Trackpad gesture - use smooth scaling
scale *= 1 + e.deltaY * (1 - this.zoom_speed) * 0.18
this.ds.changeScale(scale, [e.clientX, e.clientY], false)
} else if (e.shiftKey) {
this.ds.offset[0] -= e.deltaY * 1.18 * (1 / scale)
} else {
this.ds.offset[0] -= e.deltaX * 1.18 * (1 / scale)
this.ds.offset[1] -= e.deltaY * 1.18 * (1 / scale)
// Mouse wheel - use stepped scaling
if (e.deltaY < 0) {
scale *= this.zoom_speed
} else if (e.deltaY > 0) {
scale *= 1 / this.zoom_speed
}
this.ds.changeScale(scale, [e.clientX, e.clientY])
}
} else {
// Standard mode without ctrl - use wheel / gestures to pan
// Trackpads and mice work on significantly different scales
const factor = isTrackpad ? 0.18 : 0.008_333
if (!isTrackpad && e.shiftKey && e.deltaX === 0) {
this.ds.offset[0] -= e.deltaY * (1 + factor) * (1 / scale)
} else {
this.ds.offset[0] -= e.deltaX * (1 + factor) * (1 / scale)
this.ds.offset[1] -= e.deltaY * (1 + factor) * (1 / scale)
}
}

View File

@@ -284,6 +284,7 @@ export class LiteGraphGlobal {
]
/**
* @deprecated Removed; has no effect.
* If `true`, mouse wheel events will be interpreted as trackpad gestures.
* Tested on MacBook M4 Pro.
* @default false
@@ -292,6 +293,7 @@ export class LiteGraphGlobal {
macTrackpadGestures: boolean = false
/**
* @deprecated Removed; has no effect.
* If both this setting and {@link macTrackpadGestures} are `true`, trackpad gestures will
* only be enabled when the browser user agent includes "Mac".
* @default true

View File

@@ -1,4 +1,4 @@
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import type {
ReadOnlyRect,

View File

@@ -1,4 +1,4 @@
import { pull } from 'lodash'
import { pull } from 'es-toolkit/compat'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/LLink'

View File

@@ -1,4 +1,4 @@
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'

View File

@@ -1,4 +1,4 @@
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import type { IKnobWidget } from '@/lib/litegraph/src/types/widgets'
import { getWidgetStep } from '@/lib/litegraph/src/utils/widget'

View File

@@ -1,4 +1,4 @@
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import type { ISliderWidget } from '@/lib/litegraph/src/types/widgets'

View File

@@ -1,4 +1,4 @@
import { clamp } from 'lodash'
import { clamp } from 'es-toolkit/compat'
import { beforeEach, describe, expect, vi } from 'vitest'
import { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal'

View File

@@ -0,0 +1,197 @@
import { assert, describe, expect, it } from 'vitest'
import {
ISlotType,
LGraph,
LGraphNode,
LiteGraph
} from '@/lib/litegraph/src/litegraph'
import {
createTestSubgraph,
createTestSubgraphNode
} from './fixtures/subgraphHelpers'
function createNode(
graph: LGraph,
inputs: ISlotType[] = [],
outputs: ISlotType[] = [],
title?: string
) {
const type = JSON.stringify({ inputs, outputs })
if (!LiteGraph.registered_node_types[type]) {
class testnode extends LGraphNode {
constructor(title: string) {
super(title)
let i_count = 0
for (const input of inputs) this.addInput('input_' + i_count++, input)
let o_count = 0
for (const output of outputs)
this.addOutput('output_' + o_count++, output)
}
}
LiteGraph.registered_node_types[type] = testnode
}
const node = LiteGraph.createNode(type, title)
if (!node) {
throw new Error('Failed to create node')
}
graph.add(node)
return node
}
describe('SubgraphConversion', () => {
describe('Subgraph Unpacking Functionality', () => {
it('Should keep interior nodes and links', () => {
const subgraph = createTestSubgraph()
const subgraphNode = createTestSubgraphNode(subgraph)
const graph = subgraphNode.graph
graph.add(subgraphNode)
const node1 = createNode(subgraph, [], ['number'])
const node2 = createNode(subgraph, ['number'])
node1.connect(0, node2, 0)
graph.unpackSubgraph(subgraphNode)
expect(graph.nodes.length).toBe(2)
expect(graph.links.size).toBe(1)
})
it('Should merge boundry links', () => {
const subgraph = createTestSubgraph({
inputs: [{ name: 'value', type: 'number' }],
outputs: [{ name: 'value', type: 'number' }]
})
const subgraphNode = createTestSubgraphNode(subgraph)
const graph = subgraphNode.graph
graph.add(subgraphNode)
const innerNode1 = createNode(subgraph, [], ['number'])
const innerNode2 = createNode(subgraph, ['number'], [])
subgraph.inputNode.slots[0].connect(innerNode2.inputs[0], innerNode2)
subgraph.outputNode.slots[0].connect(innerNode1.outputs[0], innerNode1)
const outerNode1 = createNode(graph, [], ['number'])
const outerNode2 = createNode(graph, ['number'])
outerNode1.connect(0, subgraphNode, 0)
subgraphNode.connect(0, outerNode2, 0)
graph.unpackSubgraph(subgraphNode)
expect(graph.nodes.length).toBe(4)
expect(graph.links.size).toBe(2)
})
it('Should keep reroutes', () => {
const subgraph = createTestSubgraph({
outputs: [{ name: 'value', type: 'number' }]
})
const subgraphNode = createTestSubgraphNode(subgraph)
const graph = subgraphNode.graph
graph.add(subgraphNode)
const inner = createNode(subgraph, [], ['number'])
const innerLink = subgraph.outputNode.slots[0].connect(
inner.outputs[0],
inner
)
assert(innerLink)
const outer = createNode(graph, ['number'])
const outerLink = subgraphNode.connect(0, outer, 0)
assert(outerLink)
subgraph.createReroute([10, 10], innerLink)
graph.createReroute([10, 10], outerLink)
graph.unpackSubgraph(subgraphNode)
expect(graph.reroutes.size).toBe(2)
})
it('Should map reroutes onto split outputs', () => {
const subgraph = createTestSubgraph({
outputs: [
{ name: 'value1', type: 'number' },
{ name: 'value2', type: 'number' }
]
})
const subgraphNode = createTestSubgraphNode(subgraph)
const graph = subgraphNode.graph
graph.add(subgraphNode)
const inner = createNode(subgraph, [], ['number', 'number'])
const innerLink1 = subgraph.outputNode.slots[0].connect(
inner.outputs[0],
inner
)
const innerLink2 = subgraph.outputNode.slots[1].connect(
inner.outputs[1],
inner
)
const outer1 = createNode(graph, ['number'])
const outer2 = createNode(graph, ['number'])
const outer3 = createNode(graph, ['number'])
const outerLink1 = subgraphNode.connect(0, outer1, 0)
assert(innerLink1 && innerLink2 && outerLink1)
subgraphNode.connect(0, outer2, 0)
subgraphNode.connect(1, outer3, 0)
subgraph.createReroute([10, 10], innerLink1)
subgraph.createReroute([10, 20], innerLink2)
graph.createReroute([10, 10], outerLink1)
graph.unpackSubgraph(subgraphNode)
expect(graph.reroutes.size).toBe(3)
expect(graph.links.size).toBe(3)
let linkRefCount = 0
for (const reroute of graph.reroutes.values()) {
linkRefCount += reroute.linkIds.size
}
expect(linkRefCount).toBe(4)
})
it('Should map reroutes onto split inputs', () => {
const subgraph = createTestSubgraph({
inputs: [
{ name: 'value1', type: 'number' },
{ name: 'value2', type: 'number' }
]
})
const subgraphNode = createTestSubgraphNode(subgraph)
const graph = subgraphNode.graph
graph.add(subgraphNode)
const inner1 = createNode(subgraph, ['number', 'number'])
const inner2 = createNode(subgraph, ['number'])
const innerLink1 = subgraph.inputNode.slots[0].connect(
inner1.inputs[0],
inner1
)
const innerLink2 = subgraph.inputNode.slots[1].connect(
inner1.inputs[1],
inner1
)
const innerLink3 = subgraph.inputNode.slots[1].connect(
inner2.inputs[0],
inner2
)
assert(innerLink1 && innerLink2 && innerLink3)
const outer = createNode(graph, [], ['number'])
const outerLink1 = outer.connect(0, subgraphNode, 0)
const outerLink2 = outer.connect(0, subgraphNode, 1)
assert(outerLink1 && outerLink2)
graph.createReroute([10, 10], outerLink1)
graph.createReroute([10, 20], outerLink2)
subgraph.createReroute([10, 10], innerLink1)
graph.unpackSubgraph(subgraphNode)
expect(graph.reroutes.size).toBe(3)
expect(graph.links.size).toBe(3)
let linkRefCount = 0
for (const reroute of graph.reroutes.values()) {
linkRefCount += reroute.linkIds.size
}
expect(linkRefCount).toBe(4)
})
})
})

View File

@@ -0,0 +1,273 @@
{
"Comfy-Desktop_CheckForUpdates": {
"label": "التحقق من التحديثات"
},
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
"label": "فتح مجلد العقد المخصصة"
},
"Comfy-Desktop_Folders_OpenInputsFolder": {
"label": "فتح مجلد المدخلات"
},
"Comfy-Desktop_Folders_OpenLogsFolder": {
"label": "فتح مجلد السجلات"
},
"Comfy-Desktop_Folders_OpenModelConfig": {
"label": "فتح extra_model_paths.yaml"
},
"Comfy-Desktop_Folders_OpenModelsFolder": {
"label": "فتح مجلد النماذج"
},
"Comfy-Desktop_Folders_OpenOutputsFolder": {
"label": "فتح مجلد المخرجات"
},
"Comfy-Desktop_OpenDevTools": {
"label": "فتح أدوات المطور"
},
"Comfy-Desktop_OpenUserGuide": {
"label": "دليل المستخدم لسطح المكتب"
},
"Comfy-Desktop_Quit": {
"label": "خروج"
},
"Comfy-Desktop_Reinstall": {
"label": "إعادة التثبيت"
},
"Comfy-Desktop_Restart": {
"label": "إعادة التشغيل"
},
"Comfy_3DViewer_Open3DViewer": {
"label": "فتح عارض ثلاثي الأبعاد (بيتا) للعقدة المحددة"
},
"Comfy_BrowseTemplates": {
"label": "تصفح القوالب"
},
"Comfy_Canvas_AddEditModelStep": {
"label": "إضافة خطوة تحرير النموذج"
},
"Comfy_Canvas_DeleteSelectedItems": {
"label": "حذف العناصر المحددة"
},
"Comfy_Canvas_FitView": {
"label": "تعديل العرض ليناسب العقد المحددة"
},
"Comfy_Canvas_MoveSelectedNodes_Down": {
"label": "تحريك العقد المحددة للأسفل"
},
"Comfy_Canvas_MoveSelectedNodes_Left": {
"label": "تحريك العقد المحددة لليسار"
},
"Comfy_Canvas_MoveSelectedNodes_Right": {
"label": "تحريك العقد المحددة لليمين"
},
"Comfy_Canvas_MoveSelectedNodes_Up": {
"label": "تحريك العقد المحددة للأعلى"
},
"Comfy_Canvas_ResetView": {
"label": "إعادة تعيين العرض"
},
"Comfy_Canvas_Resize": {
"label": "تغيير حجم العقد المحددة"
},
"Comfy_Canvas_ToggleLinkVisibility": {
"label": "تبديل رؤية الروابط في اللوحة"
},
"Comfy_Canvas_ToggleLock": {
"label": "تبديل القفل في اللوحة"
},
"Comfy_Canvas_ToggleMinimap": {
"label": "تبديل الخريطة المصغرة في اللوحة"
},
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
"label": "تجاوز/إلغاء تجاوز العقد المحددة"
},
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
"label": "طي/توسيع العقد المحددة"
},
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
"label": "كتم/إلغاء كتم العقد المحددة"
},
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
"label": "تثبيت/إلغاء تثبيت العقد المحددة"
},
"Comfy_Canvas_ToggleSelected_Pin": {
"label": "تثبيت/إلغاء تثبيت العناصر المحددة"
},
"Comfy_Canvas_ZoomIn": {
"label": "تكبير"
},
"Comfy_Canvas_ZoomOut": {
"label": "تصغير"
},
"Comfy_ClearPendingTasks": {
"label": "مسح المهام المعلقة"
},
"Comfy_ClearWorkflow": {
"label": "مسح سير العمل"
},
"Comfy_ContactSupport": {
"label": "الاتصال بالدعم"
},
"Comfy_DuplicateWorkflow": {
"label": "تكرار سير العمل الحالي"
},
"Comfy_ExportWorkflow": {
"label": "تصدير سير العمل"
},
"Comfy_ExportWorkflowAPI": {
"label": "تصدير سير العمل (تنسيق API)"
},
"Comfy_Feedback": {
"label": "إرسال ملاحظات"
},
"Comfy_Graph_ConvertToSubgraph": {
"label": "تحويل التحديد إلى رسم فرعي"
},
"Comfy_Graph_ExitSubgraph": {
"label": "الخروج من الرسم البياني الفرعي"
},
"Comfy_Graph_FitGroupToContents": {
"label": "ضبط المجموعة على المحتويات"
},
"Comfy_Graph_GroupSelectedNodes": {
"label": "تجميع العقد المحددة"
},
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
"label": "تحويل العقد المحددة إلى عقدة مجموعة"
},
"Comfy_GroupNode_ManageGroupNodes": {
"label": "إدارة عقد المجموعات"
},
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
"label": "إلغاء تجميع عقد المجموعات المحددة"
},
"Comfy_Help_AboutComfyUI": {
"label": "حول ComfyUI"
},
"Comfy_Help_OpenComfyOrgDiscord": {
"label": "فتح خادم Comfy-Org على Discord"
},
"Comfy_Help_OpenComfyUIDocs": {
"label": "فتح مستندات ComfyUI"
},
"Comfy_Help_OpenComfyUIForum": {
"label": "فتح منتدى ComfyUI"
},
"Comfy_Help_OpenComfyUIIssues": {
"label": "فتح مشكلات ComfyUI"
},
"Comfy_Interrupt": {
"label": "إيقاف مؤقت"
},
"Comfy_LoadDefaultWorkflow": {
"label": "تحميل سير العمل الافتراضي"
},
"Comfy_Manager_CustomNodesManager": {
"label": "تبديل مدير العقد المخصصة"
},
"Comfy_Manager_ToggleManagerProgressDialog": {
"label": "تبديل شريط تقدم مدير العقد المخصصة"
},
"Comfy_MaskEditor_BrushSize_Decrease": {
"label": "تقليل حجم الفرشاة في محرر القناع"
},
"Comfy_MaskEditor_BrushSize_Increase": {
"label": "زيادة حجم الفرشاة في محرر القناع"
},
"Comfy_MaskEditor_OpenMaskEditor": {
"label": "فتح محرر القناع للعقدة المحددة"
},
"Comfy_NewBlankWorkflow": {
"label": "سير عمل جديد فارغ"
},
"Comfy_OpenClipspace": {
"label": "Clipspace"
},
"Comfy_OpenWorkflow": {
"label": "فتح سير عمل"
},
"Comfy_QueuePrompt": {
"label": "إضافة الأمر إلى قائمة الانتظار"
},
"Comfy_QueuePromptFront": {
"label": "إضافة الأمر إلى مقدمة قائمة الانتظار"
},
"Comfy_QueueSelectedOutputNodes": {
"label": "إدراج عقد الإخراج المحددة في قائمة الانتظار"
},
"Comfy_Redo": {
"label": "إعادة"
},
"Comfy_RefreshNodeDefinitions": {
"label": "تحديث تعريفات العقد"
},
"Comfy_SaveWorkflow": {
"label": "حفظ سير العمل"
},
"Comfy_SaveWorkflowAs": {
"label": "حفظ سير العمل باسم"
},
"Comfy_ShowSettingsDialog": {
"label": "عرض نافذة الإعدادات"
},
"Comfy_ToggleTheme": {
"label": "تبديل النمط (فاتح/داكن)"
},
"Comfy_Undo": {
"label": "تراجع"
},
"Comfy_User_OpenSignInDialog": {
"label": "فتح نافذة تسجيل الدخول"
},
"Comfy_User_SignOut": {
"label": "تسجيل الخروج"
},
"Workspace_CloseWorkflow": {
"label": "إغلاق سير العمل الحالي"
},
"Workspace_NextOpenedWorkflow": {
"label": "سير العمل التالي المفتوح"
},
"Workspace_PreviousOpenedWorkflow": {
"label": "سير العمل السابق المفتوح"
},
"Workspace_SearchBox_Toggle": {
"label": "تبديل مربع البحث"
},
"Workspace_ToggleBottomPanel": {
"label": "تبديل اللوحة السفلية"
},
"Workspace_ToggleBottomPanelTab_command-terminal": {
"label": "تبديل لوحة الطرفية السفلية"
},
"Workspace_ToggleBottomPanelTab_logs-terminal": {
"label": "تبديل لوحة السجلات السفلية"
},
"Workspace_ToggleBottomPanelTab_shortcuts-essentials": {
"label": "تبديل اللوحة السفلية الأساسية"
},
"Workspace_ToggleBottomPanelTab_shortcuts-view-controls": {
"label": "تبديل لوحة تحكم العرض السفلية"
},
"Workspace_ToggleBottomPanel_Shortcuts": {
"label": "عرض مربع حوار اختصارات لوحة المفاتيح"
},
"Workspace_ToggleFocusMode": {
"label": "تبديل وضع التركيز"
},
"Workspace_ToggleSidebarTab_model-library": {
"label": "تبديل الشريط الجانبي لمكتبة النماذج",
"tooltip": "مكتبة النماذج"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "تبديل الشريط الجانبي لمكتبة العقد",
"tooltip": "مكتبة العقد"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "تبديل الشريط الجانبي لقائمة الانتظار",
"tooltip": "قائمة الانتظار"
},
"Workspace_ToggleSidebarTab_workflows": {
"label": "تبديل الشريط الجانبي لسير العمل",
"tooltip": "سير العمل"
}
}

1682
src/locales/ar/main.json Normal file

File diff suppressed because it is too large Load Diff

8660
src/locales/ar/nodeDefs.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,416 @@
{
"Comfy-Desktop_AutoUpdate": {
"name": "التحقق تلقائيًا من التحديثات"
},
"Comfy-Desktop_SendStatistics": {
"name": "إرسال إحصائيات الاستخدام المجهولة"
},
"Comfy-Desktop_UV_PypiInstallMirror": {
"name": "مرآة تثبيت Pypi",
"tooltip": "مرآة التثبيت الافتراضية لـ pip"
},
"Comfy-Desktop_UV_PythonInstallMirror": {
"name": "مرآة تثبيت بايثون",
"tooltip": "يتم تحميل تثبيتات بايثون المدارة من مشروع Astral python-build-standalone. يمكن تعيين هذا المتغير إلى عنوان مرآة لاستخدام مصدر مختلف لتثبيتات بايثون. سيحل العنوان المقدم محل https://github.com/astral-sh/python-build-standalone/releases/download في، مثلاً، https://github.com/astral-sh/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz. يمكن قراءة التوزيعات من دليل محلي باستخدام نظام ملفات file://."
},
"Comfy-Desktop_UV_TorchInstallMirror": {
"name": "مرآة تثبيت Torch",
"tooltip": "مرآة تثبيت pip لـ pytorch"
},
"Comfy-Desktop_WindowStyle": {
"name": "نمط النافذة",
"options": {
"custom": "مخصص",
"default": "افتراضي"
},
"tooltip": "مخصص: استبدال شريط عنوان النظام بالقائمة العلوية لـ ComfyUI"
},
"Comfy_Canvas_BackgroundImage": {
"name": "صورة خلفية اللوحة",
"tooltip": "رابط صورة لخلفية اللوحة. يمكنك النقر بزر الفأرة الأيمن على صورة في لوحة النتائج واختيار \"تعيين كخلفية\" لاستخدامها، أو رفع صورتك الخاصة باستخدام زر الرفع."
},
"Comfy_Canvas_NavigationMode": {
"name": "وضع تنقل اللوحة",
"options": {
"Left-Click Pan (Legacy)": "سحب بالنقر الأيسر (قديم)",
"Standard (New)": "قياسي (جديد)"
}
},
"Comfy_Canvas_SelectionToolbox": {
"name": "عرض صندوق أدوات التحديد"
},
"Comfy_ConfirmClear": {
"name": "طلب التأكيد عند مسح سير العمل"
},
"Comfy_DOMClippingEnabled": {
"name": "تمكين قص عناصر DOM (قد يقلل التمكين من الأداء)"
},
"Comfy_DevMode": {
"name": "تمكين خيارات وضع المطور (حفظ API، إلخ)"
},
"Comfy_DisableFloatRounding": {
"name": "تعطيل تقريب عناصر التحكم العائمة الافتراضية",
"tooltip": "(يتطلب إعادة تحميل الصفحة) لا يمكن تعطيل التقريب عندما يتم تعيينه من العقدة في الخلفية."
},
"Comfy_DisableSliders": {
"name": "تعطيل منزلقات أدوات العقد"
},
"Comfy_EditAttention_Delta": {
"name": "دقة تحكم +Ctrl فوق/تحت"
},
"Comfy_EnableTooltips": {
"name": "تمكين التلميحات"
},
"Comfy_EnableWorkflowViewRestore": {
"name": "حفظ واستعادة موقع اللوحة ومستوى التكبير في سير العمل"
},
"Comfy_FloatRoundingPrecision": {
"name": "عدد أرقام التقريب العشرية لأدوات التحكم العائمة [0 = تلقائي]",
"tooltip": "(يتطلب إعادة تحميل الصفحة)"
},
"Comfy_Graph_CanvasInfo": {
"name": "عرض معلومات اللوحة في الزاوية السفلى اليسرى (الإطارات في الثانية، إلخ)"
},
"Comfy_Graph_CanvasMenu": {
"name": "عرض قائمة لوحة الرسم البياني"
},
"Comfy_Graph_CtrlShiftZoom": {
"name": "تمكين اختصار التكبير السريع (Ctrl + Shift + سحب)"
},
"Comfy_Graph_LinkMarkers": {
"name": "علامات منتصف الروابط",
"options": {
"Arrow": "سهم",
"Circle": "دائرة",
"None": "لا شيء"
}
},
"Comfy_Graph_ZoomSpeed": {
"name": "سرعة تكبير اللوحة"
},
"Comfy_GroupSelectedNodes_Padding": {
"name": "تباعد حول العقد المحددة في المجموعة"
},
"Comfy_Group_DoubleClickTitleToEdit": {
"name": "انقر مزدوج على عنوان المجموعة للتحرير"
},
"Comfy_LinkRelease_Action": {
"name": "الإجراء عند تحرير الرابط (بدون مفتاح تعديل)",
"options": {
"context menu": "قائمة السياق",
"no action": "لا إجراء",
"search box": "صندوق البحث"
}
},
"Comfy_LinkRelease_ActionShift": {
"name": "الإجراء عند تحرير الرابط (Shift)",
"options": {
"context menu": "قائمة السياق",
"no action": "لا إجراء",
"search box": "صندوق البحث"
}
},
"Comfy_LinkRenderMode": {
"name": "وضع عرض الروابط",
"options": {
"Hidden": "مخفي",
"Linear": "خطي",
"Spline": "منحنى",
"Straight": "مستقيم"
}
},
"Comfy_Load3D_3DViewerEnable": {
"name": "تمكين عارض ثلاثي الأبعاد (تجريبي)",
"tooltip": "تمكين عارض ثلاثي الأبعاد (تجريبي) للعقد المحددة. تتيح هذه الميزة عرض النماذج ثلاثية الأبعاد والتفاعل معها مباشرة داخل العارض ثلاثي الأبعاد بحجمه الكامل."
},
"Comfy_Load3D_BackgroundColor": {
"name": "لون الخلفية الابتدائي",
"tooltip": "يحدد لون الخلفية الافتراضي للمشهد ثلاثي الأبعاد. يمكن تعديل هذا اللون لكل عنصر ثلاثي الأبعاد بعد الإنشاء."
},
"Comfy_Load3D_CameraType": {
"name": "نوع الكاميرا الابتدائي",
"options": {
"orthographic": "متعامد",
"perspective": "منظور"
},
"tooltip": "يحدد ما إذا كانت الكاميرا منظور أو متعامدة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد. يمكن تعديل هذا الإعداد لكل عنصر بعد الإنشاء."
},
"Comfy_Load3D_LightAdjustmentIncrement": {
"name": "زيادة تعديل الضوء",
"tooltip": "يتحكم في حجم الخطوة عند تعديل شدة الإضاءة في المشاهد ثلاثية الأبعاد. قيمة أصغر تسمح بتحكم أدق، وأكبر قيمة تعطي تغييرات أكثر وضوحًا."
},
"Comfy_Load3D_LightIntensity": {
"name": "شدة الإضاءة الابتدائية",
"tooltip": "يحدد مستوى سطوع الإضاءة الافتراضي في المشهد ثلاثي الأبعاد. يمكن تعديله لكل عنصر بعد الإنشاء."
},
"Comfy_Load3D_LightIntensityMaximum": {
"name": "أقصى شدة إضاءة",
"tooltip": "يحدد الحد الأقصى المسموح به لشدة الإضاءة في المشاهد ثلاثية الأبعاد."
},
"Comfy_Load3D_LightIntensityMinimum": {
"name": "أدنى شدة إضاءة",
"tooltip": "يحدد الحد الأدنى المسموح به لشدة الإضاءة في المشاهد ثلاثية الأبعاد."
},
"Comfy_Load3D_ShowGrid": {
"name": "رؤية الشبكة الابتدائية",
"tooltip": "يتحكم في ظهور الشبكة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد."
},
"Comfy_Load3D_ShowPreview": {
"name": "رؤية المعاينة الابتدائية",
"tooltip": "يتحكم في ظهور شاشة المعاينة بشكل افتراضي عند إنشاء عنصر ثلاثي الأبعاد جديد."
},
"Comfy_Locale": {
"name": "اللغة"
},
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
"name": "مضاعف سرعة تعديل الفرشاة",
"tooltip": "يتحكم في سرعة تغير حجم الفرشاة وصلابتها أثناء التعديل. القيم الأعلى تعني تغييرات أسرع."
},
"Comfy_MaskEditor_UseDominantAxis": {
"name": "تقييد تعديل الفرشاة إلى المحور السائد",
"tooltip": "عند التمكين، تؤثر التعديلات على الحجم أو الصلابة فقط بناءً على الاتجاه الذي تتحرك فيه أكثر."
},
"Comfy_MaskEditor_UseNewEditor": {
"name": "استخدام محرر القناع الجديد",
"tooltip": "التحويل إلى واجهة محرر القناع الجديدة"
},
"Comfy_ModelLibrary_AutoLoadAll": {
"name": "تحميل جميع مجلدات النماذج تلقائيًا",
"tooltip": "إذا كانت صحيحة، سيتم تحميل جميع المجلدات عند فتح مكتبة النماذج (قد يسبب تأخيرًا أثناء التحميل). إذا كانت خاطئة، يتم تحميل مجلدات النماذج على مستوى الجذر فقط عند النقر عليها."
},
"Comfy_ModelLibrary_NameFormat": {
"name": "اسم العرض في شجرة مكتبة النماذج",
"options": {
"filename": "اسم الملف",
"title": "العنوان"
},
"tooltip": "اختر \"اسم الملف\" لعرض اسم الملف المبسط بدون المجلد أو الامتداد \".safetensors\" في قائمة النماذج. اختر \"العنوان\" لعرض عنوان بيانات النموذج القابل للتكوين."
},
"Comfy_NodeBadge_NodeIdBadgeMode": {
"name": "وضع شارة معرف العقدة",
"options": {
"None": "لا شيء",
"Show all": "عرض الكل"
}
},
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
"name": "وضع شارة دورة حياة العقدة",
"options": {
"None": "لا شيء",
"Show all": "عرض الكل"
}
},
"Comfy_NodeBadge_NodeSourceBadgeMode": {
"name": "وضع شارة مصدر العقدة",
"options": {
"Hide built-in": "إخفاء المدمج",
"None": "لا شيء",
"Show all": "عرض الكل"
}
},
"Comfy_NodeBadge_ShowApiPricing": {
"name": "عرض شارة تسعير عقدة API"
},
"Comfy_NodeSearchBoxImpl": {
"name": "تنفيذ مربع بحث العقدة",
"options": {
"default": "افتراضي",
"litegraph (legacy)": "لايت جراف (قديم)"
}
},
"Comfy_NodeSearchBoxImpl_NodePreview": {
"name": "معاينة العقدة",
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
},
"Comfy_NodeSearchBoxImpl_ShowCategory": {
"name": "عرض فئة العقدة في نتائج البحث",
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
},
"Comfy_NodeSearchBoxImpl_ShowIdName": {
"name": "عرض اسم معرف العقدة في نتائج البحث",
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
},
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
"name": "عرض تكرار العقدة في نتائج البحث",
"tooltip": "ينطبق فقط على التنفيذ الافتراضي"
},
"Comfy_NodeSuggestions_number": {
"name": "عدد اقتراحات العقد",
"tooltip": "خاص بمربع بحث / قائمة السياق في لايت جراف فقط"
},
"Comfy_Node_AllowImageSizeDraw": {
"name": "عرض العرض × الارتفاع تحت معاينة الصورة"
},
"Comfy_Node_AutoSnapLinkToSlot": {
"name": "التثبيت التلقائي للرابط إلى فتحة العقدة",
"tooltip": "عند سحب رابط فوق عقدة، يتم تثبيت الرابط تلقائيًا على فتحة إدخال صالحة في العقدة"
},
"Comfy_Node_BypassAllLinksOnDelete": {
"name": "الحفاظ على جميع الروابط عند حذف العقد",
"tooltip": "عند حذف عقدة، حاول إعادة توصيل جميع روابط الإدخال والإخراج (تجاوز العقدة المحذوفة)"
},
"Comfy_Node_DoubleClickTitleToEdit": {
"name": "النقر المزدوج على عنوان العقدة للتحرير"
},
"Comfy_Node_MiddleClickRerouteNode": {
"name": "النقر الأوسط ينشئ عقدة إعادة توجيه جديدة"
},
"Comfy_Node_Opacity": {
"name": "شفافية العقدة"
},
"Comfy_Node_ShowDeprecated": {
"name": "عرض العقدة المهجورة في البحث",
"tooltip": "العقد المهجورة مخفية افتراضيًا في واجهة المستخدم، لكنها تظل فعالة في سير العمل الحالي الذي يستخدمها."
},
"Comfy_Node_ShowExperimental": {
"name": "عرض العقدة التجريبية في البحث",
"tooltip": "يتم تمييز العقد التجريبية في واجهة المستخدم وقد تخضع لتغييرات كبيرة أو إزالتها في الإصدارات المستقبلية. استخدمها بحذر في سير العمل الإنتاجي."
},
"Comfy_Node_SnapHighlightsNode": {
"name": "تثبيت يبرز العقدة",
"tooltip": "عند سحب رابط فوق عقدة تحتوي على فتحة إدخال صالحة، يتم تمييز العقدة"
},
"Comfy_Notification_ShowVersionUpdates": {
"name": "عرض تحديثات الإصدار",
"tooltip": "عرض التحديثات للنماذج الجديدة والميزات الرئيسية."
},
"Comfy_Pointer_ClickBufferTime": {
"name": "تأخير انحراف نقرة المؤشر",
"tooltip": "بعد الضغط على زر المؤشر، هذا هو الوقت الأقصى (بالملي ثانية) الذي يمكن تجاهل حركة المؤشر خلاله.\n\nيساعد على منع دفع الكائنات عن طريق الخطأ إذا تم تحريك المؤشر أثناء النقر."
},
"Comfy_Pointer_ClickDrift": {
"name": "انحراف نقرة المؤشر (أقصى مسافة)",
"tooltip": "إذا تحرك المؤشر أكثر من هذه المسافة أثناء الضغط على زر، يعتبر سحبًا بدلاً من نقرة.\n\nيساعد على منع دفع الكائنات عن طريق الخطأ إذا تم تحريك المؤشر أثناء النقر."
},
"Comfy_Pointer_DoubleClickTime": {
"name": "فترة النقر المزدوج (قصوى)",
"tooltip": "الوقت الأقصى بالملي ثانية بين النقرتين في النقر المزدوج. زيادة هذه القيمة قد تساعد إذا لم يتم تسجيل النقرات المزدوجة أحيانًا."
},
"Comfy_PreviewFormat": {
"name": "تنسيق صورة المعاينة",
"tooltip": "عند عرض معاينة في ويدجت الصورة، يتم تحويلها إلى صورة خفيفة الوزن، مثل webp، jpeg، webp;50، إلخ."
},
"Comfy_PromptFilename": {
"name": "طلب اسم الملف عند حفظ سير العمل"
},
"Comfy_QueueButton_BatchCountLimit": {
"name": "حد عدد الدُفعات",
"tooltip": "العدد الأقصى للمهام التي تضاف إلى القائمة بنقرة زر واحدة"
},
"Comfy_Queue_MaxHistoryItems": {
"name": "حجم تاريخ قائمة الانتظار",
"tooltip": "العدد الأقصى للمهام المعروضة في تاريخ قائمة الانتظار."
},
"Comfy_Sidebar_Location": {
"name": "موقع الشريط الجانبي",
"options": {
"left": "يسار",
"right": "يمين"
}
},
"Comfy_Sidebar_Size": {
"name": "حجم الشريط الجانبي",
"options": {
"normal": "عادي",
"small": "صغير"
}
},
"Comfy_Sidebar_UnifiedWidth": {
"name": "عرض موحد للشريط الجانبي"
},
"Comfy_SnapToGrid_GridSize": {
"name": "حجم الالتصاق بالشبكة",
"tooltip": "عند سحب وتغيير حجم العقد مع الضغط على shift، يتم محاذاتها إلى الشبكة، هذا يتحكم في حجم تلك الشبكة."
},
"Comfy_TextareaWidget_FontSize": {
"name": "حجم خط ويدجت منطقة النص"
},
"Comfy_TextareaWidget_Spellcheck": {
"name": "التحقق من الإملاء في ويدجت منطقة النص"
},
"Comfy_TreeExplorer_ItemPadding": {
"name": "حشو عناصر مستعرض الشجرة"
},
"Comfy_UseNewMenu": {
"name": "استخدام القائمة الجديدة",
"options": {
"Bottom": "أسفل",
"Disabled": "معطل",
"Top": "أعلى"
},
"tooltip": "موقع شريط القائمة. على الأجهزة المحمولة، تُعرض القائمة دائمًا في الأعلى."
},
"Comfy_Validation_Workflows": {
"name": "التحقق من صحة سير العمل"
},
"Comfy_WidgetControlMode": {
"name": "وضع التحكم في الودجت",
"options": {
"after": "بعد",
"before": "قبل"
},
"tooltip": "يتحكم في متى يتم تحديث قيم الودجت (توليد عشوائي/زيادة/نقصان)، إما قبل إدراج الطلب في الطابور أو بعده."
},
"Comfy_Window_UnloadConfirmation": {
"name": "عرض تأكيد عند إغلاق النافذة"
},
"Comfy_Workflow_AutoSave": {
"name": "الحفظ التلقائي",
"options": {
"after delay": "بعد تأخير",
"off": "إيقاف"
}
},
"Comfy_Workflow_AutoSaveDelay": {
"name": "تأخير الحفظ التلقائي (بالملي ثانية)",
"tooltip": "ينطبق فقط إذا تم تعيين الحفظ التلقائي إلى \"بعد تأخير\"."
},
"Comfy_Workflow_ConfirmDelete": {
"name": "عرض تأكيد عند حذف سير العمل"
},
"Comfy_Workflow_Persist": {
"name": "الاحتفاظ بحالة سير العمل واستعادتها عند (إعادة) تحميل الصفحة"
},
"Comfy_Workflow_ShowMissingModelsWarning": {
"name": "عرض تحذير النماذج المفقودة"
},
"Comfy_Workflow_ShowMissingNodesWarning": {
"name": "عرض تحذير العقد المفقودة"
},
"Comfy_Workflow_SortNodeIdOnSave": {
"name": "ترتيب معرفات العقد عند حفظ سير العمل"
},
"Comfy_Workflow_WorkflowTabsPosition": {
"name": "موضع تبويبات سير العمل المفتوحة",
"options": {
"Sidebar": "الشريط الجانبي",
"Topbar": "شريط الأعلى",
"Topbar (2nd-row)": "شريط الأعلى (الصف الثاني)"
}
},
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
"name": "عتبة التكبير للرسم بجودة منخفضة",
"tooltip": "عرض أشكال بجودة منخفضة عند التكبير للخارج"
},
"LiteGraph_Canvas_MaximumFps": {
"name": "الحد الأقصى للإطارات في الثانية",
"tooltip": "الحد الأقصى لعدد الإطارات في الثانية التي يسمح للرسم أن يعرضها. يحد من استخدام GPU على حساب السلاسة. إذا كانت 0، يتم استخدام معدل تحديث الشاشة. الافتراضي: 0"
},
"LiteGraph_ContextMenu_Scaling": {
"name": "تغيير مقياس قوائم ودجت كومبو العقدة عند التكبير"
},
"LiteGraph_Node_DefaultPadding": {
"name": "تصغير العقد الجديدة دائمًا",
"tooltip": "تغيير حجم العقد إلى أصغر حجم ممكن عند الإنشاء. عند التعطيل، يتم توسيع العقدة المضافة حديثًا قليلاً لإظهار قيم الودجت."
},
"LiteGraph_Node_TooltipDelay": {
"name": "تأخير التلميح"
},
"LiteGraph_Reroute_SplineOffset": {
"name": "إزاحة منحنى إعادة التوجيه",
"tooltip": "إزاحة نقطة تحكم بيزير من نقطة مركز إعادة التوجيه"
},
"pysssss_SnapToGrid": {
"name": "الالتصاق بالشبكة دائمًا"
}
}

View File

@@ -125,6 +125,9 @@
"Comfy_Graph_ExitSubgraph": {
"label": "Exit Subgraph"
},
"Comfy_Graph_UnpackSubgraph": {
"label": "Unpack the selected Subgraph"
},
"Comfy_Graph_FitGroupToContents": {
"label": "Fit Group To Contents"
},

View File

@@ -145,7 +145,8 @@
"stopRecording": "Stop Recording",
"micPermissionDenied": "Microphone permission denied",
"noAudioRecorded": "No audio recorded",
"nodesRunning": "nodes running"
"nodesRunning": "nodes running",
"duplicate": "Duplicate"
},
"manager": {
"title": "Custom Nodes Manager",

View File

@@ -299,6 +299,7 @@
"disabling": "Deshabilitando",
"dismiss": "Descartar",
"download": "Descargar",
"duplicate": "Duplicar",
"edit": "Editar",
"empty": "Vacío",
"enableAll": "Habilitar todo",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "Advertencia de compatibilidad de versión",
"versionMismatchWarningMessage": "{warning}: {detail} Visita https://docs.comfy.org/installation/update_comfyui#common-update-issues para obtener instrucciones de actualización.",
"videoFailedToLoad": "Falló la carga del video",
"workflow": "Flujo de trabajo"
"workflow": "Flujo de trabajo",
"duplicate": "Duplicar"
},
"graphCanvasMenu": {
"fitView": "Ajustar vista",

View File

@@ -299,6 +299,7 @@
"disabling": "Désactivation",
"dismiss": "Fermer",
"download": "Télécharger",
"duplicate": "Dupliquer",
"edit": "Modifier",
"empty": "Vide",
"enableAll": "Activer tout",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "Avertissement de compatibilité de version",
"versionMismatchWarningMessage": "{warning} : {detail} Consultez https://docs.comfy.org/installation/update_comfyui#common-update-issues pour les instructions de mise à jour.",
"videoFailedToLoad": "Échec du chargement de la vidéo",
"workflow": "Flux de travail"
"workflow": "Flux de travail",
"duplicate": "Dupliquer"
},
"graphCanvasMenu": {
"fitView": "Adapter la vue",

View File

@@ -299,6 +299,7 @@
"disabling": "無効化",
"dismiss": "閉じる",
"download": "ダウンロード",
"duplicate": "複製",
"edit": "編集",
"empty": "空",
"enableAll": "すべて有効にする",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "バージョン互換性の警告",
"versionMismatchWarningMessage": "{warning}: {detail} 更新手順については https://docs.comfy.org/installation/update_comfyui#common-update-issues をご覧ください。",
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
"workflow": "ワークフロー"
"workflow": "ワークフロー",
"duplicate": "複製"
},
"graphCanvasMenu": {
"fitView": "ビューに合わせる",

View File

@@ -299,6 +299,7 @@
"disabling": "비활성화 중",
"dismiss": "닫기",
"download": "다운로드",
"duplicate": "복제",
"edit": "편집",
"empty": "비어 있음",
"enableAll": "모두 활성화",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "버전 호환성 경고",
"versionMismatchWarningMessage": "{warning}: {detail} 업데이트 지침은 https://docs.comfy.org/installation/update_comfyui#common-update-issues 를 방문하세요.",
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
"workflow": "워크플로"
"workflow": "워크플로",
"duplicate": "복제"
},
"graphCanvasMenu": {
"fitView": "보기 맞춤",

View File

@@ -299,6 +299,7 @@
"disabling": "Отключение",
"dismiss": "Закрыть",
"download": "Скачать",
"duplicate": "Дублировать",
"edit": "Редактировать",
"empty": "Пусто",
"enableAll": "Включить все",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "Предупреждение о несовместимости версий",
"versionMismatchWarningMessage": "{warning}: {detail} Посетите https://docs.comfy.org/installation/update_comfyui#common-update-issues для инструкций по обновлению.",
"videoFailedToLoad": "Не удалось загрузить видео",
"workflow": "Рабочий процесс"
"workflow": "Рабочий процесс",
"duplicate": "Дублировать"
},
"graphCanvasMenu": {
"fitView": "Подгонять под выделенные",

View File

@@ -299,6 +299,7 @@
"disabling": "停用中",
"dismiss": "關閉",
"download": "下載",
"duplicate": "複製",
"edit": "編輯",
"empty": "空",
"enableAll": "全部啟用",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "版本相容性警告",
"versionMismatchWarningMessage": "{warning}{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
"videoFailedToLoad": "無法載入影片",
"workflow": "工作流程"
"workflow": "工作流程",
"duplicate": "複製"
},
"graphCanvasMenu": {
"fitView": "適合視窗",

View File

@@ -299,6 +299,7 @@
"disabling": "禁用中",
"dismiss": "關閉",
"download": "下载",
"duplicate": "复制",
"edit": "编辑",
"empty": "空",
"enableAll": "启用全部",
@@ -402,7 +403,8 @@
"versionMismatchWarning": "版本相容性警告",
"versionMismatchWarningMessage": "{warning}{detail} 請參閱 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新說明。",
"videoFailedToLoad": "视频加载失败",
"workflow": "工作流"
"workflow": "工作流",
"duplicate": "复制"
},
"graphCanvasMenu": {
"fitView": "适应视图",

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import type { ToastMessageOptions } from 'primevue/toast'
import { reactive } from 'vue'

View File

@@ -1,5 +1,5 @@
import _ from 'es-toolkit/compat'
import * as jsondiffpatch from 'jsondiffpatch'
import _ from 'lodash'
import log from 'loglevel'
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { type Component, toRaw } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import {
type INodeOutputSlot,

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
@@ -805,6 +805,15 @@ export const useLitegraphService = () => {
})
}
}
if (this instanceof SubgraphNode) {
options.unshift({
content: 'Unpack Subgraph',
callback: () => {
useNodeOutputStore().revokeSubgraphPreviews(this)
this.graph.unpackSubgraph(this)
}
})
}
return []
}

View File

@@ -4,7 +4,7 @@ import type {
SearchResponse
} from 'algoliasearch/dist/lite/browser'
import { liteClient as algoliasearch } from 'algoliasearch/dist/lite/builds/browser'
import { memoize, omit } from 'lodash'
import { memoize, omit } from 'es-toolkit/compat'
import {
MIN_CHARS_FOR_SUGGESTIONS_ALGOLIA,

View File

@@ -1,5 +1,5 @@
import QuickLRU from '@alloc/quick-lru'
import { partition } from 'lodash'
import { partition } from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { useCachedRequest } from '@/composables/useCachedRequest'

View File

@@ -1,6 +1,6 @@
// We should consider moving to https://primevue.org/dynamicdialog/ once everything is in Vue.
// Currently we need to bridge between legacy app code and Vue app with a Pinia store.
import { merge } from 'lodash'
import { merge } from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import type { DialogPassThroughOptions } from 'primevue/dialog'
import { type Component, markRaw, ref } from 'vue'

View File

@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
import {
ExecutedWsMessage,
ResultItem,
@@ -268,6 +268,20 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
app.nodePreviewImages = {}
}
/**
* Revoke all preview of a subgraph node and the graph it contains.
* Does not recurse to contents of nested subgraphs.
*/
function revokeSubgraphPreviews(subgraphNode: SubgraphNode) {
const graphId = subgraphNode.graph.isRootGraph
? ''
: subgraphNode.graph.id + ':'
revokePreviewsByLocatorId(graphId + subgraphNode.id)
for (const node of subgraphNode.subgraph.nodes) {
revokePreviewsByLocatorId(subgraphNode.subgraph.id + node.id)
}
}
return {
getNodeOutputs,
getNodeImageUrls,
@@ -279,6 +293,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
setNodePreviewsByNodeId,
revokePreviewsByExecutionId,
revokeAllPreviews,
revokeSubgraphPreviews,
getPreviewParam
}
})

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { Ref, computed, ref, toRaw } from 'vue'

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { computed } from 'vue'

View File

@@ -1,5 +1,5 @@
import axios from 'axios'
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { computed, ref, toRaw } from 'vue'

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { ref } from 'vue'

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'

View File

@@ -1,4 +1,4 @@
import { groupBy } from 'lodash'
import { groupBy } from 'es-toolkit/compat'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef } from 'vue'

View File

@@ -1,4 +1,4 @@
import { memoize } from 'lodash'
import { memoize } from 'es-toolkit/compat'
type RGB = { r: number; g: number; b: number }
type HSL = { h: number; s: number; l: number }

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import { ColorOption, LGraph, Reroute } from '@/lib/litegraph/src/litegraph'
import {

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import type {
ComfyLinkObject,

View File

@@ -1,4 +1,4 @@
import _ from 'lodash'
import _ from 'es-toolkit/compat'
import type {
ComboInputSpec,

View File

@@ -189,6 +189,10 @@ const onStatus = async (e: CustomEvent<StatusWsMessageStatus>) => {
await queueStore.update()
}
const onExecutionSuccess = async () => {
await queueStore.update()
}
const reconnectingMessage: ToastMessageOptions = {
severity: 'error',
summary: t('g.reconnecting')
@@ -214,6 +218,7 @@ const onReconnected = () => {
onMounted(() => {
api.addEventListener('status', onStatus)
api.addEventListener('execution_success', onExecutionSuccess)
api.addEventListener('reconnecting', onReconnecting)
api.addEventListener('reconnected', onReconnected)
executionStore.bindExecutionEvents()
@@ -227,6 +232,7 @@ onMounted(() => {
onBeforeUnmount(() => {
api.removeEventListener('status', onStatus)
api.removeEventListener('execution_success', onExecutionSuccess)
api.removeEventListener('reconnecting', onReconnecting)
api.removeEventListener('reconnected', onReconnected)
executionStore.unbindExecutionEvents()

View File

@@ -15,7 +15,7 @@ interface ColorTestCase {
type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla'
vi.mock('lodash', () => ({
vi.mock('es-toolkit/compat', () => ({
memoize: (fn: any) => fn
}))

View File

@@ -64,6 +64,16 @@ describe('useNodePricing', () => {
})
describe('dynamic pricing - KlingTextToVideoNode', () => {
it('should return high price for kling-v2-1-master model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
{ name: 'mode', value: 'standard / 5s / v2-1-master' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.40/Run')
})
it('should return high price for kling-v2-master model', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('KlingTextToVideoNode', [
@@ -219,6 +229,49 @@ describe('useNodePricing', () => {
})
})
describe('dynamic pricing - MinimaxHailuoVideoNode', () => {
it('should return $0.28 for 6s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [
{ name: 'duration', value: '6' },
{ name: 'resolution', value: '768P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28/Run')
})
it('should return $0.60 for 10s duration and 768P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '768P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.56/Run')
})
it('should return $0.49 for 6s duration and 1080P resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [
{ name: 'duration', value: '6' },
{ name: 'resolution', value: '1080P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.49/Run')
})
it('should return range when duration widget is missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('MinimaxHailuoVideoNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.28-0.56/Run (varies with resolution & duration)')
})
})
describe('dynamic pricing - OpenAIDalle2', () => {
it('should return $0.02 for 1024x1024 size', () => {
const { getNodeDisplayPrice } = useNodePricing()
@@ -1384,11 +1437,19 @@ describe('useNodePricing', () => {
const testCases = [
{
model: 'gemini-2.5-pro-preview-05-06',
expected: '$0.00016/$0.0006 per 1K tokens'
expected: '$0.00125/$0.01 per 1K tokens'
},
{
model: 'gemini-2.5-pro',
expected: '$0.00125/$0.01 per 1K tokens'
},
{
model: 'gemini-2.5-flash-preview-04-17',
expected: '$0.00125/$0.01 per 1K tokens'
expected: '$0.0003/$0.0025 per 1K tokens'
},
{
model: 'gemini-2.5-flash',
expected: '$0.0003/$0.0025 per 1K tokens'
},
{ model: 'unknown-gemini-model', expected: 'Token-based' }
]

View File

@@ -8,7 +8,7 @@ This guide covers patterns and examples for unit testing utilities, composables,
2. [Working with LiteGraph and Nodes](#working-with-litegraph-and-nodes)
3. [Working with Workflow JSON Files](#working-with-workflow-json-files)
4. [Mocking the API Object](#mocking-the-api-object)
5. [Mocking Lodash Functions](#mocking-lodash-functions)
5. [Mocking Utility Functions](#mocking-utility-functions)
6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle)
7. [Mocking Node Definitions](#mocking-node-definitions)
@@ -147,17 +147,17 @@ it('should subscribe to logs API', () => {
## Mocking Lodash Functions
Mocking lodash functions like debounce:
Mocking utility functions like debounce:
```typescript
// Mock debounce to execute immediately
import { debounce } from 'lodash-es'
import { debounce } from 'es-toolkit/compat'
vi.mock('lodash-es', () => ({
vi.mock('es-toolkit/compat', () => ({
debounce: vi.fn((fn) => {
// Return function that calls the input function immediately
const mockDebounced = (...args: any[]) => fn(...args)
// Add cancel method that lodash debounced functions have
// Add cancel method that debounced functions have
mockDebounced.cancel = vi.fn()
return mockDebounced
})