mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
29 Commits
report-iss
...
task-runne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
628b44051b | ||
|
|
a7a5e3cf67 | ||
|
|
64e218a9f3 | ||
|
|
0b69d3cbfe | ||
|
|
8257e848c6 | ||
|
|
a07b7693b6 | ||
|
|
26ddf69451 | ||
|
|
ed6ece2099 | ||
|
|
b42516d39c | ||
|
|
ef24efe5a3 | ||
|
|
34c267c755 | ||
|
|
8b9f0ddd1d | ||
|
|
af658b7792 | ||
|
|
9c53bbd53d | ||
|
|
f9be20fa78 | ||
|
|
87fc7a2c5d | ||
|
|
1f266e826e | ||
|
|
911adfe9f8 | ||
|
|
654d72b4cc | ||
|
|
a1ed67fc74 | ||
|
|
78bc635518 | ||
|
|
f49ec175e9 | ||
|
|
e4c60e7e18 | ||
|
|
37cb2cb0a5 | ||
|
|
141825e988 | ||
|
|
78f43b1e06 | ||
|
|
a6105eb8c7 | ||
|
|
79ed598d5d | ||
|
|
816574e0ab |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,6 +16,8 @@ dist-ssr
|
||||
.vscode/*
|
||||
*.code-workspace
|
||||
!.vscode/extensions.json
|
||||
!.vscode/tailwind.json
|
||||
!.vscode/settings.json.default
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
|
||||
5
.vscode/settings.json.default
vendored
Normal file
5
.vscode/settings.json.default
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"css.customData": [
|
||||
".vscode/tailwind.json"
|
||||
]
|
||||
}
|
||||
55
.vscode/tailwind.json
vendored
Normal file
55
.vscode/tailwind.json
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": 1.1,
|
||||
"atDirectives": [
|
||||
{
|
||||
"name": "@tailwind",
|
||||
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@apply",
|
||||
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@responsive",
|
||||
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@screen",
|
||||
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@variants",
|
||||
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
|
||||
"references": [
|
||||
{
|
||||
"name": "Tailwind Documentation",
|
||||
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -165,3 +165,37 @@ test.describe('Settings', () => {
|
||||
expect(request.postData()).toContain(JSON.stringify(expectedSetting))
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Feedback dialog', () => {
|
||||
test('Should open from topmenu help command', async ({ comfyPage }) => {
|
||||
// Open feedback dialog from top menu
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
// Verify feedback dialog content is visible
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
await expect(feedbackHeader).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should close when close button clicked', async ({ comfyPage }) => {
|
||||
// Open feedback dialog
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Feedback'])
|
||||
|
||||
const feedbackHeader = comfyPage.page.getByRole('heading', {
|
||||
name: 'Feedback'
|
||||
})
|
||||
|
||||
// Close feedback dialog
|
||||
await comfyPage.page
|
||||
.getByLabel('', { exact: true })
|
||||
.getByLabel('Close')
|
||||
.click()
|
||||
await feedbackHeader.waitFor({ state: 'hidden' })
|
||||
|
||||
// Verify dialog is closed
|
||||
await expect(feedbackHeader).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,10 +82,14 @@ test.describe('Node search box', () => {
|
||||
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
|
||||
const node = 'Load Checkpoint'
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode(node)
|
||||
const firstResult = comfyPage.page
|
||||
.locator('li.p-autocomplete-option')
|
||||
.first()
|
||||
await comfyPage.searchBox.input.waitFor({ state: 'visible' })
|
||||
await comfyPage.searchBox.input.fill(node)
|
||||
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
|
||||
// Wait for some time for the auto complete list to update.
|
||||
// The auto complete list is debounced and may take some time to update.
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
const firstResult = comfyPage.searchBox.dropdown.locator('li').first()
|
||||
await expect(firstResult).toHaveAttribute('aria-label', node)
|
||||
})
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
return [
|
||||
`prettier --write ${fileNames.join(' ')} --plugin @trivago/prettier-plugin-sort-imports`,
|
||||
`prettier --write ${fileNames.join(' ')}`,
|
||||
`eslint --fix ${fileNames.join(' ')}`
|
||||
]
|
||||
}
|
||||
|
||||
336
package-lock.json
generated
336
package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.7.14",
|
||||
"version": "1.8.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.7.14",
|
||||
"version": "1.8.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.10",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.11",
|
||||
"@comfyorg/litegraph": "^0.8.61",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
@@ -32,10 +33,10 @@
|
||||
"loglevel": "^1.9.2",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.5",
|
||||
"primevue": "^4.2.5",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"vue": "^3.4.31",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8",
|
||||
@@ -1936,14 +1937,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||
"version": "0.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.10.tgz",
|
||||
"integrity": "sha512-UWBgyuWeV7vussYZVUYhCe0jj+XbIq2nglrCUy6IgFgXp9pbE8Ktg5D36WxE0RWj6SvVXErlCL9wWnMktaRbCA=="
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.4.11.tgz",
|
||||
"integrity": "sha512-RGJeWwXjyv0Ojj7xkZKgcRxC1nFv1nh7qEWpNBiofxVgFiap9Ei79b/KJYxNE0no4BoYqRMaRg+sFtCE6yEukA==",
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.8.60",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.60.tgz",
|
||||
"integrity": "sha512-LkZalBcka1xVxkL7JnkF/1EzyvspLyrSthzyN9ZumWJw7kAaZkO9omraXv2t/UiFsqwMr5M/AV5UY915Vq8cxQ==",
|
||||
"version": "0.8.61",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.8.61.tgz",
|
||||
"integrity": "sha512-7DroJ0PLgI9TFvQR//6rf0NRXRvV60hapxVX5lmKzNn4Mn2Ni/JsB2ypNLKeSU5sacNyu8QT3W5Jdpafl7lcnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -3960,63 +3962,129 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/forms": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/forms/-/forms-0.0.2.tgz",
|
||||
"integrity": "sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/forms/node_modules/@primeuix/utils": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
|
||||
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/styled": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz",
|
||||
"integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==",
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
|
||||
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.0.5"
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primeuix/utils": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz",
|
||||
"integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==",
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
|
||||
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/core": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.5.tgz",
|
||||
"integrity": "sha512-DUCslDA93eUOVW0A1I3yoZgRLI4zmI2++loZQXbUF5jaXCwKiAza14+iyUU+cWH27VSq+jQnCEP9QJtPZiJJ0w==",
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
|
||||
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.0.5",
|
||||
"@primeuix/utils": "^0.0.5"
|
||||
"@primeuix/styled": "^0.3.2",
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
"vue": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.2.5.tgz",
|
||||
"integrity": "sha512-5jarJQ9Qv32bOo/0tY5bqR3JZI6+YmmoUQ2mjhVSbVElQsE4FNfhT7a7JwF+xgBPMPc8KWGNA1QB248HhPNVAg==",
|
||||
"dependencies": {
|
||||
"@primeuix/forms": "^0.0.2",
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "4.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms/node_modules/@primeuix/styled": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.3.2.tgz",
|
||||
"integrity": "sha512-ColZes0+/WKqH4ob2x8DyNYf1NENpe5ZguOvx5yCLxaP8EIMVhLjWLO/3umJiDnQU4XXMLkn2mMHHw+fhTX/mw==",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms/node_modules/@primeuix/utils": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.3.2.tgz",
|
||||
"integrity": "sha512-B+nphqTQeq+i6JuICLdVWnDMjONome2sNz0xI65qIOyeB4EF12CoKRiCsxuZ5uKAkHi/0d1LqlQ9mIWRSdkavw==",
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/forms/node_modules/@primevue/core": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.2.5.tgz",
|
||||
"integrity": "sha512-+oWBIQs5dLd2Ini4KEVOlvPILk989EHAskiFS3R/dz3jeOllJDMZFcSp8V9ddV0R3yDaPdLVkfHm2Q5t42kU2Q==",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.3.2",
|
||||
"@primeuix/utils": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/icons": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.5.tgz",
|
||||
"integrity": "sha512-ZxR9W1wlAE2fTtUhrHyeMx5t0jNyAgxDcHPm0cNXpX8q1XF95rSM/qb48QKXIBDBrJ/xs57BcyCNADP/VDPY4g==",
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.2.5.tgz",
|
||||
"integrity": "sha512-WFbUMZhQkXf/KmwcytkjGVeJ9aGEDXjP3uweOjQZMmRdEIxFnqYYpd90wE90JE1teZn3+TVnT4ZT7ejGyEXnFQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/utils": "^0.0.5",
|
||||
"@primevue/core": "4.0.5"
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "4.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@primevue/themes": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.5.tgz",
|
||||
"integrity": "sha512-cRrAhOapOT8eFCTDwNdB/acg2ZEEkn7y6h6p188PYSjJsWnYK+D8eI1Js1ZB5HwWo4sWs3oR3Sy8bPwejnGbAw==",
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.2.5.tgz",
|
||||
"integrity": "sha512-8F7yA36xYIKtNuAuyBdZZEks/bKDwlhH5WjpqGGB0FdwfAEoBYsynQ5sdqcT2Lb/NsajHmS5lc++Ttlvr1g1Lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.0.5"
|
||||
"@primeuix/styled": "^0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
@@ -5725,49 +5793,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz",
|
||||
"integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@vue/shared": "3.4.31",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz",
|
||||
"integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz",
|
||||
"integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
||||
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@vue/compiler-core": "3.4.31",
|
||||
"@vue/compiler-dom": "3.4.31",
|
||||
"@vue/compiler-ssr": "3.4.31",
|
||||
"@vue/shared": "3.4.31",
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.10",
|
||||
"postcss": "^8.4.38",
|
||||
"magic-string": "^0.30.11",
|
||||
"postcss": "^8.4.48",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz",
|
||||
"integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
||||
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
@@ -5811,38 +5883,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/shared": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@@ -5870,49 +5910,54 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
|
||||
"integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.4.31"
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz",
|
||||
"integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz",
|
||||
"integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.4.31",
|
||||
"@vue/runtime-core": "3.4.31",
|
||||
"@vue/shared": "3.4.31",
|
||||
"@vue/reactivity": "3.5.13",
|
||||
"@vue/runtime-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz",
|
||||
"integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
"@vue/compiler-ssr": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.4.31"
|
||||
"vue": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz",
|
||||
"integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA=="
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/test-utils": {
|
||||
"version": "2.4.6",
|
||||
@@ -7846,7 +7891,8 @@
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
@@ -14426,15 +14472,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -15169,9 +15216,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -15335,9 +15383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -15352,9 +15400,10 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.0",
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -15527,15 +15576,15 @@
|
||||
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
||||
},
|
||||
"node_modules/primevue": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.5.tgz",
|
||||
"integrity": "sha512-MALszGIZ5SnEQy1XeZLBFhpMXQ1OS7D1U7H+l/JAX5U46RQ1vufo7NAiWbbV5/ADjPGw4uLplqMQxujkksNY2g==",
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.2.5.tgz",
|
||||
"integrity": "sha512-7UMOIJvdFz4jQyhC76yhNdSlHtXvVpmE2JSo2ndUTBWjWJOkYyT562rQ4ayO+bMdJLtzBGqgY64I9ZfEvNd7vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@primeuix/styled": "^0.0.5",
|
||||
"@primeuix/utils": "^0.0.5",
|
||||
"@primevue/core": "4.0.5",
|
||||
"@primevue/icons": "4.0.5"
|
||||
"@primeuix/styled": "^0.3.2",
|
||||
"@primeuix/utils": "^0.3.2",
|
||||
"@primevue/core": "4.2.5",
|
||||
"@primevue/icons": "4.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.11.0"
|
||||
@@ -18815,15 +18864,16 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.4.31",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz",
|
||||
"integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==",
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.4.31",
|
||||
"@vue/compiler-sfc": "3.4.31",
|
||||
"@vue/runtime-dom": "3.4.31",
|
||||
"@vue/server-renderer": "3.4.31",
|
||||
"@vue/shared": "3.4.31"
|
||||
"@vue/compiler-dom": "3.5.13",
|
||||
"@vue/compiler-sfc": "3.5.13",
|
||||
"@vue/runtime-dom": "3.5.13",
|
||||
"@vue/server-renderer": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
||||
13
package.json
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.7.14",
|
||||
"version": "1.8.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -83,9 +83,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.10",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.11",
|
||||
"@comfyorg/litegraph": "^0.8.61",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
@@ -105,10 +106,10 @@
|
||||
"loglevel": "^1.9.2",
|
||||
"pinia": "^2.1.7",
|
||||
"primeicons": "^7.0.0",
|
||||
"primevue": "^4.0.5",
|
||||
"primevue": "^4.2.5",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"vue": "^3.4.31",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.3",
|
||||
"zod": "^3.23.8",
|
||||
|
||||
43
scripts/update-electron-types.js
Normal file
43
scripts/update-electron-types.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { execSync } from 'child_process'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const packageName = '@comfyorg/comfyui-electron-types'
|
||||
const description = 'desktop API types'
|
||||
|
||||
try {
|
||||
// Create a new branch
|
||||
console.log('Creating new branch...')
|
||||
const date = new Date()
|
||||
const isoDate = date.toISOString().split('T')[0]
|
||||
const timestamp = date.getTime()
|
||||
const branchName = `update-electron-types-${isoDate}-${timestamp}`
|
||||
execSync(`git checkout -b ${branchName} -t origin/main`, { stdio: 'inherit' })
|
||||
|
||||
// Update npm package to latest version
|
||||
console.log(`Updating ${description}...`)
|
||||
execSync(`npm install ${packageName}@latest`, {
|
||||
stdio: 'inherit'
|
||||
})
|
||||
|
||||
// Get the new version from package.json
|
||||
const packageLock = JSON.parse(readFileSync('./package-lock.json', 'utf8'))
|
||||
const newVersion = packageLock.packages[`node_modules/${packageName}`].version
|
||||
|
||||
// Stage changes
|
||||
const message = `[chore] Update electron-types to ${newVersion}`
|
||||
execSync('git add package.json package-lock.json', { stdio: 'inherit' })
|
||||
execSync(`git commit -m "${message}"`, { stdio: 'inherit' })
|
||||
|
||||
// Create the PR
|
||||
console.log('Creating PR...')
|
||||
execSync(
|
||||
`gh pr create --title "${message}" --label "dependencies" --body "Automated update of ${description} to version ${newVersion}."`,
|
||||
{ stdio: 'inherit' }
|
||||
)
|
||||
|
||||
console.log(
|
||||
`✅ Successfully created PR for ${description} update to ${newVersion}`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('❌ Error during update process:', error.message)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import config from '@/config'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import { isElectron, showNativeMenu } from './utils/envUtil'
|
||||
import { electronAPI, isElectron } from './utils/envUtil'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const isLoading = computed<boolean>(() => workspaceStore.spinner)
|
||||
@@ -34,7 +34,7 @@ const showContextMenu = (event: PointerEvent) => {
|
||||
case target instanceof HTMLTextAreaElement:
|
||||
case target instanceof HTMLInputElement && target.type === 'text':
|
||||
// TODO: Context input menu explicitly for text input
|
||||
showNativeMenu({ type: 'text' })
|
||||
electronAPI()?.showContextMenu({ type: 'text' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<Button
|
||||
:label="item.label"
|
||||
:label="String(item.label)"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<template>
|
||||
<div :class="['flex flex-wrap', $attrs.class]">
|
||||
<div
|
||||
v-for="checkbox in checkboxes"
|
||||
:key="checkbox.value"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-model="internalSelection"
|
||||
:inputId="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
/>
|
||||
<label :for="checkbox.value" class="ml-2">{{ checkbox.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface CheckboxItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
checkboxes: CheckboxItem[]
|
||||
modelValue: string[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string[]): void
|
||||
}>()
|
||||
|
||||
const internalSelection = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string[]) => emit('update:modelValue', value)
|
||||
})
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@
|
||||
:label="$t('g.download') + ' (' + fileSize + ')'"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="props.error"
|
||||
:disabled="!!props.error"
|
||||
@click="triggerDownload"
|
||||
v-if="status === null || status === 'error'"
|
||||
icon="pi pi-download"
|
||||
@@ -30,7 +30,7 @@
|
||||
v-if="status === 'in_progress' || status === 'paused'"
|
||||
>
|
||||
<!-- Temporary fix for issue when % only comes into view only if the progress bar is large enough
|
||||
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
|
||||
https://comfy-organization.slack.com/archives/C07H3GLKDPF/p1731551013385499
|
||||
-->
|
||||
<ProgressBar
|
||||
class="flex-1"
|
||||
@@ -42,7 +42,7 @@
|
||||
class="file-action-button"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="props.error"
|
||||
:disabled="!!props.error"
|
||||
@click="triggerPauseDownload"
|
||||
v-if="status === 'in_progress'"
|
||||
icon="pi pi-pause-circle"
|
||||
@@ -53,7 +53,7 @@
|
||||
class="file-action-button"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="props.error"
|
||||
:disabled="!!props.error"
|
||||
@click="triggerResumeDownload"
|
||||
v-if="status === 'paused'"
|
||||
icon="pi pi-play-circle"
|
||||
@@ -64,7 +64,7 @@
|
||||
class="file-action-button"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="props.error"
|
||||
:disabled="!!props.error"
|
||||
@click="triggerCancelDownload"
|
||||
icon="pi pi-times-circle"
|
||||
severity="danger"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
:label="$t('g.download') + ' (' + fileSize + ')'"
|
||||
size="small"
|
||||
outlined
|
||||
:disabled="props.error"
|
||||
:disabled="!!props.error"
|
||||
:title="props.url"
|
||||
@click="download.triggerBrowserDownload"
|
||||
/>
|
||||
|
||||
@@ -1,35 +1,17 @@
|
||||
<template>
|
||||
<div class="color-picker-wrapper flex items-center gap-2">
|
||||
<ColorPicker v-model="modelValue">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between p-2">
|
||||
<span>{{ props.label }}</span>
|
||||
<Button
|
||||
v-if="props.defaultValue"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
size="small"
|
||||
@click="resetColor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ColorPicker>
|
||||
<ColorPicker v-model="modelValue" />
|
||||
<InputText v-model="modelValue" class="w-28" :placeholder="label" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ColorPicker from 'primevue/colorpicker'
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
const modelValue = defineModel<string>('modelValue')
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
defaultValue?: string
|
||||
label?: string
|
||||
}>()
|
||||
|
||||
const resetColor = () => {
|
||||
modelValue.value = props.defaultValue || '#000000'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<!-- A simple read-only terminal component that displays logs. -->
|
||||
<template>
|
||||
<div class="p-terminal rounded-none h-full w-full">
|
||||
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
|
||||
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
|
||||
</ScrollPanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
fetchLogs: () => Promise<string>
|
||||
fetchInterval: number
|
||||
}>()
|
||||
|
||||
const log = ref<string>('')
|
||||
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
|
||||
/**
|
||||
* Whether the user has scrolled to the bottom of the terminal.
|
||||
* This is used to prevent the terminal from scrolling to the bottom
|
||||
* when new logs are fetched.
|
||||
*/
|
||||
const scrolledToBottom = ref(false)
|
||||
|
||||
let intervalId: number = 0
|
||||
|
||||
onMounted(async () => {
|
||||
const element = scrollPanelRef.value?.$el
|
||||
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
|
||||
|
||||
if (scrollContainer) {
|
||||
scrollContainer.addEventListener('scroll', () => {
|
||||
scrolledToBottom.value =
|
||||
scrollContainer.scrollTop + scrollContainer.clientHeight ===
|
||||
scrollContainer.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
watch(log, () => {
|
||||
if (scrolledToBottom.value) {
|
||||
scrollToBottom()
|
||||
}
|
||||
})
|
||||
|
||||
const fetchLogs = async () => {
|
||||
log.value = await props.fetchLogs()
|
||||
}
|
||||
|
||||
await fetchLogs()
|
||||
scrollToBottom()
|
||||
intervalId = window.setInterval(fetchLogs, props.fetchInterval)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(intervalId)
|
||||
})
|
||||
</script>
|
||||
53
src/components/common/RefreshButton.vue
Normal file
53
src/components/common/RefreshButton.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
A refresh button that disables and shows a progress spinner whilst active.
|
||||
|
||||
Usage:
|
||||
```vue
|
||||
<RefreshButton
|
||||
v-model="isRefreshing"
|
||||
:outlined="false"
|
||||
@refresh="refresh"
|
||||
/>
|
||||
```
|
||||
-->
|
||||
<template>
|
||||
<Button
|
||||
class="relative p-button-icon-only"
|
||||
:outlined="props.outlined"
|
||||
:severity="props.severity"
|
||||
:disabled="active || props.disabled"
|
||||
@click="(event) => $emit('refresh', event)"
|
||||
>
|
||||
<span
|
||||
class="p-button-icon pi pi-refresh transition-all"
|
||||
:class="{ 'opacity-0': active }"
|
||||
data-pc-section="icon"
|
||||
></span>
|
||||
<span class="p-button-label" data-pc-section="label"> </span>
|
||||
<ProgressSpinner v-show="active" class="absolute w-1/2 h-1/2" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
|
||||
import { VueSeverity } from '@/types/primeVueTypes'
|
||||
|
||||
// Properties
|
||||
interface Props {
|
||||
outlined?: boolean
|
||||
disabled?: boolean
|
||||
severity?: VueSeverity
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
outlined: true,
|
||||
severity: 'secondary'
|
||||
})
|
||||
|
||||
// Model
|
||||
const active = defineModel<boolean>({ required: true })
|
||||
|
||||
// Emits
|
||||
defineEmits(['refresh'])
|
||||
</script>
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div :class="props.class">
|
||||
<div>
|
||||
<IconField>
|
||||
<Button
|
||||
v-if="props.filterIcon"
|
||||
v-if="filterIcon"
|
||||
class="p-inputicon filter-button"
|
||||
:icon="props.filterIcon"
|
||||
:icon="filterIcon"
|
||||
text
|
||||
severity="contrast"
|
||||
@click="$emit('showFilter', $event)"
|
||||
@@ -12,12 +12,12 @@
|
||||
<InputText
|
||||
class="search-box-input w-full"
|
||||
@input="handleInput"
|
||||
:modelValue="props.modelValue"
|
||||
:placeholder="props.placeholder"
|
||||
:modelValue="modelValue"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
<InputIcon v-if="!props.modelValue" :class="props.icon" />
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<Button
|
||||
v-if="props.modelValue"
|
||||
v-if="modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
@@ -47,40 +47,36 @@ import Button from 'primevue/button'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { toRefs } from 'vue'
|
||||
|
||||
import type { SearchFilter } from './SearchFilterChip.vue'
|
||||
import SearchFilterChip from './SearchFilterChip.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: string
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
filterIcon?: string
|
||||
filters?: TFilter[]
|
||||
}>(),
|
||||
{
|
||||
placeholder: 'Search...',
|
||||
icon: 'pi pi-search',
|
||||
debounceTime: 300
|
||||
}
|
||||
)
|
||||
const {
|
||||
modelValue,
|
||||
placeholder = 'Search...',
|
||||
icon = 'pi pi-search',
|
||||
debounceTime = 300,
|
||||
filterIcon,
|
||||
filters = []
|
||||
} = defineProps<{
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
icon?: string
|
||||
debounceTime?: number
|
||||
filterIcon?: string
|
||||
filters?: TFilter[]
|
||||
}>()
|
||||
|
||||
const { filters } = toRefs(props)
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'search',
|
||||
'showFilter',
|
||||
'removeFilter'
|
||||
])
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void
|
||||
(e: 'search', value: string, filters: TFilter[]): void
|
||||
(e: 'showFilter', event: Event): void
|
||||
(e: 'removeFilter', filter: TFilter): void
|
||||
}>()
|
||||
|
||||
const emitSearch = debounce((value: string) => {
|
||||
emit('search', value, props.filters)
|
||||
}, props.debounceTime)
|
||||
emit('search', value, filters)
|
||||
}, debounceTime)
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
v-for="device in props.stats.devices"
|
||||
:key="device.index"
|
||||
:header="device.name"
|
||||
:value="device.index"
|
||||
>
|
||||
<DeviceInfo :device="device" />
|
||||
</TabPanel>
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
selectionMode="single"
|
||||
:pt="{
|
||||
nodeLabel: 'tree-explorer-node-label',
|
||||
nodeContent: ({ props }) => ({
|
||||
onClick: (e: MouseEvent) => onNodeContentClick(e, props.node),
|
||||
onContextmenu: (e: MouseEvent) => handleContextMenu(props.node, e)
|
||||
nodeContent: ({ context }) => ({
|
||||
onClick: (e: MouseEvent) =>
|
||||
onNodeContentClick(e, context.node as RenderedTreeExplorerNode),
|
||||
onContextmenu: (e: MouseEvent) =>
|
||||
handleContextMenu(e, context.node as RenderedTreeExplorerNode)
|
||||
}),
|
||||
nodeToggleButton: () => ({
|
||||
onClick: (e: MouseEvent) => {
|
||||
@@ -152,7 +154,7 @@ const menuItems = computed<MenuItem[]>(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const handleContextMenu = (node: RenderedTreeExplorerNode, e: MouseEvent) => {
|
||||
const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
menuTargetNode.value = node
|
||||
emit('contextMenu', node, e)
|
||||
if (menuItems.value.filter((item) => item.visible).length > 0) {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
</template>
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
:title="$t('issueReport.submitErrorReport')"
|
||||
error-type="graphExecutionError"
|
||||
:extra-fields="[stackTraceField]"
|
||||
:tags="{ exceptionMessage: props.error.exception_message }"
|
||||
@@ -89,10 +90,10 @@ const stackTraceField = computed<ReportField>(() => {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
data: {
|
||||
getData: () => ({
|
||||
nodeType: props.error.node_type,
|
||||
stackTrace: props.error.traceback?.join('\n')
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
31
src/components/dialog/content/IssueReportDialogContent.vue
Normal file
31
src/components/dialog/content/IssueReportDialogContent.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="p-2 h-full" aria-labelledby="issue-report-title">
|
||||
<Panel
|
||||
:pt="{
|
||||
root: 'border-none',
|
||||
content: 'p-0'
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<header class="flex flex-col items-center w-full">
|
||||
<h2 id="issue-report-title" class="text-4xl">{{ title }}</h2>
|
||||
<span v-if="subtitle" class="text-muted mt-0">{{ subtitle }}</span>
|
||||
</header>
|
||||
</template>
|
||||
<ReportIssuePanel v-bind="panelProps" :pt="{ root: 'border-none' }" />
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Panel from 'primevue/panel'
|
||||
|
||||
import ReportIssuePanel from '@/components/dialog/content/error/ReportIssuePanel.vue'
|
||||
import type { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
panelProps: IssueReportPanelProps
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,201 +1,251 @@
|
||||
<template>
|
||||
<Panel>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
|
||||
<Form
|
||||
v-slot="$form"
|
||||
@submit="submit"
|
||||
:resolver="zodResolver(issueReportSchema)"
|
||||
>
|
||||
<Panel :pt="$attrs.pt">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-4">
|
||||
<Button
|
||||
v-tooltip="!submitted ? $t('g.reportIssueTooltip') : undefined"
|
||||
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
|
||||
:severity="submitted ? 'secondary' : 'primary'"
|
||||
:icon="submitted ? 'pi pi-check' : 'pi pi-send'"
|
||||
:disabled="submitted"
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 mt-2 border border-round surface-border shadow-1">
|
||||
<div class="flex flex-row gap-3 mb-2">
|
||||
<div v-for="field in fields" :key="field.value">
|
||||
<FormField
|
||||
v-if="field.optIn"
|
||||
v-slot="$field"
|
||||
:name="field.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
:inputId="field.value"
|
||||
:value="field.value"
|
||||
v-model="selection"
|
||||
/>
|
||||
<label :for="field.value">{{ field.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
<FormField class="mb-4" v-slot="$field" name="details">
|
||||
<Textarea
|
||||
v-bind="$field"
|
||||
class="w-full"
|
||||
rows="5"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.maxLength') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
<FormField v-slot="$field" name="contactInfo">
|
||||
<InputText
|
||||
v-bind="$field"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
/>
|
||||
<Message
|
||||
v-if="$field?.error && $field.touched && $field.value !== ''"
|
||||
severity="error"
|
||||
size="small"
|
||||
variant="simple"
|
||||
>
|
||||
{{ t('issueReport.validation.invalidEmail') }}
|
||||
</Message>
|
||||
</FormField>
|
||||
|
||||
<div class="flex flex-row gap-3 mt-2">
|
||||
<div v-for="checkbox in contactCheckboxes" :key="checkbox.value">
|
||||
<FormField
|
||||
v-slot="$field"
|
||||
:name="checkbox.value"
|
||||
class="flex space-x-1"
|
||||
>
|
||||
<Checkbox
|
||||
v-bind="$field"
|
||||
:inputId="checkbox.value"
|
||||
:value="checkbox.value"
|
||||
v-model="contactPrefs"
|
||||
:disabled="
|
||||
$form.contactInfo?.error || !$form.contactInfo?.value
|
||||
"
|
||||
/>
|
||||
<label :for="checkbox.value">{{ checkbox.label }}</label>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
v-tooltip="$t('g.reportIssueTooltip')"
|
||||
:label="submitted ? $t('g.reportSent') : $t('g.reportIssue')"
|
||||
:severity="isButtonDisabled ? 'secondary' : 'primary'"
|
||||
:icon="icon"
|
||||
:disabled="isButtonDisabled"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 mt-4 border border-round surface-border shadow-1">
|
||||
<CheckboxGroup
|
||||
v-model="selection"
|
||||
class="gap-4 mb-4"
|
||||
:checkboxes="reportCheckboxes"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<InputText
|
||||
v-model="contactInfo"
|
||||
class="w-full"
|
||||
:placeholder="$t('issueReport.provideEmail')"
|
||||
:maxlength="CONTACT_MAX_LEN"
|
||||
/>
|
||||
<CheckboxGroup
|
||||
v-model="contactPrefs"
|
||||
class="gap-3 mt-2"
|
||||
:checkboxes="contactCheckboxes"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<Textarea
|
||||
v-model="details"
|
||||
class="w-full"
|
||||
rows="4"
|
||||
:maxlength="DETAILS_MAX_LEN"
|
||||
:placeholder="$t('issueReport.provideAdditionalDetails')"
|
||||
:aria-label="$t('issueReport.provideAdditionalDetails')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</Panel>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, FormField, type FormSubmitEvent } from '@primevue/forms'
|
||||
// @ts-expect-error https://github.com/primefaces/primevue/issues/6722
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod'
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Message from 'primevue/message'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { DefaultField, ReportField } from '@/types/issueReportTypes'
|
||||
import {
|
||||
type IssueReportFormData,
|
||||
type ReportField,
|
||||
issueReportSchema
|
||||
} from '@/types/issueReportTypes'
|
||||
import type {
|
||||
DefaultField,
|
||||
IssueReportPanelProps
|
||||
} from '@/types/issueReportTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
|
||||
const ISSUE_NAME = 'User reported issue'
|
||||
const DETAILS_MAX_LEN = 5_000
|
||||
const CONTACT_MAX_LEN = 320
|
||||
|
||||
const props = defineProps<{
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
tags?: Record<string, string>
|
||||
}>()
|
||||
const {
|
||||
defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'],
|
||||
tags = {}
|
||||
} = props
|
||||
const props = defineProps<IssueReportPanelProps>()
|
||||
const { defaultFields = ['Workflow', 'Logs', 'SystemStats', 'Settings'] } =
|
||||
props
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const selection = ref<string[]>([])
|
||||
const contactPrefs = ref<string[]>([])
|
||||
const contactInfo = ref('')
|
||||
const details = ref('')
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
|
||||
const followUp = computed(() => contactPrefs.value.includes('FollowUp'))
|
||||
const notifyResolve = computed(() => contactPrefs.value.includes('Resolution'))
|
||||
|
||||
const icon = computed(() => {
|
||||
if (submitting.value) return 'pi pi-spin pi-spinner'
|
||||
if (submitted.value) return 'pi pi-check'
|
||||
return 'pi pi-send'
|
||||
})
|
||||
const isFormEmpty = computed(() => !selection.value.length && !details.value)
|
||||
const isButtonDisabled = computed(
|
||||
() => submitted.value || submitting.value || isFormEmpty.value
|
||||
)
|
||||
|
||||
const contactCheckboxes = [
|
||||
{ label: t('issueReport.contactFollowUp'), value: 'FollowUp' },
|
||||
{ label: t('issueReport.notifyResolve'), value: 'Resolution' }
|
||||
{ label: t('issueReport.contactFollowUp'), value: 'followUp' },
|
||||
{ label: t('issueReport.notifyResolve'), value: 'notifyOnResolution' }
|
||||
]
|
||||
const defaultReportCheckboxes = [
|
||||
{ label: t('g.workflow'), value: 'Workflow' },
|
||||
{ label: t('g.logs'), value: 'Logs' },
|
||||
{ label: t('issueReport.systemStats'), value: 'SystemStats' },
|
||||
{ label: t('g.settings'), value: 'Settings' }
|
||||
|
||||
const defaultFieldsConfig: ReportField[] = [
|
||||
{
|
||||
label: t('issueReport.systemStats'),
|
||||
value: 'SystemStats',
|
||||
getData: () => api.getSystemStats(),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.workflow'),
|
||||
value: 'Workflow',
|
||||
getData: () => cloneDeep(app.graph.asSerialisable()),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.logs'),
|
||||
value: 'Logs',
|
||||
getData: () => api.getLogs(),
|
||||
optIn: true
|
||||
},
|
||||
{
|
||||
label: t('g.settings'),
|
||||
value: 'Settings',
|
||||
getData: () => api.getSettings(),
|
||||
optIn: true
|
||||
}
|
||||
]
|
||||
const reportCheckboxes = computed(() => [
|
||||
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
|
||||
...defaultReportCheckboxes.filter(({ value }) =>
|
||||
|
||||
const fields = computed(() => [
|
||||
...defaultFieldsConfig.filter(({ value }) =>
|
||||
defaultFields.includes(value as DefaultField)
|
||||
)
|
||||
),
|
||||
...(props.extraFields ?? [])
|
||||
])
|
||||
|
||||
const getUserInfo = (): User => ({ email: contactInfo.value })
|
||||
const createUser = (formData: IssueReportFormData): User => ({
|
||||
email: formData.contactInfo || undefined
|
||||
})
|
||||
|
||||
const getLogs = async () =>
|
||||
selection.value.includes('Logs') ? api.getLogs() : null
|
||||
const createExtraData = async (formData: IssueReportFormData) => {
|
||||
const result = {}
|
||||
const isChecked = (fieldValue: string) => formData[fieldValue]
|
||||
|
||||
const getSystemStats = async () =>
|
||||
selection.value.includes('SystemStats') ? api.getSystemStats() : null
|
||||
await Promise.all(
|
||||
fields.value
|
||||
.filter((field) => !field.optIn || isChecked(field.value))
|
||||
.map(async (field) => {
|
||||
try {
|
||||
result[field.value] = await field.getData()
|
||||
} catch (error) {
|
||||
console.error(`Failed to collect ${field.value}:`, error)
|
||||
result[field.value] = { error: String(error) }
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const getSettings = async () =>
|
||||
selection.value.includes('Settings') ? api.getSettings() : null
|
||||
|
||||
const getWorkflow = () =>
|
||||
selection.value.includes('Workflow')
|
||||
? cloneDeep(app.graph.asSerialisable())
|
||||
: null
|
||||
|
||||
const createDefaultFields = async () => {
|
||||
const [settings, systemStats, logs, workflow] = await Promise.all([
|
||||
getSettings(),
|
||||
getSystemStats(),
|
||||
getLogs(),
|
||||
getWorkflow()
|
||||
])
|
||||
return { settings, systemStats, logs, workflow }
|
||||
return result
|
||||
}
|
||||
|
||||
const createExtraFields = (): Record<string, unknown> | undefined => {
|
||||
if (!props.extraFields) return undefined
|
||||
|
||||
return props.extraFields
|
||||
.filter((field) => !field.optIn || selection.value.includes(field.value))
|
||||
.reduce((acc, field) => ({ ...acc, ...cloneDeep(field.data) }), {})
|
||||
}
|
||||
|
||||
const createFeedback = () => {
|
||||
const createCaptureContext = async (
|
||||
formData: IssueReportFormData
|
||||
): Promise<CaptureContext> => {
|
||||
return {
|
||||
details: details.value,
|
||||
contactPreferences: {
|
||||
followUp: followUp.value,
|
||||
notifyOnResolution: notifyResolve.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createCaptureContext = async (): Promise<CaptureContext> => {
|
||||
return {
|
||||
user: getUserInfo(),
|
||||
user: createUser(formData),
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType,
|
||||
...tags
|
||||
followUp: formData.contactInfo ? formData.followUp : false,
|
||||
notifyOnResolution: formData.contactInfo
|
||||
? formData.notifyOnResolution
|
||||
: false,
|
||||
isElectron: isElectron(),
|
||||
...props.tags
|
||||
},
|
||||
extra: {
|
||||
...createFeedback(),
|
||||
...(await createDefaultFields()),
|
||||
...createExtraFields()
|
||||
details: formData.details,
|
||||
...(await createExtraData(formData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reportIssue = async () => {
|
||||
if (isButtonDisabled.value) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
captureMessage(ISSUE_NAME, await createCaptureContext())
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
const submit = async (event: FormSubmitEvent) => {
|
||||
if (event.valid) {
|
||||
try {
|
||||
const captureContext = await createCaptureContext(event.values)
|
||||
captureMessage(ISSUE_NAME, captureContext)
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('g.error'),
|
||||
detail: error.message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
// @ts-strict-ignore
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { Form } from '@primevue/forms'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Panel from 'primevue/panel'
|
||||
import Textarea from 'primevue/textarea'
|
||||
import Tooltip from 'primevue/tooltip'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { createApp } from 'vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import CheckboxGroup from '@/components/common/CheckboxGroup.vue'
|
||||
import enMesages from '@/locales/en/main.json'
|
||||
import { DefaultField, ReportField } from '@/types/issueReportTypes'
|
||||
import { IssueReportPanelProps } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from '../ReportIssuePanel.vue'
|
||||
|
||||
type ReportIssuePanelProps = {
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
const DEFAULT_FIELDS = ['Workflow', 'Logs', 'Settings', 'SystemStats']
|
||||
const CUSTOM_FIELDS = [
|
||||
{
|
||||
label: 'Custom Field',
|
||||
value: 'CustomField',
|
||||
optIn: true,
|
||||
getData: () => 'mock data'
|
||||
}
|
||||
]
|
||||
|
||||
async function getSubmittedContext() {
|
||||
const { captureMessage } = (await import('@sentry/core')) as any
|
||||
return captureMessage.mock.calls[0][1]
|
||||
}
|
||||
|
||||
async function submitForm(wrapper: any) {
|
||||
await wrapper.findComponent(Form).trigger('submit')
|
||||
return getSubmittedContext()
|
||||
}
|
||||
|
||||
async function findAndUpdateCheckbox(
|
||||
wrapper: any,
|
||||
value: string,
|
||||
checked = true
|
||||
) {
|
||||
const checkbox = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.find((c: any) => c.props('value') === value)
|
||||
if (!checkbox) throw new Error(`Checkbox with value "${value}" not found`)
|
||||
|
||||
await checkbox.vm.$emit('update:modelValue', checked)
|
||||
return checkbox
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
@@ -57,18 +82,64 @@ vi.mock('@sentry/core', () => ({
|
||||
captureMessage: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@primevue/forms', () => ({
|
||||
Form: {
|
||||
name: 'Form',
|
||||
template:
|
||||
'<form @submit.prevent="onSubmit"><slot :values="formValues" /></form>',
|
||||
props: ['resolver'],
|
||||
data() {
|
||||
return {
|
||||
formValues: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
valid: true,
|
||||
values: this.formValues
|
||||
})
|
||||
},
|
||||
updateFieldValue(name: string, value: any) {
|
||||
this.formValues[name] = value
|
||||
}
|
||||
}
|
||||
},
|
||||
FormField: {
|
||||
name: 'FormField',
|
||||
template:
|
||||
'<div><slot :modelValue="modelValue" @update:modelValue="updateValue" /></div>',
|
||||
props: ['name'],
|
||||
data() {
|
||||
return {
|
||||
modelValue: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.modelValue = value
|
||||
let parent = this.$parent
|
||||
while (parent && parent.$options.name !== 'Form') {
|
||||
parent = parent.$parent
|
||||
}
|
||||
if (parent) {
|
||||
parent.updateFieldValue(this.name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeAll(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
|
||||
const mountComponent = (props: IssueReportPanelProps, options = {}): any => {
|
||||
return mount(ReportIssuePanel, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createTestingPinia(), i18n],
|
||||
directives: { tooltip: Tooltip },
|
||||
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
|
||||
plugins: [PrimeVue, i18n],
|
||||
directives: { tooltip: Tooltip }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
@@ -78,44 +149,66 @@ describe('ReportIssuePanel', () => {
|
||||
it('renders the panel with all required components', () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
expect(wrapper.find('.p-panel').exists()).toBe(true)
|
||||
expect(wrapper.findAllComponents(CheckboxGroup).length).toBe(2)
|
||||
expect(wrapper.findAllComponents(Checkbox).length).toBe(6)
|
||||
expect(wrapper.findComponent(InputText).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Textarea).exists()).toBe(true)
|
||||
expect(wrapper.findComponent(Button).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates selection when checkboxes are selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.setValue(['Workflow', 'Logs'])
|
||||
expect(wrapper.vm.selection).toEqual(['Workflow', 'Logs'])
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error'
|
||||
})
|
||||
|
||||
const checkboxes = wrapper.findAllComponents(Checkbox)
|
||||
|
||||
for (const field of DEFAULT_FIELDS) {
|
||||
const checkbox = checkboxes.find(
|
||||
(checkbox) => checkbox.props('value') === field
|
||||
)
|
||||
expect(checkbox).toBeDefined()
|
||||
|
||||
await checkbox?.vm.$emit('update:modelValue', [field])
|
||||
expect(wrapper.vm.selection).toContain(field)
|
||||
}
|
||||
})
|
||||
|
||||
it('updates contactInfo when input is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const input = wrapper.findComponent(InputText)
|
||||
await input.setValue('test@example.com')
|
||||
expect(wrapper.vm.contactInfo).toBe('test@example.com')
|
||||
|
||||
await input.vm.$emit('update:modelValue', 'test@example.com')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.user.email).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('updates additional details when textarea is changed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
await textarea.setValue('This is a test detail.')
|
||||
expect(wrapper.vm.details).toBe('This is a test detail.')
|
||||
|
||||
await textarea.vm.$emit('update:modelValue', 'This is a test detail.')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra.details).toBe('This is a test detail.')
|
||||
})
|
||||
|
||||
it('updates contactPrefs when preferences are selected', async () => {
|
||||
it('set contact preferences back to false if email is removed', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
|
||||
await preferences?.setValue(['FollowUp'])
|
||||
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
|
||||
})
|
||||
const input = wrapper.findComponent(InputText)
|
||||
|
||||
it('does not allow submission if the form is empty', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(wrapper.vm.submitted).toBe(false)
|
||||
// Set a valid email, enabling the contact preferences to be changed
|
||||
await input.vm.$emit('update:modelValue', 'name@example.com')
|
||||
|
||||
// Enable both contact preferences
|
||||
for (const pref of ['followUp', 'notifyOnResolution']) {
|
||||
await findAndUpdateCheckbox(wrapper, pref)
|
||||
}
|
||||
|
||||
// Change the email back to empty
|
||||
await input.vm.$emit('update:modelValue', '')
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
// Check that the contact preferences are back to false automatically
|
||||
expect(context.tags.followUp).toBe(false)
|
||||
expect(context.tags.notifyOnResolution).toBe(false)
|
||||
})
|
||||
|
||||
it('renders with overridden default fields', () => {
|
||||
@@ -123,83 +216,87 @@ describe('ReportIssuePanel', () => {
|
||||
errorType: 'Test Error',
|
||||
defaultFields: ['Settings']
|
||||
})
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toEqual([
|
||||
{ label: 'Settings', value: 'Settings' }
|
||||
])
|
||||
|
||||
// Filter out the contact preferences checkboxes
|
||||
const fieldCheckboxes = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.filter(
|
||||
(checkbox) =>
|
||||
!['followUp', 'notifyOnResolution'].includes(checkbox.props('value'))
|
||||
)
|
||||
expect(fieldCheckboxes.length).toBe(1)
|
||||
expect(fieldCheckboxes.at(0)?.props('value')).toBe('Settings')
|
||||
})
|
||||
|
||||
it('renders additional fields when extraFields prop is provided', () => {
|
||||
const extraFields = [
|
||||
{ label: 'Custom Field', value: 'CustomField', optIn: true, data: {} }
|
||||
]
|
||||
const wrapper = mountComponent({ errorType: 'Test Error', extraFields })
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toContainEqual({
|
||||
label: 'Custom Field',
|
||||
value: 'CustomField'
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
extraFields: CUSTOM_FIELDS
|
||||
})
|
||||
const customCheckbox = wrapper
|
||||
.findAllComponents(Checkbox)
|
||||
.find((checkbox) => checkbox.props('value') === 'CustomField')
|
||||
expect(customCheckbox).toBeDefined()
|
||||
})
|
||||
|
||||
it('allows custom fields to be selected', async () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
extraFields: CUSTOM_FIELDS
|
||||
})
|
||||
|
||||
await findAndUpdateCheckbox(wrapper, 'CustomField')
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra.CustomField).toBe('mock data')
|
||||
})
|
||||
|
||||
it('does not submit unchecked fields', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const textarea = wrapper.findComponent(Textarea)
|
||||
|
||||
await textarea.setValue('Report with only text but no fields selected')
|
||||
await wrapper.vm.reportIssue()
|
||||
// Set details but don't check any field checkboxes
|
||||
await textarea.vm.$emit(
|
||||
'update:modelValue',
|
||||
'Report with only text but no fields selected'
|
||||
)
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
const { captureMessage } = (await import('@sentry/core')) as any
|
||||
const captureContext = captureMessage.mock.calls[0][1]
|
||||
|
||||
expect(captureContext.extra.logs).toBeNull()
|
||||
expect(captureContext.extra.systemStats).toBeNull()
|
||||
expect(captureContext.extra.settings).toBeNull()
|
||||
expect(captureContext.extra.workflow).toBeNull()
|
||||
// Verify none of the optional fields were included
|
||||
for (const field of DEFAULT_FIELDS) {
|
||||
expect(context.extra[field]).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
checkbox: 'Logs',
|
||||
apiMethod: 'getLogs',
|
||||
expectedKey: 'logs',
|
||||
expectedKey: 'Logs',
|
||||
mockValue: 'mock logs'
|
||||
},
|
||||
{
|
||||
checkbox: 'SystemStats',
|
||||
apiMethod: 'getSystemStats',
|
||||
expectedKey: 'systemStats',
|
||||
expectedKey: 'SystemStats',
|
||||
mockValue: 'mock stats'
|
||||
},
|
||||
{
|
||||
checkbox: 'Settings',
|
||||
apiMethod: 'getSettings',
|
||||
expectedKey: 'settings',
|
||||
expectedKey: 'Settings',
|
||||
mockValue: 'mock settings'
|
||||
}
|
||||
])(
|
||||
'submits (%s) when the (%s) checkbox is selected',
|
||||
'submits $checkbox data when checkbox is selected',
|
||||
async ({ checkbox, apiMethod, expectedKey, mockValue }) => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { api } = (await import('@/scripts/api')) as any
|
||||
vi.spyOn(api, apiMethod).mockResolvedValue(mockValue)
|
||||
|
||||
const { captureMessage } = await import('@sentry/core')
|
||||
|
||||
// Select the checkbox
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.vm.$emit('update:modelValue', [checkbox])
|
||||
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(api[apiMethod]).toHaveBeenCalled()
|
||||
|
||||
// Verify the message includes the associated data
|
||||
expect(captureMessage).toHaveBeenCalledWith(
|
||||
'User reported issue',
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({ [expectedKey]: mockValue })
|
||||
})
|
||||
)
|
||||
await findAndUpdateCheckbox(wrapper, checkbox)
|
||||
const context = await submitForm(wrapper)
|
||||
expect(context.extra[expectedKey]).toBe(mockValue)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -207,24 +304,12 @@ describe('ReportIssuePanel', () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
|
||||
const { app } = (await import('@/scripts/app')) as any
|
||||
const { captureMessage } = await import('@sentry/core')
|
||||
|
||||
const mockWorkflow = { nodes: [], edges: [] }
|
||||
vi.spyOn(app.graph, 'asSerialisable').mockReturnValue(mockWorkflow)
|
||||
|
||||
// Select the "Workflow" checkbox
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
await checkboxes?.vm.$emit('update:modelValue', ['Workflow'])
|
||||
await findAndUpdateCheckbox(wrapper, 'Workflow')
|
||||
const context = await submitForm(wrapper)
|
||||
|
||||
await wrapper.vm.reportIssue()
|
||||
expect(app.graph.asSerialisable).toHaveBeenCalled()
|
||||
|
||||
// Verify the message includes the workflow
|
||||
expect(captureMessage).toHaveBeenCalledWith(
|
||||
'User reported issue',
|
||||
expect.objectContaining({
|
||||
extra: expect.objectContaining({ workflow: mockWorkflow })
|
||||
})
|
||||
)
|
||||
expect(context.extra.Workflow).toEqual(mockWorkflow)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
optionValue="id"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-upload"
|
||||
icon="pi pi-file-export"
|
||||
text
|
||||
:title="$t('g.export')"
|
||||
@click="colorPaletteService.exportColorPalette(activePaletteId)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-download"
|
||||
icon="pi pi-file-import"
|
||||
text
|
||||
:title="$t('g.import')"
|
||||
@click="importCustomPalette"
|
||||
|
||||
@@ -130,8 +130,8 @@ import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const autoUpdate = defineModel('autoUpdate', { required: true })
|
||||
const allowMetrics = defineModel('allowMetrics', { required: true })
|
||||
const autoUpdate = defineModel<boolean>('autoUpdate', { required: true })
|
||||
const allowMetrics = defineModel<boolean>('allowMetrics', { required: true })
|
||||
|
||||
const showMetricsInfo = () => {
|
||||
showDialog.value = true
|
||||
|
||||
40
src/components/maintenance/StatusTag.vue
Normal file
40
src/components/maintenance/StatusTag.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<Tag :icon :severity :value />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons, type PrimeIconsOptions } from '@primevue/core/api'
|
||||
import Tag, { TagProps } from 'primevue/tag'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
error: boolean
|
||||
refreshing?: boolean
|
||||
}>()
|
||||
|
||||
// Bindings
|
||||
const icon = ref<string>(null)
|
||||
const severity = ref<TagProps['severity']>(null)
|
||||
const value = ref<PrimeIconsOptions[keyof PrimeIconsOptions]>(null)
|
||||
|
||||
const updateBindings = () => {
|
||||
if (props.refreshing) {
|
||||
icon.value = PrimeIcons.QUESTION
|
||||
severity.value = 'info'
|
||||
value.value = t('maintenance.refreshing')
|
||||
} else if (props.error) {
|
||||
icon.value = PrimeIcons.TIMES
|
||||
severity.value = 'danger'
|
||||
value.value = t('g.error')
|
||||
} else {
|
||||
icon.value = PrimeIcons.CHECK
|
||||
severity.value = 'success'
|
||||
value.value = t('maintenance.OK')
|
||||
}
|
||||
}
|
||||
|
||||
watch(props, updateBindings, { deep: true })
|
||||
</script>
|
||||
127
src/components/maintenance/TaskCard.vue
Normal file
127
src/components/maintenance/TaskCard.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div max-w-48 min-h-52 grid relative"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
>
|
||||
<Card
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="object-contain w-full h-full opacity-25 pt-4 px-4"
|
||||
/>
|
||||
</template>
|
||||
<template #title>{{ task.name }}</template>
|
||||
<template #content>{{ description }}</template>
|
||||
<template #footer>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
class="w-full"
|
||||
raised
|
||||
icon-pos="right"
|
||||
@click="(event) => $emit('execute', event)"
|
||||
:loading="isExecuting"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
<i
|
||||
v-if="!isLoading && runner.state === 'OK'"
|
||||
class="task-card-ok pi pi-check"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Card from 'primevue/card'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const runner = computed(() => taskStore.getRunner(props.task))
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
task: MaintenanceTask
|
||||
}>()
|
||||
|
||||
// Events
|
||||
defineEmits<{
|
||||
execute: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
runner.value.state === 'error'
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveLoading = computed(() => runner.value.refreshing)
|
||||
const reactiveExecuting = computed(() => runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-card-ok {
|
||||
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
|
||||
|
||||
font-size: 4rem;
|
||||
text-shadow: 0.25rem 0 0.5rem black;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.p-card {
|
||||
@apply transition-opacity;
|
||||
|
||||
--p-card-background: var(--p-button-secondary-background);
|
||||
opacity: 0.9;
|
||||
|
||||
&.opacity-65 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-card-header) {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
:deep(.p-card-body) {
|
||||
z-index: 1;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-div {
|
||||
> i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover > i {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/components/maintenance/TaskListItem.vue
Normal file
86
src/components/maintenance/TaskListItem.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<tr
|
||||
class="border-neutral-700 border-solid border-y"
|
||||
:class="{
|
||||
'opacity-50': runner.resolved,
|
||||
'opacity-75': isLoading && runner.resolved
|
||||
}"
|
||||
>
|
||||
<td class="text-center w-16">
|
||||
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
|
||||
</td>
|
||||
<td>
|
||||
<p class="inline-block">{{ task.name }}</p>
|
||||
<Button
|
||||
class="inline-block mx-2"
|
||||
type="button"
|
||||
:icon="PrimeIcons.INFO_CIRCLE"
|
||||
severity="secondary"
|
||||
:text="true"
|
||||
@click="toggle"
|
||||
/>
|
||||
|
||||
<Popover ref="infoPopover" class="block m-1 max-w-64 min-w-32">
|
||||
<span class="whitespace-pre-line">{{ task.description }}</span>
|
||||
</Popover>
|
||||
</td>
|
||||
<td class="text-right px-4">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
:severity
|
||||
icon-pos="right"
|
||||
@click="(event) => $emit('execute', event)"
|
||||
:loading="isExecuting"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { VueSeverity } from '@/types/primeVueTypes'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
import TaskListStatusIcon from './TaskListStatusIcon.vue'
|
||||
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const runner = computed(() => taskStore.getRunner(props.task))
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
task: MaintenanceTask
|
||||
}>()
|
||||
|
||||
// Events
|
||||
defineEmits<{
|
||||
execute: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
// Binding
|
||||
const severity = computed<VueSeverity>(() =>
|
||||
runner.value.state === 'error' || runner.value.state === 'warning'
|
||||
? 'primary'
|
||||
: 'secondary'
|
||||
)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveLoading = computed(() => runner.value.refreshing)
|
||||
const reactiveExecuting = computed(() => runner.value.executing)
|
||||
|
||||
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
|
||||
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
|
||||
|
||||
// Popover
|
||||
const infoPopover = ref()
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
infoPopover.value.toggle(event)
|
||||
}
|
||||
</script>
|
||||
115
src/components/maintenance/TaskListPanel.vue
Normal file
115
src/components/maintenance/TaskListPanel.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<!-- Tasks -->
|
||||
<section class="my-4">
|
||||
<template v-if="filter.tasks.length === 0">
|
||||
<!-- Empty filter -->
|
||||
<Divider />
|
||||
<p class="text-neutral-400 w-full text-center">
|
||||
{{ $t('maintenance.allOk') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- Display: List -->
|
||||
<table
|
||||
v-if="displayAsList === PrimeIcons.LIST"
|
||||
class="w-full border-collapse border-hidden"
|
||||
>
|
||||
<TaskListItem
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
:task
|
||||
@execute="(event) => confirmButton(event, task)"
|
||||
/>
|
||||
</table>
|
||||
|
||||
<!-- Display: Cards -->
|
||||
<template v-else>
|
||||
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
|
||||
<TaskCard
|
||||
v-for="task in filter.tasks"
|
||||
:key="task.id"
|
||||
:task
|
||||
@execute="(event) => confirmButton(event, task)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<ConfirmPopup />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import { useConfirm, useToast } from 'primevue'
|
||||
import ConfirmPopup from 'primevue/confirmpopup'
|
||||
import Divider from 'primevue/divider'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import type {
|
||||
MaintenanceFilter,
|
||||
MaintenanceTask
|
||||
} from '@/types/desktop/maintenanceTypes'
|
||||
|
||||
import TaskCard from './TaskCard.vue'
|
||||
import TaskListItem from './TaskListItem.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
|
||||
// Properties
|
||||
const props = defineProps<{
|
||||
displayAsList: string
|
||||
filter: MaintenanceFilter
|
||||
isRefreshing: boolean
|
||||
}>()
|
||||
|
||||
const executeTask = async (task: MaintenanceTask) => {
|
||||
let message: string | undefined
|
||||
|
||||
try {
|
||||
// Success
|
||||
if ((await taskStore.execute(task)) === true) return
|
||||
|
||||
message = t('maintenance.error.taskFailed')
|
||||
} catch (error) {
|
||||
message = (error as Error)?.message
|
||||
}
|
||||
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('maintenance.error.toastTitle'),
|
||||
detail: message ?? t('maintenance.error.defaultDescription'),
|
||||
life: 10_000
|
||||
})
|
||||
}
|
||||
|
||||
// Commands
|
||||
const confirmButton = async (event: MouseEvent, task: MaintenanceTask) => {
|
||||
if (!task.requireConfirm) {
|
||||
await executeTask(task)
|
||||
return
|
||||
}
|
||||
|
||||
confirm.require({
|
||||
target: event.currentTarget as HTMLElement,
|
||||
message: task.confirmText ?? t('maintenance.confirmTitle'),
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
rejectProps: {
|
||||
label: t('g.cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: task.button?.text ?? t('g.save'),
|
||||
severity: task.severity ?? 'primary'
|
||||
},
|
||||
// TODO: Not awaited.
|
||||
accept: async () => {
|
||||
await executeTask(task)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
45
src/components/maintenance/TaskListStatusIcon.vue
Normal file
45
src/components/maintenance/TaskListStatusIcon.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<ProgressSpinner v-if="!state || loading" class="h-8 w-8" />
|
||||
<template v-else>
|
||||
<i :class="cssClasses" v-tooltip.top="{ value: tooltip, showDelay: 250 }" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { MaybeRef, computed } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { MaintenanceTaskState } from '@/stores/maintenanceTaskStore'
|
||||
|
||||
// Properties
|
||||
const tooltip = computed(() => {
|
||||
if (props.state === 'error') {
|
||||
return t('g.error')
|
||||
} else if (props.state === 'OK') {
|
||||
return t('maintenance.OK')
|
||||
} else {
|
||||
return t('maintenance.Skipped')
|
||||
}
|
||||
})
|
||||
|
||||
const cssClasses = computed(() => {
|
||||
let classes: string
|
||||
if (props.state === 'error') {
|
||||
classes = `${PrimeIcons.EXCLAMATION_TRIANGLE} text-red-500`
|
||||
} else if (props.state === 'OK') {
|
||||
classes = `${PrimeIcons.CHECK} text-green-500`
|
||||
} else {
|
||||
classes = PrimeIcons.MINUS
|
||||
}
|
||||
|
||||
return `text-3xl pi ${classes}`
|
||||
})
|
||||
|
||||
// Model
|
||||
const props = defineProps<{
|
||||
state?: MaintenanceTaskState
|
||||
loading?: MaybeRef<boolean>
|
||||
}>()
|
||||
</script>
|
||||
@@ -6,11 +6,27 @@ export default {
|
||||
name: 'AutoCompletePlus',
|
||||
extends: AutoComplete,
|
||||
emits: ['focused-option-changed'],
|
||||
data() {
|
||||
return {
|
||||
// Flag to determine if IME is active
|
||||
isComposing: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (typeof AutoComplete.mounted === 'function') {
|
||||
AutoComplete.mounted.call(this)
|
||||
}
|
||||
|
||||
// Retrieve the actual <input> element and attach IME events
|
||||
const inputEl = this.$el.querySelector('input')
|
||||
if (inputEl) {
|
||||
inputEl.addEventListener('compositionstart', () => {
|
||||
this.isComposing = true
|
||||
})
|
||||
inputEl.addEventListener('compositionend', () => {
|
||||
this.isComposing = false
|
||||
})
|
||||
}
|
||||
// Add a watcher on the focusedOptionIndex property
|
||||
this.$watch(
|
||||
() => this.focusedOptionIndex,
|
||||
@@ -19,6 +35,18 @@ export default {
|
||||
this.$emit('focused-option-changed', newVal)
|
||||
}
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
// Override onKeyDown to block Enter when IME is active
|
||||
onKeyDown(event) {
|
||||
if (event.key === 'Enter' && this.isComposing) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
AutoComplete.methods.onKeyDown.call(this, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,8 +19,9 @@ export const CORE_MENU_COMMANDS = [
|
||||
[
|
||||
'Comfy.Help.OpenComfyUIIssues',
|
||||
'Comfy.Help.OpenComfyUIDocs',
|
||||
'Comfy.Help.OpenComfyOrgDiscord'
|
||||
'Comfy.Help.OpenComfyOrgDiscord',
|
||||
'Comfy.Help.OpenComfyUIForum'
|
||||
]
|
||||
],
|
||||
[['Help'], ['Comfy.Help.AboutComfyUI']]
|
||||
[['Help'], ['Comfy.Help.AboutComfyUI', 'Comfy.Feedback']]
|
||||
]
|
||||
|
||||
144
src/constants/desktopMaintenanceTasks.ts
Normal file
144
src/constants/desktopMaintenanceTasks.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { PrimeIcons } from '@primevue/core'
|
||||
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const electron = electronAPI()
|
||||
|
||||
const openUrl = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
return true
|
||||
}
|
||||
|
||||
export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
|
||||
{
|
||||
id: 'basePath',
|
||||
execute: async () => await electron.setBasePath(),
|
||||
name: 'Base path',
|
||||
shortDescription: 'Change the application base path.',
|
||||
errorDescription: 'Unable to open the base path. Please select a new one.',
|
||||
description:
|
||||
'The base path is the default location where ComfyUI stores data. It is the location fo the python environment, and may also contain models, custom nodes, and other extensions.',
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.QUESTION,
|
||||
text: 'Select'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'git',
|
||||
headerImg: '/assets/images/Git-Logo-White.svg',
|
||||
execute: () => openUrl('https://git-scm.com/downloads/'),
|
||||
name: 'Download git',
|
||||
shortDescription: 'Open the git download page.',
|
||||
description:
|
||||
'Git is required to download and manage custom nodes and other extensions. This fixer simply opens the download page in your browser. You must download and install git manually.',
|
||||
button: {
|
||||
icon: PrimeIcons.EXTERNAL_LINK,
|
||||
text: 'Download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'vcRedist',
|
||||
execute: () => openUrl('https://aka.ms/vs/17/release/vc_redist.x64.exe'),
|
||||
name: 'Download VC++ Redist',
|
||||
shortDescription: 'Download the latest VC++ Redistributable runtime.',
|
||||
description:
|
||||
'The Visual C++ runtime libraries are required to run ComfyUI. You will need to download and install this file.',
|
||||
button: {
|
||||
icon: PrimeIcons.EXTERNAL_LINK,
|
||||
text: 'Download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'reinstall',
|
||||
severity: 'danger',
|
||||
requireConfirm: true,
|
||||
execute: async () => {
|
||||
await electron.reinstall()
|
||||
return true
|
||||
},
|
||||
name: 'Reinstall ComfyUI',
|
||||
shortDescription:
|
||||
'Deletes the desktop app config and load the welcome screen.',
|
||||
description:
|
||||
'Delete the desktop app config, restart the app, and load the installation screen.',
|
||||
confirmText: 'Delete all saved config and reinstall?',
|
||||
button: {
|
||||
icon: PrimeIcons.EXCLAMATION_TRIANGLE,
|
||||
text: 'Reinstall'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pythonPackages',
|
||||
requireConfirm: true,
|
||||
execute: async () => {
|
||||
try {
|
||||
await electron.uv.installRequirements()
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
name: 'Install python packages',
|
||||
shortDescription:
|
||||
'Installs the base python packages required to run ComfyUI.',
|
||||
errorDescription:
|
||||
'Python packages that are required to run ComfyUI are not installed.',
|
||||
description:
|
||||
'This will install the python packages required to run ComfyUI. This includes torch, torchvision, and other dependencies.',
|
||||
usesTerminal: true,
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.DOWNLOAD,
|
||||
text: 'Install'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'uv',
|
||||
execute: () =>
|
||||
openUrl('https://docs.astral.sh/uv/getting-started/installation/'),
|
||||
name: 'uv executable',
|
||||
shortDescription: 'uv installs and maintains the python environment.',
|
||||
description:
|
||||
"This will open the download page for Astral's uv tool. uv is used to install python and manage python packages.",
|
||||
button: {
|
||||
icon: 'pi pi-asterisk',
|
||||
text: 'Download'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'uvCache',
|
||||
severity: 'danger',
|
||||
requireConfirm: true,
|
||||
execute: async () => await electron.uv.clearCache(),
|
||||
name: 'uv cache',
|
||||
shortDescription: 'Remove the Astral uv cache of python packages.',
|
||||
description:
|
||||
'This will remove the uv cache directory and its contents. All downloaded python packages will need to be downloaded again.',
|
||||
confirmText: 'Delete uv cache of python packages?',
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.TRASH,
|
||||
text: 'Clear cache'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'venvDirectory',
|
||||
severity: 'danger',
|
||||
requireConfirm: true,
|
||||
execute: async () => await electron.uv.resetVenv(),
|
||||
name: 'Reset virtual environment',
|
||||
shortDescription:
|
||||
'Remove and recreate the .venv directory. This removes all python packages.',
|
||||
description:
|
||||
'The python environment is where ComfyUI installs python and python packages. It is used to run the ComfyUI server.',
|
||||
confirmText: 'Delete the .venv directory?',
|
||||
usesTerminal: true,
|
||||
isInstallationFix: true,
|
||||
button: {
|
||||
icon: PrimeIcons.FOLDER,
|
||||
text: 'Recreate'
|
||||
}
|
||||
}
|
||||
] as const
|
||||
@@ -1,6 +1,8 @@
|
||||
import { t } from '@/i18n'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
;(async () => {
|
||||
@@ -8,6 +10,7 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
const electronAPI = getElectronAPI()
|
||||
const desktopAppVersion = await electronAPI.getElectronVersion()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const onChangeRestartApp = (newValue: string, oldValue: string) => {
|
||||
// Add a delay to allow changes to take effect before restarting.
|
||||
@@ -39,18 +42,18 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
id: 'Comfy-Desktop.WindowStyle',
|
||||
category: ['Comfy-Desktop', 'General', 'Window Style'],
|
||||
name: 'Window Style',
|
||||
tooltip: 'Choose custom option to hide the system title bar',
|
||||
tooltip: "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
type: 'combo',
|
||||
experimental: true,
|
||||
defaultValue: 'default',
|
||||
options: ['default', 'custom'],
|
||||
onChange: (
|
||||
newValue: 'default' | 'custom',
|
||||
oldValue: 'default' | 'custom'
|
||||
oldValue?: 'default' | 'custom'
|
||||
) => {
|
||||
electronAPI.Config.setWindowStyle(newValue)
|
||||
if (!oldValue) return
|
||||
|
||||
onChangeRestartApp(newValue, oldValue)
|
||||
electronAPI.Config.setWindowStyle(newValue)
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -112,14 +115,6 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
electronAPI.openDevTools()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.OpenFeedbackPage',
|
||||
label: 'Feedback',
|
||||
icon: 'pi pi-envelope',
|
||||
function() {
|
||||
window.open('https://forum.comfy.org/c/v1-feedback/', '_blank')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.OpenUserGuide',
|
||||
label: 'Desktop User Guide',
|
||||
@@ -149,16 +144,32 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
function() {
|
||||
electronAPI.restartApp()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy-Desktop.Quit',
|
||||
label: 'Quit',
|
||||
icon: 'pi pi-sign-out',
|
||||
async function() {
|
||||
// Confirm if unsaved workflows are open
|
||||
if (workflowStore.modifiedWorkflows.length > 0) {
|
||||
const confirmed = await useDialogService().confirm({
|
||||
message: t('desktopMenu.confirmQuit'),
|
||||
title: t('desktopMenu.quit'),
|
||||
type: 'default'
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
electronAPI.quit()
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
menuCommands: [
|
||||
{
|
||||
path: ['Help'],
|
||||
commands: [
|
||||
'Comfy-Desktop.OpenUserGuide',
|
||||
'Comfy-Desktop.OpenFeedbackPage'
|
||||
]
|
||||
commands: ['Comfy-Desktop.OpenUserGuide']
|
||||
},
|
||||
{
|
||||
path: ['Help'],
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -538,6 +539,32 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
if (workflowStore.activeWorkflow)
|
||||
workflowService.closeWorkflow(workflowStore.activeWorkflow)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Feedback',
|
||||
icon: 'pi pi-megaphone',
|
||||
label: 'Give Feedback',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
dialogService.showIssueReportDialog({
|
||||
title: t('g.feedback'),
|
||||
subtitle: t('issueReport.feedbackTitle'),
|
||||
panelProps: {
|
||||
errorType: 'Feedback',
|
||||
defaultFields: ['SystemStats', 'Settings']
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Help.OpenComfyUIForum',
|
||||
icon: 'pi pi-comments',
|
||||
label: 'Open ComfyUI Forum',
|
||||
menubarLabel: 'ComfyUI Forum',
|
||||
versionAdded: '1.8.2',
|
||||
function: () => {
|
||||
window.open('https://forum.comfy.org/', '_blank')
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Open DevTools"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "Feedback"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Desktop User Guide"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Quit"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Reinstall"
|
||||
},
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Export Workflow (API Format)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Give Feedback"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Fit Group To Contents"
|
||||
},
|
||||
@@ -107,6 +110,9 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "Open ComfyUI Docs"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Open ComfyUI Forum"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "Open ComfyUI Issues"
|
||||
},
|
||||
|
||||
@@ -72,17 +72,24 @@
|
||||
"export": "Export",
|
||||
"workflow": "Workflow",
|
||||
"success": "Success",
|
||||
"ok": "OK"
|
||||
"ok": "OK",
|
||||
"feedback": "Feedback"
|
||||
},
|
||||
"issueReport": {
|
||||
"submitErrorReport": "Submit Error Report (Optional)",
|
||||
"provideEmail": "Give us your email (Optional)",
|
||||
"provideEmail": "Give us your email (optional)",
|
||||
"provideAdditionalDetails": "Provide additional details (optional)",
|
||||
"stackTrace": "Stack Trace",
|
||||
"systemStats": "System Stats",
|
||||
"contactFollowUp": "Contact me for follow up",
|
||||
"notifyResolve": "Notify me when resolved",
|
||||
"helpFix": "Help Fix This"
|
||||
"helpFix": "Help Fix This",
|
||||
"rating": "Rating",
|
||||
"feedbackTitle": "Help us improve ComfyUI by providing feedback",
|
||||
"validation": {
|
||||
"maxLength": "Message too long",
|
||||
"invalidEmail": "Please enter a valid email address"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"default": "Default",
|
||||
@@ -359,8 +366,8 @@
|
||||
"Open Models Folder": "Open Models Folder",
|
||||
"Open Outputs Folder": "Open Outputs Folder",
|
||||
"Open DevTools": "Open DevTools",
|
||||
"Feedback": "Feedback",
|
||||
"Desktop User Guide": "Desktop User Guide",
|
||||
"Quit": "Quit",
|
||||
"Reinstall": "Reinstall",
|
||||
"Restart": "Restart",
|
||||
"Browse Templates": "Browse Templates",
|
||||
@@ -380,6 +387,7 @@
|
||||
"Duplicate Current Workflow": "Duplicate Current Workflow",
|
||||
"Export": "Export",
|
||||
"Export (API)": "Export (API)",
|
||||
"Give Feedback": "Give Feedback",
|
||||
"Fit Group To Contents": "Fit Group To Contents",
|
||||
"Group Selected Nodes": "Group Selected Nodes",
|
||||
"Convert selected nodes to group node": "Convert selected nodes to group node",
|
||||
@@ -388,6 +396,7 @@
|
||||
"About ComfyUI": "About ComfyUI",
|
||||
"Comfy-Org Discord": "Comfy-Org Discord",
|
||||
"ComfyUI Docs": "ComfyUI Docs",
|
||||
"ComfyUI Forum": "ComfyUI Forum",
|
||||
"ComfyUI Issues": "ComfyUI Issues",
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
@@ -418,7 +427,9 @@
|
||||
},
|
||||
"desktopMenu": {
|
||||
"reinstall": "Reinstall",
|
||||
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?"
|
||||
"confirmReinstall": "This will clear your extra_models_config.yaml file,\nand begin installation again.\n\nAre you sure?",
|
||||
"quit": "Quit",
|
||||
"confirmQuit": "There are unsaved workflows open; any unsaved changes will be lost. Ignore this and quit?"
|
||||
},
|
||||
"settingsCategories": {
|
||||
"Comfy-Desktop": "Comfy-Desktop",
|
||||
@@ -681,5 +692,21 @@
|
||||
"UPSCALE_MODEL": "UPSCALE_MODEL",
|
||||
"VAE": "VAE",
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"maintenance": {
|
||||
"allOk": "No issues were detected.",
|
||||
"status": "Status",
|
||||
"detected": "Detected",
|
||||
"refreshing": "Refreshing",
|
||||
"None": "None",
|
||||
"OK": "OK",
|
||||
"Skipped": "Skipped",
|
||||
"showManual": "Show maintenance tasks",
|
||||
"confirmTitle": "Are you sure?",
|
||||
"error": {
|
||||
"toastTitle": "Task error",
|
||||
"taskFailed": "Task failed to run.",
|
||||
"defaultDescription": "An error occurred while running a maintenance task."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Window Style",
|
||||
"tooltip": "Choose custom option to hide the system title bar",
|
||||
"tooltip": "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
"options": {
|
||||
"default": "default",
|
||||
"custom": "custom"
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Ouvrir les outils de développement"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "Retour d'information"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Guide de l'utilisateur du bureau"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Réinstaller"
|
||||
},
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Exporter le flux de travail (format API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Retour d'information"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Ajuster le groupe au contenu"
|
||||
},
|
||||
@@ -107,6 +110,9 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "Ouvrir les documents ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Ouvrir le forum Comfy-Org"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "Ouvrir les problèmes ComfyUI"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"WEBCAM": "WEBCAM"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "Il y a des flux de travail non enregistrés ouverts; toutes les modifications non enregistrées seront perdues. Ignorer cela et quitter?",
|
||||
"confirmReinstall": "Cela effacera votre fichier extra_models_config.yaml,\net commencera l'installation à nouveau.\n\nÊtes-vous sûr ?",
|
||||
"quit": "Quitter",
|
||||
"reinstall": "Réinstaller"
|
||||
},
|
||||
"downloadGit": {
|
||||
@@ -87,6 +89,7 @@
|
||||
"experimental": "BETA",
|
||||
"export": "Exportation",
|
||||
"extensionName": "Nom de l'extension",
|
||||
"feedback": "Commentaires",
|
||||
"findIssues": "Trouver des problèmes",
|
||||
"firstTimeUIMessage": "C'est la première fois que vous utilisez la nouvelle interface utilisateur. Choisissez \"Menu > Utiliser le nouveau menu > Désactivé\" pour restaurer l'ancienne interface utilisateur.",
|
||||
"goToNode": "Aller au nœud",
|
||||
@@ -244,13 +247,35 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Contactez-moi pour un suivi",
|
||||
"feedbackTitle": "Aidez-nous à améliorer ComfyUI en fournissant des commentaires",
|
||||
"helpFix": "Aidez à résoudre cela",
|
||||
"notifyResolve": "Prévenez-moi lorsque résolu",
|
||||
"provideAdditionalDetails": "Fournir des détails supplémentaires (facultatif)",
|
||||
"provideEmail": "Donnez-nous votre email (Facultatif)",
|
||||
"rating": "Évaluation",
|
||||
"stackTrace": "Trace de la pile",
|
||||
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
|
||||
"systemStats": "Statistiques du système"
|
||||
"systemStats": "Statistiques du système",
|
||||
"validation": {
|
||||
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
|
||||
"maxLength": "Message trop long"
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"None": "Aucun",
|
||||
"OK": "OK",
|
||||
"Skipped": "Ignoré",
|
||||
"allOk": "Aucun problème détecté.",
|
||||
"confirmTitle": "Êtes-vous sûr ?",
|
||||
"detected": "Détecté",
|
||||
"error": {
|
||||
"defaultDescription": "Une erreur s'est produite lors de l'exécution d'une tâche de maintenance.",
|
||||
"taskFailed": "La tâche a échoué.",
|
||||
"toastTitle": "Erreur de tâche"
|
||||
},
|
||||
"refreshing": "Actualisation",
|
||||
"showManual": "Afficher les tâches de maintenance",
|
||||
"status": "Statut"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "File d'attente automatique",
|
||||
@@ -288,6 +313,7 @@
|
||||
"Collapse/Expand Selected Nodes": "Réduire/Étendre les nœuds sélectionnés",
|
||||
"Comfy-Org Discord": "Discord de Comfy-Org",
|
||||
"ComfyUI Docs": "Docs de ComfyUI",
|
||||
"ComfyUI Forum": "Forum ComfyUI",
|
||||
"ComfyUI Issues": "Problèmes de ComfyUI",
|
||||
"Convert selected nodes to group node": "Convertir les nœuds sélectionnés en nœud de groupe",
|
||||
"Desktop User Guide": "Guide de l'utilisateur de bureau",
|
||||
@@ -295,9 +321,9 @@
|
||||
"Edit": "Éditer",
|
||||
"Export": "Exporter",
|
||||
"Export (API)": "Exporter (API)",
|
||||
"Feedback": "Retour d'information",
|
||||
"Fit Group To Contents": "Ajuster le groupe au contenu",
|
||||
"Fit view to selected nodes": "Ajuster la vue aux nœuds sélectionnés",
|
||||
"Give Feedback": "Donnez votre avis",
|
||||
"Group Selected Nodes": "Grouper les nœuds sélectionnés",
|
||||
"Help": "Aide",
|
||||
"Interrupt": "Interrompre",
|
||||
@@ -319,6 +345,7 @@
|
||||
"Previous Opened Workflow": "Flux de travail ouvert précédent",
|
||||
"Queue Prompt": "Invite de file d'attente",
|
||||
"Queue Prompt (Front)": "Invite de file d'attente (Front)",
|
||||
"Quit": "Quitter",
|
||||
"Redo": "Refaire",
|
||||
"Refresh Node Definitions": "Actualiser les définitions de nœud",
|
||||
"Reinstall": "Réinstaller",
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "DevToolsを開く"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "フィードバック"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "デスクトップユーザーガイド"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "終了"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "再インストール"
|
||||
},
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "ワークフローをエクスポート(API形式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "フィードバック"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "グループを内容に合わせて調整"
|
||||
},
|
||||
@@ -107,6 +110,9 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "ComfyUIのドキュメントを開く"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Comfy-Orgフォーラムを開く"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "ComfyUIの問題を開く"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"WEBCAM": "ウェブカメラ"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "保存されていないワークフローが開いています。保存されていない変更はすべて失われます。これを無視して終了しますか?",
|
||||
"confirmReinstall": "これにより、extra_models_config.yamlファイルがクリアされ、再インストールが開始されます。本当によろしいですか?",
|
||||
"quit": "終了",
|
||||
"reinstall": "再インストール"
|
||||
},
|
||||
"downloadGit": {
|
||||
@@ -87,6 +89,7 @@
|
||||
"experimental": "ベータ",
|
||||
"export": "エクスポート",
|
||||
"extensionName": "拡張機能名",
|
||||
"feedback": "フィードバック",
|
||||
"findIssues": "問題を見つける",
|
||||
"firstTimeUIMessage": "新しいUIを初めて使用しています。「メニュー > 新しいメニューを使用 > 無効」を選択して古いUIに戻してください。",
|
||||
"goToNode": "ノードに移動",
|
||||
@@ -244,13 +247,35 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "フォローアップのために私に連絡する",
|
||||
"feedbackTitle": "フィードバックを提供してComfyUIの改善にご協力ください",
|
||||
"helpFix": "これを修正するのを助ける",
|
||||
"notifyResolve": "解決したときに通知する",
|
||||
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
|
||||
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
|
||||
"rating": "評価",
|
||||
"stackTrace": "スタックトレース",
|
||||
"submitErrorReport": "エラーレポートを提出する(オプション)",
|
||||
"systemStats": "システム統計"
|
||||
"systemStats": "システム統計",
|
||||
"validation": {
|
||||
"invalidEmail": "有効なメールアドレスを入力してください",
|
||||
"maxLength": "メッセージが長すぎます"
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"None": "なし",
|
||||
"OK": "OK",
|
||||
"Skipped": "スキップされました",
|
||||
"allOk": "問題は検出されませんでした。",
|
||||
"confirmTitle": "よろしいですか?",
|
||||
"detected": "検出されました",
|
||||
"error": {
|
||||
"defaultDescription": "メンテナンスタスクの実行中にエラーが発生しました。",
|
||||
"taskFailed": "タスクの実行に失敗しました。",
|
||||
"toastTitle": "タスクエラー"
|
||||
},
|
||||
"refreshing": "更新中",
|
||||
"showManual": "メンテナンスタスクを表示",
|
||||
"status": "ステータス"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自動キュー",
|
||||
@@ -288,6 +313,7 @@
|
||||
"Collapse/Expand Selected Nodes": "選択したノードの折りたたみ/展開",
|
||||
"Comfy-Org Discord": "Comfy-Org Discord",
|
||||
"ComfyUI Docs": "ComfyUIのドキュメント",
|
||||
"ComfyUI Forum": "ComfyUI フォーラム",
|
||||
"ComfyUI Issues": "ComfyUIの問題",
|
||||
"Convert selected nodes to group node": "選択したノードをグループノードに変換",
|
||||
"Desktop User Guide": "デスクトップユーザーガイド",
|
||||
@@ -295,9 +321,9 @@
|
||||
"Edit": "編集",
|
||||
"Export": "エクスポート",
|
||||
"Export (API)": "エクスポート (API)",
|
||||
"Feedback": "フィードバック",
|
||||
"Fit Group To Contents": "グループを内容に合わせる",
|
||||
"Fit view to selected nodes": "選択したノードにビューを合わせる",
|
||||
"Give Feedback": "フィードバックを送る",
|
||||
"Group Selected Nodes": "選択したノードをグループ化",
|
||||
"Help": "ヘルプ",
|
||||
"Interrupt": "中断",
|
||||
@@ -319,6 +345,7 @@
|
||||
"Previous Opened Workflow": "前に開いたワークフロー",
|
||||
"Queue Prompt": "キューのプロンプト",
|
||||
"Queue Prompt (Front)": "キューのプロンプト (前面)",
|
||||
"Quit": "終了",
|
||||
"Redo": "やり直す",
|
||||
"Refresh Node Definitions": "ノード定義を更新",
|
||||
"Reinstall": "再インストール",
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "DevTools 열기"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "피드백"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "데스크톱 사용자 가이드"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "종료"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "재설치"
|
||||
},
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "워크플로 내보내기 (API 형식)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "피드백"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "그룹을 내용에 맞게 맞추기"
|
||||
},
|
||||
@@ -107,6 +110,9 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "ComfyUI 문서 열기"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Comfy-Org 포럼 열기"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "ComfyUI 문제 열기"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"WEBCAM": "웹캠"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "저장되지 않은 워크플로가 열려 있습니다. 저장되지 않은 변경 사항은 모두 손실됩니다. 이를 무시하고 종료하시겠습니까?",
|
||||
"confirmReinstall": "이 작업은 extra_models_config.yaml 파일을 지우고 설치를 다시 시작합니다. 정말로 진행하시겠습니까?",
|
||||
"quit": "종료",
|
||||
"reinstall": "재설치"
|
||||
},
|
||||
"downloadGit": {
|
||||
@@ -87,6 +89,7 @@
|
||||
"experimental": "베타",
|
||||
"export": "내보내기",
|
||||
"extensionName": "확장 이름",
|
||||
"feedback": "피드백",
|
||||
"findIssues": "문제 찾기",
|
||||
"firstTimeUIMessage": "새 UI를 처음 사용합니다. \"메뉴 > 새 메뉴 사용 > 비활성화\"를 선택하여 이전 UI로 복원하세요.",
|
||||
"goToNode": "노드로 이동",
|
||||
@@ -135,7 +138,7 @@
|
||||
"terminal": "터미널",
|
||||
"upload": "업로드",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로우"
|
||||
"workflow": "워크플로"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "보기 맞춤",
|
||||
@@ -200,13 +203,13 @@
|
||||
"title": "수동 구성",
|
||||
"virtualEnvironmentPath": "가상 환경 경로"
|
||||
},
|
||||
"metricsDisabled": "메트릭스 비활성화",
|
||||
"metricsEnabled": "메트릭스 활성화",
|
||||
"metricsDisabled": "데이터 수집 비활성화",
|
||||
"metricsEnabled": "데이터 수집 활성화",
|
||||
"migrateFromExistingInstallation": "기존 설치에서 마이그레이션",
|
||||
"migration": "마이그레이션",
|
||||
"migrationOptional": "마이그레이션은 선택 사항입니다. 기존에 설치된 것이 없다면, 이 단계를 건너뛸 수 있습니다.",
|
||||
"migrationSourcePathDescription": "기존의 ComfyUI 설치가 있으면, 기존 사용자 파일과 모델을 새 설치로 복사/링크할 수 있습니다. 기존의 ComfyUI 설치는 영향을 받지 않습니다.",
|
||||
"moreInfo": "자세한 정보는 우리의",
|
||||
"migrationSourcePathDescription": "기존에 설치된 ComfyUI가 있으면, 기존 사용자 파일과 모델을 새 설치본으로 복사하거나 링크 할 수 있습니다. 기존의 ComfyUI 설치는 영향을 받지 않습니다.",
|
||||
"moreInfo": "더 많은 정보를 원하시면, 다음을 읽어주세요",
|
||||
"parentMissing": "경로가 존재하지 않습니다 - 먼저 포함하는 디렉토리를 생성하세요",
|
||||
"pathExists": "디렉토리가 이미 존재합니다 - 모든 데이터를 백업했는지 확인해 주세요",
|
||||
"pathValidationFailed": "경로 유효성 검사 실패",
|
||||
@@ -214,43 +217,65 @@
|
||||
"selectItemsToMigrate": "마이그레이션 항목 선택",
|
||||
"settings": {
|
||||
"allowMetrics": "사용 통계",
|
||||
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로우 내용은 수집되지 않습니다.",
|
||||
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다.",
|
||||
"autoUpdate": "자동 업데이트",
|
||||
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
|
||||
"dataCollectionDialog": {
|
||||
"collect": {
|
||||
"errorReports": "오류 메시지 및 스택 추적",
|
||||
"systemInfo": "하드웨어, OS 유형, 앱 버전",
|
||||
"userJourneyEvents": "사용자 여정 이벤트"
|
||||
"userJourneyEvents": "사용자 행동 흐름 이벤트"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "사용자 정의 노드 구성",
|
||||
"customNodeConfigurations": "커스텀 노드 구성",
|
||||
"fileSystemInformation": "파일 시스템 정보",
|
||||
"personalInformation": "개인 정보",
|
||||
"workflowContents": "워크플로우 내용"
|
||||
"workflowContents": "워크플로 내용"
|
||||
},
|
||||
"title": "데이터 수집 안내",
|
||||
"viewFullPolicy": "전체 정책 보기",
|
||||
"whatWeCollect": "수집하는 정보:",
|
||||
"whatWeDoNotCollect": "수집하지 않는 정보:"
|
||||
},
|
||||
"errorUpdatingConsent": "동의 업데이트 오류",
|
||||
"errorUpdatingConsentDetail": "메트릭스 동의 설정 업데이트에 실패했습니다",
|
||||
"errorUpdatingConsent": "데이터 수집 동의 설정 업데이트 오류",
|
||||
"errorUpdatingConsentDetail": "데이터 수집 동의 설정 업데이트에 실패했습니다",
|
||||
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
|
||||
},
|
||||
"systemLocations": "시스템 위치",
|
||||
"unhandledError": "알 수 없는 오류",
|
||||
"updateConsent": "당신은 이전에 충돌 보고에 동의했습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 메트릭스를 추적하고 있습니다. 개인 식별 정보는 수집하지 않습니다."
|
||||
"updateConsent": "이전에 충돌 보고에 동의하셨습니다. 이제 버그를 식별하고 앱을 개선하기 위해 이벤트 기반 통계 정보의 추적을 시작합니다. 개인을 식별할 수 있는 정보는 수집되지 않습니다."
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
|
||||
"feedbackTitle": "피드백을 제공함으로써 ComfyUI를 개선하는 데 도움을 주십시오",
|
||||
"helpFix": "이 문제 해결에 도움을 주세요",
|
||||
"notifyResolve": "해결되었을 때 알려주세요",
|
||||
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
|
||||
"provideEmail": "이메일을 알려주세요 (선택 사항)",
|
||||
"rating": "평가",
|
||||
"stackTrace": "스택 추적",
|
||||
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
|
||||
"systemStats": "시스템 통계"
|
||||
"systemStats": "시스템 통계",
|
||||
"validation": {
|
||||
"invalidEmail": "유효한 이메일 주소를 입력해 주세요",
|
||||
"maxLength": "메시지가 너무 깁니다"
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"None": "없음",
|
||||
"OK": "확인",
|
||||
"Skipped": "건너뜀",
|
||||
"allOk": "문제가 발견되지 않았습니다.",
|
||||
"confirmTitle": "확실합니까?",
|
||||
"detected": "감지됨",
|
||||
"error": {
|
||||
"defaultDescription": "유지 보수 작업을 실행하는 동안 오류가 발생했습니다.",
|
||||
"taskFailed": "작업 실행에 실패했습니다.",
|
||||
"toastTitle": "작업 오류"
|
||||
},
|
||||
"refreshing": "새로 고침 중",
|
||||
"showManual": "유지 보수 작업 보기",
|
||||
"status": "상태"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "자동 실행 큐",
|
||||
@@ -284,20 +309,21 @@
|
||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||
"Clear Workflow": "워크플로 지우기",
|
||||
"Clipspace": "클립스페이스",
|
||||
"Close Current Workflow": "현재 워크플로우 닫기",
|
||||
"Close Current Workflow": "현재 워크플로 닫기",
|
||||
"Collapse/Expand Selected Nodes": "선택한 노드 축소/확장",
|
||||
"Comfy-Org Discord": "Comfy-Org 디스코드",
|
||||
"ComfyUI Docs": "ComfyUI 문서",
|
||||
"ComfyUI Forum": "ComfyUI 포럼",
|
||||
"ComfyUI Issues": "ComfyUI 이슈 페이지",
|
||||
"Convert selected nodes to group node": "선택한 노드를 그룹 노드로 변환",
|
||||
"Desktop User Guide": "데스크톱 사용자 가이드",
|
||||
"Duplicate Current Workflow": "현재 워크플로우 복제",
|
||||
"Duplicate Current Workflow": "현재 워크플로 복제",
|
||||
"Edit": "편집",
|
||||
"Export": "내보내기",
|
||||
"Export (API)": "내보내기 (API)",
|
||||
"Feedback": "피드백",
|
||||
"Fit Group To Contents": "그룹을 내용에 맞게 조정",
|
||||
"Fit view to selected nodes": "선택한 노드에 맞게 보기 조정",
|
||||
"Give Feedback": "피드백 제공",
|
||||
"Group Selected Nodes": "선택한 노드 그룹화",
|
||||
"Help": "도움말",
|
||||
"Interrupt": "중단",
|
||||
@@ -319,6 +345,7 @@
|
||||
"Previous Opened Workflow": "이전 열린 워크플로",
|
||||
"Queue Prompt": "실행 큐에 프롬프트 추가",
|
||||
"Queue Prompt (Front)": "실행 큐 맨 앞에 프롬프트 추가",
|
||||
"Quit": "종료",
|
||||
"Redo": "다시 실행",
|
||||
"Refresh Node Definitions": "노드 정의 새로 고침",
|
||||
"Reinstall": "재설치",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "노이즈"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "시그마"
|
||||
"name": "시그마 배열"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1425,10 +1425,10 @@
|
||||
}
|
||||
},
|
||||
"FlipSigmas": {
|
||||
"display_name": "시그마 뒤집기",
|
||||
"display_name": "시그마 배열 뒤집기",
|
||||
"inputs": {
|
||||
"sigmas": {
|
||||
"name": "시그마"
|
||||
"name": "시그마 배열"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4567,7 +4567,7 @@
|
||||
"name": "샘플러"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "시그마"
|
||||
"name": "시그마 배열"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -4595,7 +4595,7 @@
|
||||
"name": "샘플러"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "시그마"
|
||||
"name": "시그마 배열"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -4875,7 +4875,7 @@
|
||||
"name": "시그마"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "시그마들"
|
||||
"name": "시그마 배열"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4983,10 +4983,10 @@
|
||||
}
|
||||
},
|
||||
"SplitSigmas": {
|
||||
"display_name": "시그마 분할 (스텝)",
|
||||
"display_name": "시그마 배열 분할 (스텝)",
|
||||
"inputs": {
|
||||
"sigmas": {
|
||||
"name": "시그마"
|
||||
"name": "시그마 배열"
|
||||
},
|
||||
"step": {
|
||||
"name": "분할 스텝"
|
||||
@@ -4994,29 +4994,29 @@
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "높은 시그마"
|
||||
"name": "높은 시그마 배열"
|
||||
},
|
||||
"1": {
|
||||
"name": "낮은 시그마"
|
||||
"name": "낮은 시그마 배열"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SplitSigmasDenoise": {
|
||||
"display_name": "시그마 분할 (노이즈 제거양)",
|
||||
"display_name": "시그마 배열 분할 (노이즈 제거양)",
|
||||
"inputs": {
|
||||
"denoise": {
|
||||
"name": "노이즈 제거양"
|
||||
},
|
||||
"sigmas": {
|
||||
"name": "시그마"
|
||||
"name": "시그마 배열"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "높은 시그마"
|
||||
"name": "높은 시그마 배열"
|
||||
},
|
||||
"1": {
|
||||
"name": "낮은 시그마"
|
||||
"name": "낮은 시그마 배열"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
{
|
||||
"Comfy-Desktop_Folders_OpenCustomNodesFolder": {
|
||||
"label": "Открыть папку с пользовательскими узлами"
|
||||
"label": "Открыть папку пользовательских нод"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenInputsFolder": {
|
||||
"label": "Открыть папку с входными данными"
|
||||
"label": "Открыть папку входных данных"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenLogsFolder": {
|
||||
"label": "Открыть папку с логами"
|
||||
"label": "Открыть папку логов"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelConfig": {
|
||||
"label": "Открыть extra_model_paths.yaml"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenModelsFolder": {
|
||||
"label": "Открыть папку с моделями"
|
||||
"label": "Открыть папку моделей"
|
||||
},
|
||||
"Comfy-Desktop_Folders_OpenOutputsFolder": {
|
||||
"label": "Открыть папку с результатами"
|
||||
"label": "Открыть папку результатов"
|
||||
},
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "Открыть инструменты разработчика"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "Обратная связь"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "Руководство пользователя для рабочего стола"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "Выйти"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "Переустановить"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"label": "Просмотр шаблонов"
|
||||
},
|
||||
"Comfy_Canvas_FitView": {
|
||||
"label": "Подогнать вид к выбранным узлам"
|
||||
"label": "Подогнать вид к выбранным нодам"
|
||||
},
|
||||
"Comfy_Canvas_ResetView": {
|
||||
"label": "Сбросить вид"
|
||||
@@ -48,19 +48,19 @@
|
||||
"label": "Переключить блокировку холста"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "Обход/Необход выбранных узлов"
|
||||
"label": "Обход/Необход выбранных нод"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Collapse": {
|
||||
"label": "Свернуть/Развернуть выбранные узлы"
|
||||
"label": "Свернуть/Развернуть выбранные ноды"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Mute": {
|
||||
"label": "Отключить/Включить звук выбранных узлов"
|
||||
"label": "Отключить/Включить звук выбранных нод"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Pin": {
|
||||
"label": "Закрепить/Открепить выбранные узлы"
|
||||
"label": "Закрепить/Открепить выбранные ноды"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "Закрепить/Открепить выбранные элементы"
|
||||
"label": "Закрепить/Открепить выбранных нод"
|
||||
},
|
||||
"Comfy_Canvas_ZoomIn": {
|
||||
"label": "Увеличить"
|
||||
@@ -83,32 +83,38 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "Экспорт рабочего процесса (формат API)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "Обратная связь"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "Подогнать группу к содержимому"
|
||||
},
|
||||
"Comfy_Graph_GroupSelectedNodes": {
|
||||
"label": "Группировать выбранные узлы"
|
||||
"label": "Группировать выбранные ноды"
|
||||
},
|
||||
"Comfy_GroupNode_ConvertSelectedNodesToGroupNode": {
|
||||
"label": "Преобразовать выбранные узлы в групповой узел"
|
||||
"label": "Преобразовать выбранные ноды в групповую ноду"
|
||||
},
|
||||
"Comfy_GroupNode_ManageGroupNodes": {
|
||||
"label": "Управление групповыми узлами"
|
||||
"label": "Управление групповыми нодами"
|
||||
},
|
||||
"Comfy_GroupNode_UngroupSelectedGroupNodes": {
|
||||
"label": "Разгруппировать выбранные групповые узлы"
|
||||
"label": "Разгруппировать выбранные групповые ноды"
|
||||
},
|
||||
"Comfy_Help_AboutComfyUI": {
|
||||
"label": "Открыть о ComfyUI"
|
||||
"label": "Открыть «О ComfyUI»"
|
||||
},
|
||||
"Comfy_Help_OpenComfyOrgDiscord": {
|
||||
"label": "Открыть Comfy-Org Discord"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "Открыть документы ComfyUI"
|
||||
"label": "Открыть документацию ComfyUI"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "Открыть форум Comfy-Org"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "Открыть проблемы ComfyUI"
|
||||
"label": "Открыть ComfyUI Issues"
|
||||
},
|
||||
"Comfy_Interrupt": {
|
||||
"label": "Прервать"
|
||||
@@ -135,7 +141,7 @@
|
||||
"label": "Повторить"
|
||||
},
|
||||
"Comfy_RefreshNodeDefinitions": {
|
||||
"label": "Обновить определения узлов"
|
||||
"label": "Обновить определения нод"
|
||||
},
|
||||
"Comfy_SaveWorkflow": {
|
||||
"label": "Сохранить рабочий процесс"
|
||||
@@ -147,7 +153,7 @@
|
||||
"label": "Показать диалог настроек"
|
||||
},
|
||||
"Comfy_ToggleTheme": {
|
||||
"label": "Переключить тему (Темная/Светлая)"
|
||||
"label": "Переключить тему (Тёмная/Светлая)"
|
||||
},
|
||||
"Comfy_Undo": {
|
||||
"label": "Отменить"
|
||||
@@ -171,7 +177,7 @@
|
||||
"label": "Переключить нижнюю панель терминала"
|
||||
},
|
||||
"Workspace_ToggleBottomPanelTab_logs-terminal": {
|
||||
"label": "Переключить нижнюю панель журналов"
|
||||
"label": "Переключить нижнюю панель логов"
|
||||
},
|
||||
"Workspace_ToggleFocusMode": {
|
||||
"label": "Переключить режим фокуса"
|
||||
@@ -181,8 +187,8 @@
|
||||
"tooltip": "Библиотека моделей"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_node-library": {
|
||||
"label": "Переключить боковую панель библиотеки узлов",
|
||||
"tooltip": "Библиотека узлов"
|
||||
"label": "Переключить боковую панель библиотеки нод",
|
||||
"tooltip": "Библиотека нод"
|
||||
},
|
||||
"Workspace_ToggleSidebarTab_queue": {
|
||||
"label": "Переключить боковую панель очереди",
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"blue": "Синий",
|
||||
"custom": "Пользовательский",
|
||||
"default": "По умолчанию",
|
||||
"green": "Зеленый",
|
||||
"green": "Зелёный",
|
||||
"pink": "Розовый",
|
||||
"red": "Красный",
|
||||
"yellow": "Желтый"
|
||||
"yellow": "Жёлтый"
|
||||
},
|
||||
"dataTypes": {
|
||||
"AUDIO": "АУДИО",
|
||||
@@ -16,17 +16,17 @@
|
||||
"CLIP_VISION_OUTPUT": "CLIP_VISION_OUTPUT",
|
||||
"COMBO": "КОМБО",
|
||||
"CONDITIONING": "КОНДИЦИОНИРОВАНИЕ",
|
||||
"CONTROL_NET": "СЕТЬ УПРАВЛЕНИЯ",
|
||||
"CONTROL_NET": "CONTROL_NET",
|
||||
"FLOAT": "ПЛАВАЮЩИЙ",
|
||||
"FLOATS": "ПЛАВАЮЩИЕ",
|
||||
"GLIGEN": "GLIGEN",
|
||||
"GUIDER": "РУКОВОДИТЕЛЬ",
|
||||
"HOOKS": "КРЮКИ",
|
||||
"HOOK_KEYFRAMES": "КЛЮЧЕВЫЕ КАДРЫ КРЮКА",
|
||||
"GUIDER": "ГИД",
|
||||
"HOOKS": "ХУКИ",
|
||||
"HOOK_KEYFRAMES": "КЛЮЧЕВЫЕ_КАДРЫ_ХУКА",
|
||||
"IMAGE": "ИЗОБРАЖЕНИЕ",
|
||||
"INT": "ЦЕЛОЕ",
|
||||
"LATENT": "ЛАТЕНТНЫЙ",
|
||||
"LATENT_OPERATION": "ЛАТЕНТНАЯ ОПЕРАЦИЯ",
|
||||
"LATENT_OPERATION": "ЛАТЕНТНАЯ_ОПЕРАЦИЯ",
|
||||
"LOAD_3D": "ЗАГРУЗИТЬ_3D",
|
||||
"LOAD_3D_ANIMATION": "ЗАГРУЗИТЬ_3D_АНИМАЦИЮ",
|
||||
"MASK": "МАСКА",
|
||||
@@ -36,23 +36,25 @@
|
||||
"SAMPLER": "СЭМПЛЕР",
|
||||
"SIGMAS": "СИГМЫ",
|
||||
"STRING": "СТРОКА",
|
||||
"STYLE_MODEL": "МОДЕЛЬ СТИЛЯ",
|
||||
"TIMESTEPS_RANGE": "ДИАПАЗОН ВРЕМЕННЫХ ШАГОВ",
|
||||
"UPSCALE_MODEL": "МОДЕЛЬ УВЕЛИЧЕНИЯ",
|
||||
"STYLE_MODEL": "МОДЕЛЬ_СТИЛЯ",
|
||||
"TIMESTEPS_RANGE": "ДИАПАЗОН_ВРЕМЕННЫХ_ШАГОВ",
|
||||
"UPSCALE_MODEL": "МОДЕЛЬ_АПСКЕЙЛА",
|
||||
"VAE": "VAE",
|
||||
"WEBCAM": "ВЕБ-КАМЕРА"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнет установку заново. Вы уверены?",
|
||||
"confirmQuit": "Открыты несохраненные рабочие процессы; все несохраненные изменения будут потеряны. Проигнорировать это и выйти?",
|
||||
"confirmReinstall": "Это очистит ваш файл extra_models_config.yaml и начнёт установку заново. Вы уверены?",
|
||||
"quit": "Выйти",
|
||||
"reinstall": "Переустановить"
|
||||
},
|
||||
"downloadGit": {
|
||||
"gitWebsite": "Скачать git",
|
||||
"instructions": "Пожалуйста, скачайте и установите последнюю версию для вашей операционной системы. Кнопка 'Скачать git' ниже открывает страницу загрузок git-scm.com.",
|
||||
"instructions": "Пожалуйста, скачайте и установите последнюю версию для вашей операционной системы. Кнопка «Скачать git» ниже открывает страницу загрузок git-scm.com.",
|
||||
"message": "Не удалось найти git. Рабочая копия git необходима для нормальной работы.",
|
||||
"skip": "Пропустить",
|
||||
"title": "Скачать git",
|
||||
"warning": "Если вы уверены, что вам не нужно устанавливать git, или произошла ошибка, вы можете нажать 'Пропустить', чтобы обойти эту проверку. Попытка запустить ComfyUI без рабочей копии git в настоящее время не поддерживается."
|
||||
"warning": "Если вы уверены, что вам не нужно устанавливать git, или произошла ошибка, вы можете нажать «Пропустить», чтобы обойти эту проверку. Попытка запустить ComfyUI без рабочей копии git в настоящее время не поддерживается."
|
||||
},
|
||||
"electronFileDownload": {
|
||||
"cancel": "Отменить загрузку",
|
||||
@@ -77,7 +79,7 @@
|
||||
"customize": "Настроить",
|
||||
"customizeFolder": "Настроить папку",
|
||||
"delete": "Удалить",
|
||||
"deprecated": "УСТАРЕЛО",
|
||||
"deprecated": "Устарело",
|
||||
"devices": "Устройства",
|
||||
"disableAll": "Отключить все",
|
||||
"download": "Скачать",
|
||||
@@ -86,10 +88,11 @@
|
||||
"error": "Ошибка",
|
||||
"experimental": "БЕТА",
|
||||
"export": "Экспорт",
|
||||
"extensionName": "Имя расширения",
|
||||
"extensionName": "Название расширения",
|
||||
"feedback": "Обратная связь",
|
||||
"findIssues": "Найти проблемы",
|
||||
"firstTimeUIMessage": "Вы впервые используете новый интерфейс. Выберите \"Меню > Использовать новое меню > Отключено\", чтобы восстановить старый интерфейс.",
|
||||
"goToNode": "Перейти к узлу",
|
||||
"goToNode": "Перейти к ноде",
|
||||
"icon": "Иконка",
|
||||
"imageFailedToLoad": "Не удалось загрузить изображение",
|
||||
"import": "Импорт",
|
||||
@@ -99,7 +102,7 @@
|
||||
"loadAllFolders": "Загрузить все папки",
|
||||
"loadWorkflow": "Загрузить рабочий процесс",
|
||||
"loading": "Загрузка",
|
||||
"logs": "Журналы",
|
||||
"logs": "Логи",
|
||||
"newFolder": "Новая папка",
|
||||
"next": "Далее",
|
||||
"no": "Нет",
|
||||
@@ -115,21 +118,21 @@
|
||||
"refresh": "Обновить",
|
||||
"reloadToApplyChanges": "Перезагрузите, чтобы применить изменения",
|
||||
"rename": "Переименовать",
|
||||
"reportIssue": "Отправить отчет",
|
||||
"reportIssueTooltip": "Отправить отчет об ошибке в Comfy Org",
|
||||
"reportSent": "Отчет отправлен",
|
||||
"reportIssue": "Отправить отчёт",
|
||||
"reportIssueTooltip": "Отправить отчёт об ошибке в Comfy Org",
|
||||
"reportSent": "Отчёт отправлен",
|
||||
"reset": "Сбросить",
|
||||
"resetKeybindingsTooltip": "Сбросить сочетания клавиш к умолчанию",
|
||||
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
|
||||
"save": "Сохранить",
|
||||
"searchExtensions": "Поиск расширений",
|
||||
"searchFailedMessage": "Мы не смогли найти настройки, соответствующие вашему запросу. Попробуйте изменить поисковые термины.",
|
||||
"searchKeybindings": "Поиск сочетаний клавиш",
|
||||
"searchModels": "Поиск моделей",
|
||||
"searchNodes": "Поиск узлов",
|
||||
"searchNodes": "Поиск нод",
|
||||
"searchSettings": "Поиск настроек",
|
||||
"searchWorkflows": "Поиск рабочих процессов",
|
||||
"settings": "Настройки",
|
||||
"showReport": "Показать отчет",
|
||||
"showReport": "Показать отчёт",
|
||||
"success": "Успех",
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
@@ -147,8 +150,8 @@
|
||||
"zoomOut": "Уменьшить"
|
||||
},
|
||||
"groupNode": {
|
||||
"create": "Создать узел группы",
|
||||
"enterName": "Введите имя"
|
||||
"create": "Создать ноду группы",
|
||||
"enterName": "Введите название"
|
||||
},
|
||||
"icon": {
|
||||
"bookmark": "Закладка",
|
||||
@@ -166,17 +169,17 @@
|
||||
"appPathLocationTooltip": "Директория активов приложения ComfyUI. Хранит код и активы ComfyUI",
|
||||
"cannotWrite": "Невозможно записать в выбранный путь",
|
||||
"chooseInstallationLocation": "Выберите место установки",
|
||||
"customNodes": "Пользовательские узлы",
|
||||
"customNodesDescription": "Переустановите пользовательские узлы из существующих установок ComfyUI.",
|
||||
"desktopAppSettings": "Настройки настольного приложения",
|
||||
"desktopAppSettingsDescription": "Настройте, как ComfyUI ведет себя на вашем рабочем столе. Вы можете изменить эти настройки позже.",
|
||||
"customNodes": "Пользовательские ноды",
|
||||
"customNodesDescription": "Переустановите пользовательские ноды из существующих установок ComfyUI.",
|
||||
"desktopAppSettings": "Настройки десктопного приложения",
|
||||
"desktopAppSettingsDescription": "Настройте, как ComfyUI ведёт себя на вашем рабочем столе. Вы можете изменить эти настройки позже.",
|
||||
"desktopSettings": "Настройки рабочего стола",
|
||||
"failedToSelectDirectory": "Не удалось выбрать директорию",
|
||||
"gpu": "GPU",
|
||||
"gpuSelection": {
|
||||
"cpuMode": "Режим CPU",
|
||||
"cpuModeDescription": "Режим CPU предназначен только для разработчиков и редких крайних случаев.",
|
||||
"cpuModeDescription2": "Если вы не абсолютно уверены, что вам это нужно, пожалуйста, проигнорируйте эту галочку и выберите ваш GPU выше.",
|
||||
"cpuModeDescription": "Режим CPU предназначен только для разработчиков и крайне редких случаев.",
|
||||
"cpuModeDescription2": "Если вы не полностью уверены, что вам это нужно, пожалуйста, проигнорируйте эту галочку и выберите ваш GPU выше.",
|
||||
"customComfyNeedsPython": "ComfyUI не будет работать, пока python не будет настроен",
|
||||
"customInstallRequirements": "Установите все требования и зависимости (например, custom torch)",
|
||||
"customManualVenv": "Вручную настроить python venv",
|
||||
@@ -191,8 +194,8 @@
|
||||
"helpImprove": "Пожалуйста, помогите улучшить ComfyUI",
|
||||
"installLocation": "Место установки",
|
||||
"installLocationDescription": "Выберите директорию для пользовательских данных ComfyUI. В выбранном месте будет установлена среда Python. Пожалуйста, убедитесь, что на выбранном диске достаточно места (~15 ГБ).",
|
||||
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские узлы\n",
|
||||
"insufficientFreeSpace": "Недостаточно места - минимально необходимое свободное место",
|
||||
"installLocationTooltip": "Директория пользовательских данных ComfyUI. Хранит:\n- Среда Python\n- Модели\n- Пользовательские ноды\n",
|
||||
"insufficientFreeSpace": "Недостаточно места — минимально необходимое свободное место",
|
||||
"manualConfiguration": {
|
||||
"createVenv": "Вам потребуется создать виртуальное окружение в следующем каталоге",
|
||||
"requirements": "Требования",
|
||||
@@ -205,10 +208,10 @@
|
||||
"migrateFromExistingInstallation": "Миграция из существующей установки",
|
||||
"migration": "Миграция",
|
||||
"migrationOptional": "Миграция является необязательной. Если у вас нет существующей установки, вы можете пропустить этот шаг.",
|
||||
"migrationSourcePathDescription": "Если у вас уже есть установка ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели с новой установкой. Ваша существующая установка ComfyUI не будет затронута.",
|
||||
"migrationSourcePathDescription": "Если у вас уже есть установленный ComfyUI, мы можем скопировать/связать ваши существующие пользовательские файлы и модели с новой установкой. Ваша существующая установка ComfyUI не будет затронута.",
|
||||
"moreInfo": "Для получения дополнительной информации, пожалуйста, прочтите нашу",
|
||||
"parentMissing": "Путь не существует - сначала создайте родительский каталог",
|
||||
"pathExists": "Директория уже существует - пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
|
||||
"parentMissing": "Путь не существует — сначала создайте родительский каталог",
|
||||
"pathExists": "Директория уже существует — пожалуйста, убедитесь, что вы сделали резервное копирование всех данных",
|
||||
"pathValidationFailed": "Не удалось проверить путь",
|
||||
"privacyPolicy": "политику конфиденциальности",
|
||||
"selectItemsToMigrate": "Выберите элементы для миграции",
|
||||
@@ -224,7 +227,7 @@
|
||||
"userJourneyEvents": "События пользовательского пути"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "Пользовательские конфигурации узлов",
|
||||
"customNodeConfigurations": "Пользовательские конфигурации нод",
|
||||
"fileSystemInformation": "Информация о файловой системе",
|
||||
"personalInformation": "Личная информация",
|
||||
"workflowContents": "Содержание рабочего процесса"
|
||||
@@ -240,17 +243,39 @@
|
||||
},
|
||||
"systemLocations": "Системные места",
|
||||
"unhandledError": "Неизвестная ошибка",
|
||||
"updateConsent": "Вы ранее согласились на отчетность об ошибках. Теперь мы отслеживаем событийные метрики, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
|
||||
"updateConsent": "Вы ранее согласились на отчётность об ошибках. Теперь мы отслеживаем метрики событий, чтобы помочь выявить ошибки и улучшить приложение. Личная идентифицируемая информация не собирается."
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Свяжитесь со мной для уточнения",
|
||||
"feedbackTitle": "Помогите нам улучшить ComfyUI, оставив отзыв",
|
||||
"helpFix": "Помочь исправить это",
|
||||
"notifyResolve": "Уведомить меня, когда проблема будет решена",
|
||||
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
|
||||
"provideEmail": "Укажите вашу электронную почту (необязательно)",
|
||||
"rating": "Рейтинг",
|
||||
"stackTrace": "Трассировка стека",
|
||||
"submitErrorReport": "Отправить отчет об ошибке (необязательно)",
|
||||
"systemStats": "Статистика системы"
|
||||
"submitErrorReport": "Отправить отчёт об ошибке (необязательно)",
|
||||
"systemStats": "Статистика системы",
|
||||
"validation": {
|
||||
"invalidEmail": "Пожалуйста, введите действительный адрес электронной почты",
|
||||
"maxLength": "Сообщение слишком длинное"
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"None": "Нет",
|
||||
"OK": "OK",
|
||||
"Skipped": "Пропущено",
|
||||
"allOk": "Проблем не обнаружено.",
|
||||
"confirmTitle": "Вы уверены?",
|
||||
"detected": "Обнаружено",
|
||||
"error": {
|
||||
"defaultDescription": "Произошла ошибка при выполнении задачи по обслуживанию.",
|
||||
"taskFailed": "Не удалось выполнить задачу.",
|
||||
"toastTitle": "Ошибка задачи"
|
||||
},
|
||||
"refreshing": "Обновление",
|
||||
"showManual": "Показать задачи по обслуживанию",
|
||||
"status": "Статус"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "Автоочередь",
|
||||
@@ -270,7 +295,7 @@
|
||||
"queue": "Выполнить",
|
||||
"queueWorkflow": "Очередь рабочего процесса (Shift для вставки спереди)",
|
||||
"queueWorkflowFront": "Очередь рабочего процесса (Вставка спереди)",
|
||||
"refresh": "Обновить определения узлов",
|
||||
"refresh": "Обновить определения нод",
|
||||
"resetView": "Сбросить вид холста",
|
||||
"showMenu": "Показать меню",
|
||||
"toggleBottomPanel": "Переключить нижнюю панель"
|
||||
@@ -278,36 +303,37 @@
|
||||
"menuLabels": {
|
||||
"About ComfyUI": "О ComfyUI",
|
||||
"Browse Templates": "Просмотреть шаблоны",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные узлы",
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
"Clipspace": "Клиппространство",
|
||||
"Close Current Workflow": "Закрыть текущий рабочий процесс",
|
||||
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные узлы",
|
||||
"Collapse/Expand Selected Nodes": "Свернуть/развернуть выбранные ноды",
|
||||
"Comfy-Org Discord": "Discord Comfy-Org",
|
||||
"ComfyUI Docs": "Документация ComfyUI",
|
||||
"ComfyUI Forum": "Форум ComfyUI",
|
||||
"ComfyUI Issues": "Проблемы ComfyUI",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные узлы в групповой узел",
|
||||
"Convert selected nodes to group node": "Преобразовать выбранные ноды в групповую ноду",
|
||||
"Desktop User Guide": "Руководство пользователя для настольных ПК",
|
||||
"Duplicate Current Workflow": "Дублировать текущий рабочий процесс",
|
||||
"Edit": "Редактировать",
|
||||
"Export": "Экспортировать",
|
||||
"Export (API)": "Экспорт (API)",
|
||||
"Feedback": "Обратная связь",
|
||||
"Fit Group To Contents": "Подогнать группу под содержимое",
|
||||
"Fit view to selected nodes": "Подогнать вид под выбранные узлы",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные узлы",
|
||||
"Fit view to selected nodes": "Подогнать вид под выбранные ноды",
|
||||
"Give Feedback": "Оставить отзыв",
|
||||
"Group Selected Nodes": "Сгруппировать выбранные ноды",
|
||||
"Help": "Помощь",
|
||||
"Interrupt": "Прервать",
|
||||
"Load Default Workflow": "Загрузить стандартный рабочий процесс",
|
||||
"Manage group nodes": "Управление групповыми узлами",
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных узлов",
|
||||
"Manage group nodes": "Управление групповыми нодами",
|
||||
"Mute/Unmute Selected Nodes": "Отключить/включить звук для выбранных нод",
|
||||
"New": "Новый",
|
||||
"Next Opened Workflow": "Следующий открытый рабочий процесс",
|
||||
"Open": "Открыть",
|
||||
"Open Custom Nodes Folder": "Открыть папку пользовательских узлов",
|
||||
"Open Custom Nodes Folder": "Открыть папку пользовательских нод",
|
||||
"Open DevTools": "Открыть инструменты разработчика",
|
||||
"Open Inputs Folder": "Открыть папку входных данных",
|
||||
"Open Logs Folder": "Открыть папку журналов",
|
||||
@@ -315,12 +341,13 @@
|
||||
"Open Outputs Folder": "Открыть папку выходных данных",
|
||||
"Open extra_model_paths_yaml": "Открыть extra_model_paths.yaml",
|
||||
"Pin/Unpin Selected Items": "Закрепить/открепить выбранные элементы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные узлы",
|
||||
"Pin/Unpin Selected Nodes": "Закрепить/открепить выбранные ноды",
|
||||
"Previous Opened Workflow": "Предыдущий открытый рабочий процесс",
|
||||
"Queue Prompt": "Запрос в очереди",
|
||||
"Queue Prompt (Front)": "Запрос в очереди (спереди)",
|
||||
"Quit": "Выйти",
|
||||
"Redo": "Повторить",
|
||||
"Refresh Node Definitions": "Обновить определения узлов",
|
||||
"Refresh Node Definitions": "Обновить определения нод",
|
||||
"Reinstall": "Переустановить",
|
||||
"Reset View": "Сбросить вид",
|
||||
"Restart": "Перезапустить",
|
||||
@@ -331,14 +358,14 @@
|
||||
"Toggle Focus Mode": "Переключить режим фокуса",
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки узлов",
|
||||
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
|
||||
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
|
||||
"Toggle Theme (Dark/Light)": "Переключение темы (Темная/Светлая)",
|
||||
"Toggle Theme (Dark/Light)": "Переключение темы (Тёмная/Светлая)",
|
||||
"Toggle Workflows Sidebar": "Переключение боковой панели рабочих процессов",
|
||||
"Undo": "Отменить",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые узлы",
|
||||
"Ungroup selected group nodes": "Разгруппировать выбранные групповые ноды",
|
||||
"Workflow": "Рабочий процесс",
|
||||
"Zoom In": "Увеличить",
|
||||
"Zoom Out": "Уменьшить"
|
||||
@@ -354,24 +381,24 @@
|
||||
"attention_experiments": "эксперименты_внимания",
|
||||
"audio": "аудио",
|
||||
"batch": "пакет",
|
||||
"clip": "клип",
|
||||
"clip": "clip",
|
||||
"combine": "объединить",
|
||||
"compositing": "композитинг",
|
||||
"compositing": "композиционирование",
|
||||
"cond pair": "условие_пара",
|
||||
"cond single": "условие_одиночное",
|
||||
"conditioning": "условие",
|
||||
"controlnet": "контрольная_сеть",
|
||||
"controlnet": "controlnet",
|
||||
"create": "создать",
|
||||
"custom_sampling": "пользовательская_выборка",
|
||||
"custom_sampling": "пользовательский_семплинг",
|
||||
"deprecated": "устаревший",
|
||||
"flux": "flux",
|
||||
"gligen": "глиген",
|
||||
"gligen": "gligen",
|
||||
"guidance": "направление",
|
||||
"guiders": "направляющие",
|
||||
"hooks": "хуки",
|
||||
"image": "изображение",
|
||||
"inpaint": "восстановление",
|
||||
"instructpix2pix": "инструктпикс2пикс",
|
||||
"instructpix2pix": "instructpix2pix",
|
||||
"latent": "латентный",
|
||||
"loaders": "загрузчики",
|
||||
"ltxv": "ltxv",
|
||||
@@ -387,26 +414,26 @@
|
||||
"preprocessors": "предобработчики",
|
||||
"samplers": "семплеры",
|
||||
"sampling": "выборка",
|
||||
"schedulers": "планировщики",
|
||||
"scheduling": "планирование",
|
||||
"schedulers": "schedulers",
|
||||
"scheduling": "scheduling",
|
||||
"sd3": "sd3",
|
||||
"sigmas": "сигмы",
|
||||
"stable_cascade": "стабильная_каскадная",
|
||||
"style_model": "модель_стиля",
|
||||
"transform": "преобразование",
|
||||
"unet": "унет",
|
||||
"upscale_diffusion": "увеличение_диффузии",
|
||||
"upscaling": "увеличение",
|
||||
"unet": "unet",
|
||||
"upscale_diffusion": "диффузии_апскейла",
|
||||
"upscaling": "апскейл",
|
||||
"video": "видео",
|
||||
"video_models": "видеомодели"
|
||||
},
|
||||
"nodeTemplates": {
|
||||
"enterName": "Введите имя",
|
||||
"enterName": "Введите название",
|
||||
"saveAsTemplate": "Сохранить как шаблон"
|
||||
},
|
||||
"notSupported": {
|
||||
"continue": "Продолжить",
|
||||
"continueTooltip": "Я уверен, что мое устройство поддерживается",
|
||||
"continueTooltip": "Я уверен, что моё устройство поддерживается",
|
||||
"learnMore": "Узнать больше",
|
||||
"message": "Поддерживаются только следующие устройства:",
|
||||
"reportIssue": "Сообщить о проблеме",
|
||||
@@ -437,14 +464,14 @@
|
||||
"name": "Использовать классическую систему кэширования"
|
||||
},
|
||||
"cache-lru": {
|
||||
"name": "Использовать LRU кэширование с максимальным количеством N кэшированных результатов узлов.",
|
||||
"name": "Использовать LRU кэширование с максимальным количеством N кэшированных результатов нод.",
|
||||
"tooltip": "Может использовать больше ОЗУ/ВРП."
|
||||
},
|
||||
"cpu-vae": {
|
||||
"name": "Запуск VAE на CPU"
|
||||
},
|
||||
"cross-attention-method": {
|
||||
"name": "Метод перекрестного внимания"
|
||||
"name": "Метод перекрёстного внимания"
|
||||
},
|
||||
"cuda-device": {
|
||||
"name": "Индекс устройства CUDA для использования"
|
||||
@@ -463,7 +490,7 @@
|
||||
"name": "Индекс устройства DirectML"
|
||||
},
|
||||
"disable-all-custom-nodes": {
|
||||
"name": "Отключить загрузку всех пользовательских узлов."
|
||||
"name": "Отключить загрузку всех пользовательских нод."
|
||||
},
|
||||
"disable-ipex-optimize": {
|
||||
"name": "Отключить оптимизацию IPEX"
|
||||
@@ -479,7 +506,7 @@
|
||||
"name": "Отключить оптимизацию xFormers"
|
||||
},
|
||||
"dont-print-server": {
|
||||
"name": "Не выводить вывод сервера в консоль."
|
||||
"name": "Не показывать вывод сервера в консоль."
|
||||
},
|
||||
"dont-upcast-attention": {
|
||||
"name": "Предотвратить повышение внимания"
|
||||
@@ -526,7 +553,7 @@
|
||||
},
|
||||
"reserve-vram": {
|
||||
"name": "Резервируемая VRAM (ГБ)",
|
||||
"tooltip": "Установите количество VRAM в ГБ, которое вы хотите зарезервировать для использования вашей ОС/другими программами. По умолчанию резервируется определенное количество в зависимости от вашей ОС."
|
||||
"tooltip": "Установите количество VRAM в ГБ, которое вы хотите зарезервировать для использования вашей ОС/другими программами. По умолчанию резервируется определённое количество в зависимости от вашей ОС."
|
||||
},
|
||||
"text-encoder-precision": {
|
||||
"name": "Точность текстового кодировщика",
|
||||
@@ -554,10 +581,10 @@
|
||||
"openLogs": "Открыть логи",
|
||||
"process": {
|
||||
"error": "Не удалось запустить ComfyUI Desktop",
|
||||
"initial-state": "Загрузка...",
|
||||
"python-setup": "Настройка окружения Python...",
|
||||
"ready": "Завершение...",
|
||||
"starting-server": "Запуск сервера ComfyUI..."
|
||||
"initial-state": "Загрузка…",
|
||||
"python-setup": "Настройка окружения Python…",
|
||||
"ready": "Завершение…",
|
||||
"starting-server": "Запуск сервера ComfyUI…"
|
||||
},
|
||||
"reinstall": "Переустановить",
|
||||
"reportIssue": "Сообщить о проблеме",
|
||||
@@ -570,7 +597,7 @@
|
||||
"Canvas": "Холст",
|
||||
"ColorPalette": "Цветовая палитра",
|
||||
"Comfy": "Comfy",
|
||||
"Comfy-Desktop": "Comfy рабочий стол",
|
||||
"Comfy-Desktop": "Десктопный Comfy",
|
||||
"CustomColorPalettes": "Пользовательские цветовые палитры",
|
||||
"DevMode": "Режим разработчика",
|
||||
"EditTokenWeight": "Редактировать вес токена",
|
||||
@@ -587,11 +614,11 @@
|
||||
"Menu": "Меню",
|
||||
"ModelLibrary": "Библиотека моделей",
|
||||
"NewEditor": "Новый редактор",
|
||||
"Node": "Узел",
|
||||
"Node Search Box": "Поисковая строка узлов",
|
||||
"Node Widget": "Виджет узла",
|
||||
"NodeInputConversionSubmenus": "Подменю преобразования ввода узла",
|
||||
"NodeLibrary": "Библиотека узлов",
|
||||
"Node": "Нода",
|
||||
"Node Search Box": "Поисковая строка нод",
|
||||
"Node Widget": "Виджет ноды",
|
||||
"NodeInputConversionSubmenus": "Подменю преобразования ввода ноды",
|
||||
"NodeLibrary": "Библиотека нод",
|
||||
"Pointer": "Указатель",
|
||||
"Queue": "Очередь",
|
||||
"QueueButton": "Кнопка очереди",
|
||||
@@ -611,7 +638,7 @@
|
||||
"logout": "Выйти",
|
||||
"modelLibrary": "Библиотека моделей",
|
||||
"newBlankWorkflow": "Создайте новый пустой рабочий процесс",
|
||||
"nodeLibrary": "Библиотека узлов",
|
||||
"nodeLibrary": "Библиотека нод",
|
||||
"nodeLibraryTab": {
|
||||
"sortOrder": "Порядок сортировки"
|
||||
},
|
||||
@@ -637,7 +664,7 @@
|
||||
"confirmOverwriteTitle": "Перезаписать существующий файл?",
|
||||
"deleteFailed": "Попытка удалить рабочий процесс не удалась.",
|
||||
"deleteFailedTitle": "Не удалось удалить",
|
||||
"deleted": "Рабочий процесс удален",
|
||||
"deleted": "Рабочий процесс удалён",
|
||||
"dirtyClose": "Файлы ниже были изменены. Вы хотите сохранить их перед закрытием?",
|
||||
"dirtyCloseTitle": "Сохранить изменения?",
|
||||
"workflowTreeType": {
|
||||
@@ -659,12 +686,12 @@
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
"default": "Image Generation",
|
||||
"default": "Генерация изображений",
|
||||
"flux_schnell": "Flux Schnell",
|
||||
"image2image": "Image to Image",
|
||||
"upscale": "2 Pass Upscale"
|
||||
"image2image": "Изображение в изображение",
|
||||
"upscale": "2-этапный апскейл"
|
||||
},
|
||||
"title": "Начните работу с шаблона"
|
||||
"title": "Начните с шаблона"
|
||||
},
|
||||
"userSelect": {
|
||||
"enterUsername": "Введите имя пользователя",
|
||||
@@ -678,7 +705,7 @@
|
||||
"title": "Добро пожаловать в ComfyUI"
|
||||
},
|
||||
"workflowService": {
|
||||
"enterFilename": "Введите имя файла",
|
||||
"enterFilename": "Введите название файла",
|
||||
"exportWorkflow": "Экспорт рабочего процесса",
|
||||
"saveWorkflow": "Сохранить рабочий процесс"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,14 +20,14 @@
|
||||
"name": "Включить обрезку элементов DOM (включение может снизить производительность)"
|
||||
},
|
||||
"Comfy_DevMode": {
|
||||
"name": "Включить параметры режима разработчика (сохранение API и т.д.)"
|
||||
"name": "Включить параметры режима разработчика (сохранение API и т. д.)"
|
||||
},
|
||||
"Comfy_DisableFloatRounding": {
|
||||
"name": "Отключить округление по умолчанию для плавающих виджетов.",
|
||||
"tooltip": "(требуется перезагрузка страницы) Невозможно отключить округление, если оно установлено узлом на сервере."
|
||||
},
|
||||
"Comfy_DisableSliders": {
|
||||
"name": "Отключить ползунки виджетов узлов"
|
||||
"name": "Отключить ползунки виджетов нод"
|
||||
},
|
||||
"Comfy_EditAttention_Delta": {
|
||||
"name": "Точность Ctrl+вверх/вниз"
|
||||
@@ -49,7 +49,7 @@
|
||||
"name": "Показать меню холста графа"
|
||||
},
|
||||
"Comfy_Graph_CtrlShiftZoom": {
|
||||
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Перетаскивание)"
|
||||
"name": "Включить быстрый зум с помощью сочетания клавиш (Ctrl + Shift + Колёсико мыши)"
|
||||
},
|
||||
"Comfy_Graph_LinkMarkers": {
|
||||
"name": "Маркер середины ссылки",
|
||||
@@ -63,10 +63,10 @@
|
||||
"name": "Скорость зума холста"
|
||||
},
|
||||
"Comfy_GroupSelectedNodes_Padding": {
|
||||
"name": "Отступ для выбранных узлов группы"
|
||||
"name": "Отступ для выбранных нод группы"
|
||||
},
|
||||
"Comfy_Group_DoubleClickTitleToEdit": {
|
||||
"name": "Дважды щелкните по заголовку группы, чтобы редактировать"
|
||||
"name": "Дважды щёлкните по заголовку группы, чтобы редактировать"
|
||||
},
|
||||
"Comfy_LinkRelease_Action": {
|
||||
"name": "Действие при отпускании ссылки (без модификатора)",
|
||||
@@ -98,11 +98,11 @@
|
||||
},
|
||||
"Comfy_MaskEditor_BrushAdjustmentSpeed": {
|
||||
"name": "Множитель скорости регулировки кисти",
|
||||
"tooltip": "Управляет тем, как быстро изменяются размер и жесткость кисти при регулировке. Более высокие значения означают более быстрые изменения."
|
||||
"tooltip": "Управляет тем, как быстро изменяются размер и жёсткость кисти при регулировке. Более высокие значения означают более быстрые изменения."
|
||||
},
|
||||
"Comfy_MaskEditor_UseDominantAxis": {
|
||||
"name": "Закрепить регулировку кисти по доминирующей оси",
|
||||
"tooltip": "При включении регулировки кисти будет влиять только на размер ИЛИ жесткость в зависимости от того, в каком направлении вы двигаетесь больше"
|
||||
"tooltip": "При включении регулировки кисти будет влиять только на размер или жёсткость в зависимости от того, в каком направлении вы двигаетесь больше"
|
||||
},
|
||||
"Comfy_MaskEditor_UseNewEditor": {
|
||||
"name": "Использовать новый редактор масок",
|
||||
@@ -113,29 +113,29 @@
|
||||
"tooltip": "Если true, все папки будут загружены, как только вы откроете библиотеку моделей (это может вызвать задержки при загрузке). Если false, корневые папки моделей будут загружены только после нажатия на них."
|
||||
},
|
||||
"Comfy_ModelLibrary_NameFormat": {
|
||||
"name": "Какое имя отображать в древовидном представлении библиотеки моделей",
|
||||
"name": "Какое название отображать в древовидном представлении библиотеки моделей",
|
||||
"options": {
|
||||
"filename": "имя файла",
|
||||
"filename": "название файла",
|
||||
"title": "название"
|
||||
},
|
||||
"tooltip": "Выберите \"имя файла\", чтобы отобразить упрощенный вид сырого имени файла (без директории или расширения \".safetensors\") в списке моделей. Выберите \"название\", чтобы отобразить настраиваемое название метаданных модели."
|
||||
"tooltip": "Выберите \"название файла\", чтобы отобразить упрощённый вид сырого названия файла (без директории или расширения \".safetensors\") в списке моделей. Выберите \"название\", чтобы отобразить настраиваемое название метаданных модели."
|
||||
},
|
||||
"Comfy_NodeBadge_NodeIdBadgeMode": {
|
||||
"name": "Режим значка ID узла",
|
||||
"name": "Режим значка ID ноды",
|
||||
"options": {
|
||||
"None": "Нет",
|
||||
"Show all": "Показать все"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeLifeCycleBadgeMode": {
|
||||
"name": "Режим значка жизненного цикла узла",
|
||||
"name": "Режим значка жизненного цикла ноды",
|
||||
"options": {
|
||||
"None": "Нет",
|
||||
"Show all": "Показать все"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeBadge_NodeSourceBadgeMode": {
|
||||
"name": "Режим значка источника узла",
|
||||
"name": "Режим значка источника ноды",
|
||||
"options": {
|
||||
"Hide built-in": "Скрыть встроенные",
|
||||
"None": "Нет",
|
||||
@@ -143,63 +143,63 @@
|
||||
}
|
||||
},
|
||||
"Comfy_NodeInputConversionSubmenus": {
|
||||
"name": "В контекстном меню узла разместите элементы, которые конвертируют между вводом/виджетом в подменю."
|
||||
"name": "В контекстном меню ноды разместите элементы, которые конвертируют между вводом/виджетом в подменю."
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl": {
|
||||
"name": "Реализация поискового поля узлов",
|
||||
"name": "Реализация поискового поля нод",
|
||||
"options": {
|
||||
"default": "по умолчанию",
|
||||
"litegraph (legacy)": "litegraph (устаревший)"
|
||||
}
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_NodePreview": {
|
||||
"name": "Предварительный просмотр узла",
|
||||
"name": "Предварительный просмотр ноды",
|
||||
"tooltip": "Применяется только к стандартной реализации"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowCategory": {
|
||||
"name": "Показать категорию узла в результатах поиска",
|
||||
"name": "Показать категорию ноды в результатах поиска",
|
||||
"tooltip": "Применяется только к стандартной реализации"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowIdName": {
|
||||
"name": "Показать имя ID узла в результатах поиска",
|
||||
"name": "Показать название ID ноды в результатах поиска",
|
||||
"tooltip": "Применяется только к стандартной реализации"
|
||||
},
|
||||
"Comfy_NodeSearchBoxImpl_ShowNodeFrequency": {
|
||||
"name": "Показать частоту узла в результатах поиска",
|
||||
"name": "Показать частоту ноды в результатах поиска",
|
||||
"tooltip": "Применяется только к стандартной реализации"
|
||||
},
|
||||
"Comfy_NodeSuggestions_number": {
|
||||
"name": "Количество предложений узлов",
|
||||
"name": "Количество предложенных нод",
|
||||
"tooltip": "Только для поля поиска litegraph/контекстного меню"
|
||||
},
|
||||
"Comfy_Node_AutoSnapLinkToSlot": {
|
||||
"name": "Автоматически привязывать ссылку к слоту узла",
|
||||
"tooltip": "При перетаскивании ссылки над узлом ссылка автоматически привязывается к подходящему входному слоту узла"
|
||||
"name": "Автоматически привязывать ссылку к слоту ноды",
|
||||
"tooltip": "При перетаскивании ссылки над нодой ссылка автоматически привязывается к подходящему входному слоту ноды"
|
||||
},
|
||||
"Comfy_Node_BypassAllLinksOnDelete": {
|
||||
"name": "Сохранить все ссылки при удалении узлов",
|
||||
"tooltip": "При удалении узла попытаться переподключить все его входные и выходные ссылки (обходя удаленный узел)"
|
||||
"name": "Сохранить все ссылки при удалении нод",
|
||||
"tooltip": "При удалении ноды попытаться переподключить все её входные и выходные ссылки (обходя удалённую ноду)"
|
||||
},
|
||||
"Comfy_Node_DoubleClickTitleToEdit": {
|
||||
"name": "Дважды щелкните по заголовку узла, чтобы редактировать"
|
||||
"name": "Дважды щёлкните по заголовку ноды, чтобы редактировать"
|
||||
},
|
||||
"Comfy_Node_MiddleClickRerouteNode": {
|
||||
"name": "Средний щелчок создает новый узел перенаправления"
|
||||
"name": "Средний щелчок создаёт новую ноду перенаправления"
|
||||
},
|
||||
"Comfy_Node_Opacity": {
|
||||
"name": "Непрозрачность узла"
|
||||
"name": "Непрозрачность ноды"
|
||||
},
|
||||
"Comfy_Node_ShowDeprecated": {
|
||||
"name": "Показать устаревшие узлы в поиске",
|
||||
"tooltip": "Устаревшие узлы по умолчанию скрыты в интерфейсе, но остаются функциональными в существующих рабочих процессах, которые их используют."
|
||||
"name": "Показать устаревшие ноды в поиске",
|
||||
"tooltip": "Устаревшие ноды по умолчанию скрыты в интерфейсе, но остаются функциональными в существующих рабочих процессах, которые их используют."
|
||||
},
|
||||
"Comfy_Node_ShowExperimental": {
|
||||
"name": "Показать экспериментальные узлы в поиске",
|
||||
"tooltip": "Экспериментальные узлы помечены как таковые в интерфейсе и могут подвергаться значительным изменениям или удалению в будущих версиях. Используйте с осторожностью в производственных рабочих процессах"
|
||||
"name": "Показать экспериментальные ноды в поиске",
|
||||
"tooltip": "Экспериментальные ноды помечены как таковые в интерфейсе и могут подвергаться значительным изменениям или удалению в будущих версиях. Используйте с осторожностью в производственных рабочих процессах"
|
||||
},
|
||||
"Comfy_Node_SnapHighlightsNode": {
|
||||
"name": "Подсветка узла при привязке",
|
||||
"tooltip": "При перетаскивании ссылки над узлом с подходящим входным слотом, узел подсвечивается"
|
||||
"name": "Подсветка ноды при привязке",
|
||||
"tooltip": "При перетаскивании ссылки над нодой с подходящим входным слотом, нода подсвечивается"
|
||||
},
|
||||
"Comfy_Pointer_ClickBufferTime": {
|
||||
"name": "Задержка дрейфа щелчка указателя",
|
||||
@@ -218,7 +218,7 @@
|
||||
"tooltip": "При отображении предварительного просмотра в виджете изображения, преобразуйте его в легковесное изображение, например, webp, jpeg, webp;50 и т.д."
|
||||
},
|
||||
"Comfy_PromptFilename": {
|
||||
"name": "Запрос имени файла при сохранении рабочего процесса"
|
||||
"name": "Запрос названия файла при сохранении рабочего процесса"
|
||||
},
|
||||
"Comfy_QueueButton_BatchCountLimit": {
|
||||
"name": "Ограничение количества партий",
|
||||
@@ -230,7 +230,7 @@
|
||||
},
|
||||
"Comfy_RerouteBeta": {
|
||||
"name": "Участвовать в бета-тестировании перенаправления",
|
||||
"tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно - перенаправления скрыты."
|
||||
"tooltip": "Включает новые нативные перенаправления.\n\nПеренаправления можно добавлять, удерживая alt и перетаскивая от линии ссылки или в меню ссылки.\n\nОтключение этой опции не разрушительно — перенаправления скрыты."
|
||||
},
|
||||
"Comfy_Sidebar_Location": {
|
||||
"name": "Расположение боковой панели",
|
||||
@@ -248,7 +248,7 @@
|
||||
},
|
||||
"Comfy_SnapToGrid_GridSize": {
|
||||
"name": "Размер сетки привязки",
|
||||
"tooltip": "При перетаскивании и изменении размера узлов, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
|
||||
"tooltip": "При перетаскивании и изменении размера нод, удерживая shift, они будут выровнены по сетке, это контролирует размер этой сетки."
|
||||
},
|
||||
"Comfy_TextareaWidget_FontSize": {
|
||||
"name": "Размер шрифта виджета текстовой области"
|
||||
@@ -268,8 +268,8 @@
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "Проверка определений узлов (медленно)",
|
||||
"tooltip": "Рекомендуется для разработчиков узлов. Это проверит все определения узлов при запуске."
|
||||
"name": "Проверка определений нод (медленно)",
|
||||
"tooltip": "Рекомендуется для разработчиков нод. Это проверит все определения нод при запуске."
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "Проверка рабочих процессов"
|
||||
@@ -292,10 +292,10 @@
|
||||
"name": "Показать предупреждение об отсутствующих моделях"
|
||||
},
|
||||
"Comfy_Workflow_ShowMissingNodesWarning": {
|
||||
"name": "Показать предупреждение об отсутствующих узлах"
|
||||
"name": "Показать предупреждение об отсутствующих нодах"
|
||||
},
|
||||
"Comfy_Workflow_SortNodeIdOnSave": {
|
||||
"name": "Сортировать ID узлов при сохранении рабочего процесса"
|
||||
"name": "Сортировать ID нод при сохранении рабочего процесса"
|
||||
},
|
||||
"Comfy_Workflow_WorkflowTabsPosition": {
|
||||
"name": "Положение открытых рабочих процессов",
|
||||
@@ -307,7 +307,7 @@
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Максимум FPS",
|
||||
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счет плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
|
||||
"tooltip": "Максимальное количество кадров в секунду, которое холст может рендерить. Ограничивает использование GPU за счёт плавности. Если 0, используется частота обновления экрана. По умолчанию: 0"
|
||||
},
|
||||
"pysssss_SnapToGrid": {
|
||||
"name": "Всегда привязываться к сетке"
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
"Comfy-Desktop_OpenDevTools": {
|
||||
"label": "打开开发者工具"
|
||||
},
|
||||
"Comfy-Desktop_OpenFeedbackPage": {
|
||||
"label": "反馈"
|
||||
},
|
||||
"Comfy-Desktop_OpenUserGuide": {
|
||||
"label": "桌面用户指南"
|
||||
},
|
||||
"Comfy-Desktop_Quit": {
|
||||
"label": "退出"
|
||||
},
|
||||
"Comfy-Desktop_Reinstall": {
|
||||
"label": "重新安装"
|
||||
},
|
||||
@@ -83,6 +83,9 @@
|
||||
"Comfy_ExportWorkflowAPI": {
|
||||
"label": "导出工作流(API格式)"
|
||||
},
|
||||
"Comfy_Feedback": {
|
||||
"label": "反馈"
|
||||
},
|
||||
"Comfy_Graph_FitGroupToContents": {
|
||||
"label": "适应节点框到内容"
|
||||
},
|
||||
@@ -107,6 +110,9 @@
|
||||
"Comfy_Help_OpenComfyUIDocs": {
|
||||
"label": "打开ComfyUI文档"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIForum": {
|
||||
"label": "打开 Comfy-Org 论坛"
|
||||
},
|
||||
"Comfy_Help_OpenComfyUIIssues": {
|
||||
"label": "打开ComfyUI问题"
|
||||
},
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"WEBCAM": "摄像头"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "有未保存的工作流程开启;任何未保存的更改都将丢失。忽略此警告并退出?",
|
||||
"confirmReinstall": "这将清除您的 extra_models_config.yaml 文件,并重新开始安装。您确定吗?",
|
||||
"quit": "退出",
|
||||
"reinstall": "重新安装"
|
||||
},
|
||||
"downloadGit": {
|
||||
@@ -87,6 +89,7 @@
|
||||
"experimental": "测试版",
|
||||
"export": "导出",
|
||||
"extensionName": "扩展名称",
|
||||
"feedback": "反馈",
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"goToNode": "转到节点",
|
||||
@@ -244,13 +247,35 @@
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "跟进联系我",
|
||||
"feedbackTitle": "通过提供反馈帮助我们改进ComfyUI",
|
||||
"helpFix": "帮助修复这个",
|
||||
"notifyResolve": "解决时通知我",
|
||||
"provideAdditionalDetails": "提供额外的详细信息(可选)",
|
||||
"provideEmail": "提供您的电子邮件(可选)",
|
||||
"rating": "评分",
|
||||
"stackTrace": "堆栈跟踪",
|
||||
"submitErrorReport": "提交错误报告(可选)",
|
||||
"systemStats": "系统状态"
|
||||
"systemStats": "系统状态",
|
||||
"validation": {
|
||||
"invalidEmail": "请输入有效的电子邮件地址",
|
||||
"maxLength": "消息过长"
|
||||
}
|
||||
},
|
||||
"maintenance": {
|
||||
"None": "无",
|
||||
"OK": "确定",
|
||||
"Skipped": "跳过",
|
||||
"allOk": "未检测到任何问题。",
|
||||
"confirmTitle": "你确定吗?",
|
||||
"detected": "检测到",
|
||||
"error": {
|
||||
"defaultDescription": "运行维护任务时发生错误。",
|
||||
"taskFailed": "任务运行失败。",
|
||||
"toastTitle": "任务错误"
|
||||
},
|
||||
"refreshing": "刷新中",
|
||||
"showManual": "显示维护任务",
|
||||
"status": "状态"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自动执行",
|
||||
@@ -288,6 +313,7 @@
|
||||
"Collapse/Expand Selected Nodes": "折叠/展开选定节点",
|
||||
"Comfy-Org Discord": "Comfy-Org Discord",
|
||||
"ComfyUI Docs": "ComfyUI 文档",
|
||||
"ComfyUI Forum": "ComfyUI 论坛",
|
||||
"ComfyUI Issues": "ComfyUI 问题",
|
||||
"Convert selected nodes to group node": "将选中节点转换为组节点",
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
@@ -295,9 +321,9 @@
|
||||
"Edit": "编辑",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"Feedback": "反馈",
|
||||
"Fit Group To Contents": "适应组内容",
|
||||
"Fit view to selected nodes": "适应视图到选中节点",
|
||||
"Give Feedback": "提供反馈",
|
||||
"Group Selected Nodes": "将选中节点转换为组节点",
|
||||
"Help": "帮助",
|
||||
"Interrupt": "中断",
|
||||
@@ -319,6 +345,7 @@
|
||||
"Previous Opened Workflow": "上一个打开的工作流",
|
||||
"Queue Prompt": "执行提示词",
|
||||
"Queue Prompt (Front)": "执行提示词 (优先执行)",
|
||||
"Quit": "退出",
|
||||
"Redo": "重做",
|
||||
"Refresh Node Definitions": "刷新节点定义",
|
||||
"Reinstall": "重新安装",
|
||||
|
||||
@@ -104,6 +104,12 @@ const router = createRouter({
|
||||
name: 'DesktopStartView',
|
||||
component: () => import('@/views/DesktopStartView.vue'),
|
||||
beforeEnter: guardElectronAccess
|
||||
},
|
||||
{
|
||||
path: 'maintenance',
|
||||
name: 'MaintenanceView',
|
||||
component: () => import('@/views/MaintenanceView.vue'),
|
||||
beforeEnter: guardElectronAccess
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -942,7 +942,7 @@ export class ComfyApp {
|
||||
|
||||
api.addEventListener('execution_error', ({ detail }) => {
|
||||
this.lastExecutionError = detail
|
||||
useDialogService().showExecutionErrorDialog(detail)
|
||||
useDialogService().showExecutionErrorDialog({ error: detail })
|
||||
this.canvas.draw(true, true)
|
||||
})
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ LGraphCanvas.prototype.computeVisibleNodes = function (): LGraphNode[] {
|
||||
w.element.hidden = actualHidden
|
||||
w.element.style.display = actualHidden ? 'none' : null
|
||||
if (actualHidden && !wasHidden) {
|
||||
w.options.onHide?.(w)
|
||||
w.options.onHide?.(w as DOMWidget<HTMLElement, object>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
|
||||
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
@@ -8,8 +9,6 @@ import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
|
||||
import type { ExecutionErrorWsMessage } from '@/types/apiTypes'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
export type ConfirmationDialogType =
|
||||
| 'default'
|
||||
@@ -20,10 +19,9 @@ export type ConfirmationDialogType =
|
||||
|
||||
export const useDialogService = () => {
|
||||
const dialogStore = useDialogStore()
|
||||
function showLoadWorkflowWarning(props: {
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
[key: string]: any
|
||||
}) {
|
||||
function showLoadWorkflowWarning(
|
||||
props: InstanceType<typeof LoadWorkflowWarning>['$props']
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-load-workflow-warning',
|
||||
component: LoadWorkflowWarning,
|
||||
@@ -31,11 +29,9 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showMissingModelsWarning(props: {
|
||||
missingModels: any[]
|
||||
paths: Record<string, string[]>
|
||||
[key: string]: any
|
||||
}) {
|
||||
function showMissingModelsWarning(
|
||||
props: InstanceType<typeof MissingModelsWarning>['$props']
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-missing-models-warning',
|
||||
component: MissingModelsWarning,
|
||||
@@ -67,21 +63,34 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showExecutionErrorDialog(error: ExecutionErrorWsMessage) {
|
||||
function showExecutionErrorDialog(
|
||||
props: InstanceType<typeof ExecutionErrorDialogContent>['$props']
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-execution-error',
|
||||
component: ExecutionErrorDialogContent,
|
||||
props: {
|
||||
error
|
||||
}
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
function showTemplateWorkflowsDialog() {
|
||||
function showTemplateWorkflowsDialog(
|
||||
props: InstanceType<typeof TemplateWorkflowsContent>['$props'] = {}
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-template-workflows',
|
||||
title: t('templateWorkflows.title'),
|
||||
component: TemplateWorkflowsContent
|
||||
component: TemplateWorkflowsContent,
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
function showIssueReportDialog(
|
||||
props: InstanceType<typeof IssueReportDialogContent>['$props']
|
||||
) {
|
||||
dialogStore.showDialog({
|
||||
key: 'global-issue-report',
|
||||
component: IssueReportDialogContent,
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,6 +171,7 @@ export const useDialogService = () => {
|
||||
showAboutDialog,
|
||||
showExecutionErrorDialog,
|
||||
showTemplateWorkflowsDialog,
|
||||
showIssueReportDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -367,7 +367,6 @@ export const useLitegraphService = () => {
|
||||
const w = node.widgets[node.widgets.length - 1]
|
||||
shiftY = w.last_y
|
||||
if (w.computeSize) {
|
||||
// @ts-expect-error requires 1 param
|
||||
shiftY += w.computeSize()[1] + 4
|
||||
// @ts-expect-error computedHeight only exists for DOMWidget
|
||||
} else if (w.computedHeight) {
|
||||
|
||||
175
src/stores/maintenanceTaskStore.ts
Normal file
175
src/stores/maintenanceTaskStore.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import type { InstallValidation } from '@comfyorg/comfyui-electron-types'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { DESKTOP_MAINTENANCE_TASKS } from '@/constants/desktopMaintenanceTasks'
|
||||
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
/** State of a maintenance task, managed by the maintenance task store. */
|
||||
export type MaintenanceTaskState = 'warning' | 'error' | 'OK' | 'skipped'
|
||||
|
||||
// Type not exported by API
|
||||
type ValidationState = InstallValidation['basePath']
|
||||
// Add index to API type
|
||||
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
|
||||
|
||||
/** State of a maintenance task, managed by the maintenance task store. */
|
||||
export class MaintenanceTaskRunner {
|
||||
constructor(readonly task: MaintenanceTask) {}
|
||||
|
||||
private _state?: MaintenanceTaskState
|
||||
/** The current state of the task. Setter also controls {@link resolved} as a side-effect. */
|
||||
get state() {
|
||||
return this._state
|
||||
}
|
||||
|
||||
/** Updates the task state and {@link resolved} status. */
|
||||
setState(value: MaintenanceTaskState) {
|
||||
// Mark resolved
|
||||
if (this._state === 'error' && value === 'OK') this.resolved = true
|
||||
// Mark unresolved (if previously resolved)
|
||||
if (value === 'error') this.resolved &&= false
|
||||
|
||||
this._state = value
|
||||
}
|
||||
|
||||
/** `true` if the task has been resolved (was `error`, now `OK`). This is a side-effect of the {@link state} setter. */
|
||||
resolved?: boolean
|
||||
|
||||
/** Whether the task state is currently being refreshed. */
|
||||
refreshing?: boolean
|
||||
/** Whether the task is currently running. */
|
||||
executing?: boolean
|
||||
/** The error message that occurred when the task failed. */
|
||||
error?: string
|
||||
|
||||
update(update: IndexedUpdate) {
|
||||
const state = update[this.task.id]
|
||||
|
||||
this.refreshing = state === undefined
|
||||
if (state) this.setState(state)
|
||||
}
|
||||
|
||||
finaliseUpdate(update: IndexedUpdate) {
|
||||
this.refreshing = false
|
||||
this.setState(update[this.task.id] ?? 'skipped')
|
||||
}
|
||||
|
||||
/** Wraps the execution of a maintenance task, updating state and rethrowing errors. */
|
||||
async execute(task: MaintenanceTask) {
|
||||
try {
|
||||
this.executing = true
|
||||
const success = await task.execute()
|
||||
if (!success) return false
|
||||
|
||||
this.error = undefined
|
||||
return true
|
||||
} catch (error) {
|
||||
this.error = (error as Error)?.message
|
||||
throw error
|
||||
} finally {
|
||||
this.executing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User-initiated maintenance tasks. Currently only used by the desktop app maintenance view.
|
||||
*
|
||||
* Includes running state, task list, and execution / refresh logic.
|
||||
* @returns The maintenance task store
|
||||
*/
|
||||
export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
|
||||
/** Refresh should run for at least this long, even if it completes much faster. Ensures refresh feels like it is doing something. */
|
||||
const electron = electronAPI()
|
||||
|
||||
// Reactive state
|
||||
const isRefreshing = ref(false)
|
||||
const isRunningTerminalCommand = computed(() =>
|
||||
tasks.value
|
||||
.filter((task) => task.usesTerminal)
|
||||
.some((task) => getRunner(task)?.executing)
|
||||
)
|
||||
const isRunningInstallationFix = computed(() =>
|
||||
tasks.value
|
||||
.filter((task) => task.isInstallationFix)
|
||||
.some((task) => getRunner(task)?.executing)
|
||||
)
|
||||
|
||||
// Task list
|
||||
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
|
||||
|
||||
const taskStates = ref(
|
||||
new Map<MaintenanceTask['id'], MaintenanceTaskRunner>(
|
||||
DESKTOP_MAINTENANCE_TASKS.map((x) => [x.id, new MaintenanceTaskRunner(x)])
|
||||
)
|
||||
)
|
||||
|
||||
/** True if any tasks are in an error state. */
|
||||
const anyErrors = computed(() =>
|
||||
tasks.value.some((task) => getRunner(task).state === 'error')
|
||||
)
|
||||
|
||||
/**
|
||||
* Returns the matching state object for a task.
|
||||
* @param task Task to get the matching state object for
|
||||
* @returns The state object for this task
|
||||
*/
|
||||
const getRunner = (task: MaintenanceTask) => taskStates.value.get(task.id)!
|
||||
|
||||
/**
|
||||
* Updates the task list with the latest validation state.
|
||||
* @param validationUpdate Update details passed in by electron
|
||||
*/
|
||||
const processUpdate = (validationUpdate: InstallValidation) => {
|
||||
const update = validationUpdate as IndexedUpdate
|
||||
isRefreshing.value = true
|
||||
|
||||
// Update each task state
|
||||
for (const task of tasks.value) {
|
||||
getRunner(task).update(update)
|
||||
}
|
||||
|
||||
// Final update
|
||||
if (!update.inProgress && isRefreshing.value) {
|
||||
isRefreshing.value = false
|
||||
|
||||
for (const task of tasks.value) {
|
||||
getRunner(task).finaliseUpdate(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears the resolved status of tasks (when changing filters) */
|
||||
const clearResolved = () => {
|
||||
for (const task of tasks.value) {
|
||||
getRunner(task).resolved &&= false
|
||||
}
|
||||
}
|
||||
|
||||
/** @todo Refreshes Electron tasks only. */
|
||||
const refreshDesktopTasks = async () => {
|
||||
isRefreshing.value = true
|
||||
console.log('Refreshing desktop tasks')
|
||||
await electron.Validation.validateInstallation(processUpdate)
|
||||
}
|
||||
|
||||
const execute = async (task: MaintenanceTask) => {
|
||||
return getRunner(task).execute(task)
|
||||
}
|
||||
|
||||
return {
|
||||
tasks,
|
||||
isRefreshing,
|
||||
isRunningTerminalCommand,
|
||||
isRunningInstallationFix,
|
||||
execute,
|
||||
getRunner,
|
||||
processUpdate,
|
||||
clearResolved,
|
||||
/** True if any tasks are in an error state. */
|
||||
anyErrors,
|
||||
refreshDesktopTasks
|
||||
}
|
||||
})
|
||||
0
src/types/desktop/index.d.ts
vendored
Normal file
0
src/types/desktop/index.d.ts
vendored
Normal file
50
src/types/desktop/maintenanceTypes.ts
Normal file
50
src/types/desktop/maintenanceTypes.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { VueSeverity } from '../primeVueTypes'
|
||||
|
||||
interface MaintenanceTaskButton {
|
||||
/** The text to display on the button. */
|
||||
text?: string
|
||||
/** CSS classes used for the button icon, e.g. 'pi pi-external-link' */
|
||||
icon?: string
|
||||
}
|
||||
|
||||
/** A maintenance task, used by the maintenance page. */
|
||||
export interface MaintenanceTask {
|
||||
/** ID string used as i18n key */
|
||||
id: string
|
||||
/** The display name of the task, e.g. Git */
|
||||
name: string
|
||||
/** Short description of the task. */
|
||||
shortDescription?: string
|
||||
/** Description of the task when it is in an error state. */
|
||||
errorDescription?: string
|
||||
/** Description of the task when it is in a warning state. */
|
||||
warningDescription?: string
|
||||
/** Full description of the task when it is in an OK state. */
|
||||
description?: string
|
||||
/** URL to the image to show in card mode. */
|
||||
headerImg?: string
|
||||
/** The button to display on the task card / list item. */
|
||||
button?: MaintenanceTaskButton
|
||||
/** Whether to show a confirmation dialog before running the task. */
|
||||
requireConfirm?: boolean
|
||||
/** The text to display in the confirmation dialog. */
|
||||
confirmText?: string
|
||||
/** Called by onClick to run the actual task. */
|
||||
execute: (args?: unknown[]) => boolean | Promise<boolean>
|
||||
/** Show the button with `severity="danger"` */
|
||||
severity?: VueSeverity
|
||||
/** Whether this task should display the terminal window when run. */
|
||||
usesTerminal?: boolean
|
||||
/** If `true`, successful completion of this task will refresh install validation and automatically continue if successful. */
|
||||
isInstallationFix?: boolean
|
||||
}
|
||||
|
||||
/** The filter options for the maintenance task list. */
|
||||
export interface MaintenanceFilter {
|
||||
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */
|
||||
icon: string
|
||||
/** The text to display on the filter button. */
|
||||
value: string
|
||||
/** The tasks to display when this filter is selected. */
|
||||
tasks: ReadonlyArray<MaintenanceTask>
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
|
||||
|
||||
export interface ReportField {
|
||||
@@ -14,7 +16,7 @@ export interface ReportField {
|
||||
/**
|
||||
* The data associated with this field, sent as part of the report.
|
||||
*/
|
||||
data: Record<string, unknown>
|
||||
getData: () => unknown
|
||||
|
||||
/**
|
||||
* Indicates whether the field requires explicit opt-in from the user
|
||||
@@ -22,3 +24,42 @@ export interface ReportField {
|
||||
*/
|
||||
optIn: boolean
|
||||
}
|
||||
|
||||
export interface IssueReportPanelProps {
|
||||
/**
|
||||
* The type of error being reported. This is used to categorize the error.
|
||||
*/
|
||||
errorType: string
|
||||
|
||||
/**
|
||||
* Which of the default fields to include in the report.
|
||||
*/
|
||||
defaultFields?: DefaultField[]
|
||||
|
||||
/**
|
||||
* Additional fields to include in the report.
|
||||
*/
|
||||
extraFields?: ReportField[]
|
||||
|
||||
/**
|
||||
* Tags that will be added to the report. Tags are used to further categorize the error.
|
||||
*/
|
||||
tags?: Record<string, string>
|
||||
|
||||
/**
|
||||
* The title displayed in the dialog.
|
||||
*/
|
||||
title?: string
|
||||
}
|
||||
|
||||
const checkboxField = z.boolean().optional()
|
||||
export const issueReportSchema = z
|
||||
.object({
|
||||
contactInfo: z.string().email().max(320).optional().or(z.literal('')),
|
||||
details: z.string().max(5_000).optional()
|
||||
})
|
||||
.catchall(checkboxField)
|
||||
.refine((data) => Object.values(data).some((value) => value), {
|
||||
path: ['details']
|
||||
})
|
||||
export type IssueReportFormData = z.infer<typeof issueReportSchema>
|
||||
|
||||
14
src/types/litegraph-augmentation.d.ts
vendored
14
src/types/litegraph-augmentation.d.ts
vendored
@@ -1,10 +1,24 @@
|
||||
import '@comfyorg/litegraph'
|
||||
import type { LLink } from '@comfyorg/litegraph'
|
||||
|
||||
import type { DOMWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyNodeDef } from '@/types/apiTypes'
|
||||
|
||||
import type { NodeId } from './comfyWorkflow'
|
||||
|
||||
/** ComfyUI extensions of litegraph */
|
||||
declare module '@comfyorg/litegraph/dist/types/widgets' {
|
||||
interface IWidgetOptions {
|
||||
/** Currently used by DOM widgets only. Declaring here reduces complexity. */
|
||||
onHide?: (widget: DOMWidget) => void
|
||||
}
|
||||
|
||||
interface IBaseWidget {
|
||||
onRemove?: () => void
|
||||
beforeQueued?: () => unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ComfyUI extensions of litegraph
|
||||
*/
|
||||
|
||||
10
src/types/primeVueTypes.ts
Normal file
10
src/types/primeVueTypes.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/** Button, Tag, etc severity type is 'string' instead of this list. */
|
||||
export type VueSeverity =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'info'
|
||||
| 'warn'
|
||||
| 'help'
|
||||
| 'danger'
|
||||
| 'contrast'
|
||||
@@ -11,6 +11,6 @@ export function electronAPI() {
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
export function showNativeMenu(options?: ElectronContextMenuOptions) {
|
||||
electronAPI()?.showContextMenu(options)
|
||||
export function showNativeMenu(event: MouseEvent) {
|
||||
electronAPI()?.showContextMenu(event as ElectronContextMenuOptions)
|
||||
}
|
||||
|
||||
29
src/utils/refUtil.ts
Normal file
29
src/utils/refUtil.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useTimeout } from '@vueuse/core'
|
||||
import { type Ref, computed, ref, watch } from 'vue'
|
||||
|
||||
/**
|
||||
* Vue boolean ref (writable computed) with one difference: when set to `true` it stays that way for at least {@link minDuration}.
|
||||
* If set to `false` before {@link minDuration} has passed, it uses a timer to delay the change.
|
||||
* @param value The default value to set on this ref
|
||||
* @param minDuration The minimum time that this ref must be `true` for
|
||||
* @returns A custom boolean vue ref with a minimum activation time
|
||||
*/
|
||||
export function useMinLoadingDurationRef(
|
||||
value: Ref<boolean>,
|
||||
minDuration = 250
|
||||
) {
|
||||
const current = ref(value.value)
|
||||
|
||||
const { ready, start } = useTimeout(minDuration, {
|
||||
controls: true,
|
||||
immediate: false
|
||||
})
|
||||
|
||||
watch(value, (newValue) => {
|
||||
if (newValue && !current.value) start()
|
||||
|
||||
current.value = newValue
|
||||
})
|
||||
|
||||
return computed(() => current.value || !ready.value)
|
||||
}
|
||||
@@ -94,6 +94,9 @@ if (isElectron()) {
|
||||
`execution:${task.displayStatus.toLowerCase()}`,
|
||||
1
|
||||
)
|
||||
electronAPI().Events.trackEvent('execution', {
|
||||
status: task.displayStatus.toLowerCase()
|
||||
})
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
|
||||
221
src/views/MaintenanceView.vue
Normal file
221
src/views/MaintenanceView.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark>
|
||||
<div
|
||||
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto overflow-y-auto"
|
||||
>
|
||||
<div class="max-w-screen-sm w-screen m-8 relative">
|
||||
<!-- Header -->
|
||||
<h1 class="backspan pi-wrench text-4xl font-bold">Maintenance</h1>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="w-full flex flex-wrap gap-4 items-center">
|
||||
<span class="grow">
|
||||
Status: <StatusTag :refreshing="isRefreshing" :error="anyErrors" />
|
||||
</span>
|
||||
<div class="flex gap-4 items-center">
|
||||
<SelectButton
|
||||
v-model="displayAsList"
|
||||
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
|
||||
:allow-empty="false"
|
||||
>
|
||||
<template #option="opts"><i :class="opts.option" /></template>
|
||||
</SelectButton>
|
||||
<SelectButton
|
||||
v-model="filter"
|
||||
:options="filterOptions"
|
||||
:allow-empty="false"
|
||||
optionLabel="value"
|
||||
dataKey="value"
|
||||
area-labelledby="custom"
|
||||
@change="clearResolved"
|
||||
>
|
||||
<template #option="opts">
|
||||
<i :class="opts.option.icon"></i>
|
||||
<span class="max-sm:hidden">{{ opts.option.value }}</span>
|
||||
</template>
|
||||
</SelectButton>
|
||||
<RefreshButton
|
||||
v-model="isRefreshing"
|
||||
severity="secondary"
|
||||
@refresh="refreshDesktopTasks"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tasks -->
|
||||
<TaskListPanel
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
:filter
|
||||
:displayAsList
|
||||
:isRefreshing
|
||||
/>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between gap-4 flex-row">
|
||||
<Button
|
||||
label="Console Logs"
|
||||
icon="pi pi-desktop"
|
||||
icon-pos="left"
|
||||
severity="secondary"
|
||||
@click="toggleConsoleDrawer"
|
||||
/>
|
||||
<Button
|
||||
label="Continue"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="left"
|
||||
:severity="anyErrors ? 'secondary' : 'primary'"
|
||||
@click="() => completeValidation()"
|
||||
:loading="isRefreshing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
v-model:visible="terminalVisible"
|
||||
header="Terminal"
|
||||
position="bottom"
|
||||
style="height: max(50vh, 34rem)"
|
||||
>
|
||||
<BaseTerminal @created="terminalCreated" />
|
||||
</Drawer>
|
||||
<Toast />
|
||||
</div>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PrimeIcons } from '@primevue/core/api'
|
||||
import Button from 'primevue/button'
|
||||
import Drawer from 'primevue/drawer'
|
||||
import SelectButton from 'primevue/selectbutton'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { Ref, computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import BaseTerminal from '@/components/bottomPanel/tabs/terminal/BaseTerminal.vue'
|
||||
import RefreshButton from '@/components/common/RefreshButton.vue'
|
||||
import StatusTag from '@/components/maintenance/StatusTag.vue'
|
||||
import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
|
||||
import type { useTerminal } from '@/hooks/bottomPanelTabs/useTerminal'
|
||||
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
|
||||
import { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import { useMinLoadingDurationRef } from '@/utils/refUtil'
|
||||
|
||||
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
|
||||
|
||||
const electron = electronAPI()
|
||||
const toast = useToast()
|
||||
const taskStore = useMaintenanceTaskStore()
|
||||
const { clearResolved, processUpdate, refreshDesktopTasks } = taskStore
|
||||
|
||||
const terminalVisible = ref(false)
|
||||
|
||||
// Use a minimum run time to ensure tasks "feel" like they have run
|
||||
const reactiveIsRefreshing = computed(() => taskStore.isRefreshing)
|
||||
/** `true` when waiting on tasks to complete. */
|
||||
const isRefreshing = useMinLoadingDurationRef(reactiveIsRefreshing, 250)
|
||||
|
||||
/** True if any tasks are in an error state. */
|
||||
const anyErrors = computed(() => taskStore.anyErrors)
|
||||
|
||||
/** Whether to display tasks as a list or cards. */
|
||||
const displayAsList = ref(PrimeIcons.TH_LARGE)
|
||||
|
||||
const errorFilter = computed(() =>
|
||||
taskStore.tasks.filter((x) => {
|
||||
const { state, resolved } = taskStore.getRunner(x)
|
||||
return state === 'error' || resolved
|
||||
})
|
||||
)
|
||||
|
||||
const filterOptions = ref([
|
||||
{ icon: PrimeIcons.FILTER_FILL, value: 'All', tasks: taskStore.tasks },
|
||||
{ icon: PrimeIcons.EXCLAMATION_TRIANGLE, value: 'Errors', tasks: errorFilter }
|
||||
])
|
||||
|
||||
/** Filter binding; can be set to show all tasks, or only errors. */
|
||||
const filter = ref<MaintenanceFilter>(filterOptions.value[1])
|
||||
|
||||
/** If valid, leave the validation window. */
|
||||
const completeValidation = async (alertOnFail = true) => {
|
||||
const isValid = await electron.Validation.complete()
|
||||
if (alertOnFail && !isValid) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Unable to continue - errors remain',
|
||||
life: 5_000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const terminalCreated = (
|
||||
{ terminal, useAutoSize }: ReturnType<typeof useTerminal>,
|
||||
root: Ref<HTMLElement>
|
||||
) => {
|
||||
useAutoSize({ root, autoRows: true, autoCols: true })
|
||||
electron.onLogMessage((message: string) => {
|
||||
terminal.write(message)
|
||||
})
|
||||
|
||||
terminal.options.cursorBlink = false
|
||||
terminal.options.cursorStyle = 'bar'
|
||||
terminal.options.cursorInactiveStyle = 'bar'
|
||||
terminal.options.disableStdin = true
|
||||
}
|
||||
|
||||
const toggleConsoleDrawer = () => {
|
||||
terminalVisible.value = !terminalVisible.value
|
||||
}
|
||||
|
||||
// Show terminal when in use
|
||||
watch(
|
||||
() => taskStore.isRunningTerminalCommand,
|
||||
(value) => {
|
||||
terminalVisible.value = value
|
||||
}
|
||||
)
|
||||
|
||||
// If we're running a fix that may resolve all issues, auto-recheck and continue if everything is OK
|
||||
watch(
|
||||
() => taskStore.isRunningInstallationFix,
|
||||
(value, oldValue) => {
|
||||
if (!value && oldValue) completeValidation(false)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
electron.Validation.onUpdate(processUpdate)
|
||||
|
||||
const update = await electron.Validation.getStatus()
|
||||
processUpdate(update)
|
||||
})
|
||||
|
||||
onUnmounted(() => electron.Validation.dispose())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-tag) {
|
||||
--p-tag-gap: 0.375rem;
|
||||
}
|
||||
|
||||
.backspan::before {
|
||||
@apply m-0 absolute text-muted;
|
||||
font-family: 'primeicons';
|
||||
top: -2rem;
|
||||
right: -2rem;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
opacity: 0.02;
|
||||
font-size: min(14rem, 90vw);
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user