mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-09 22:09:58 +00:00
Compare commits
15 Commits
desktop-wi
...
node-group
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f30b96f9 | ||
|
|
c13190cd07 | ||
|
|
00f031e382 | ||
|
|
04153caaf5 | ||
|
|
210bfdeb7d | ||
|
|
ce0726d85e | ||
|
|
dd69f9dc30 | ||
|
|
3f261f0e53 | ||
|
|
3b2cc23f65 | ||
|
|
c50a86b258 | ||
|
|
1a8c2bba42 | ||
|
|
fc09951b3e | ||
|
|
76d5f39607 | ||
|
|
9d3bc0f173 | ||
|
|
d9b350e159 |
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -25,6 +25,8 @@ jobs:
|
||||
id: current_version
|
||||
run: echo ::set-output name=version::$(node -p "require('./package.json').version")
|
||||
- name: Build project
|
||||
env:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
12
.prettierrc
12
.prettierrc
@@ -6,5 +6,13 @@
|
||||
"printWidth": 80,
|
||||
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
}
|
||||
"importOrderSortSpecifiers": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
|
||||
"options": {
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -44,6 +44,18 @@ test.describe('Execution error', () => {
|
||||
const executionError = comfyPage.page.locator('.comfy-error-report')
|
||||
await expect(executionError).toBeVisible()
|
||||
})
|
||||
|
||||
test('Can display Issue Report form', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('execution_error')
|
||||
await comfyPage.queueButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await comfyPage.page.getByLabel('Help Fix This').click()
|
||||
const issueReportForm = comfyPage.page.getByText(
|
||||
'Submit Error Report (Optional)'
|
||||
)
|
||||
await expect(issueReportForm).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Missing models warning', () => {
|
||||
|
||||
@@ -9,6 +9,12 @@ export class Topbar {
|
||||
.allInnerTexts()
|
||||
}
|
||||
|
||||
async getActiveTabName(): Promise<string> {
|
||||
return this.page
|
||||
.locator('.workflow-tabs .p-togglebutton-checked')
|
||||
.innerText()
|
||||
}
|
||||
|
||||
async openSubmenuMobile() {
|
||||
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
|
||||
}
|
||||
|
||||
@@ -615,6 +615,67 @@ test.describe('Load workflow', () => {
|
||||
'single_ksampler_modified.png'
|
||||
)
|
||||
})
|
||||
|
||||
test.describe('Restore all open workflows on reload', () => {
|
||||
let workflowA: string
|
||||
let workflowB: string
|
||||
|
||||
const generateUniqueFilename = (extension = '') =>
|
||||
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
|
||||
workflowA = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowA)
|
||||
workflowB = generateUniqueFilename()
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
|
||||
await comfyPage.menu.topbar.saveWorkflow(workflowB)
|
||||
|
||||
// Wait for localStorage to persist the workflow paths before reloading
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => !!window.localStorage.getItem('Comfy.OpenWorkflowsPaths')
|
||||
)
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
})
|
||||
|
||||
test('Restores topbar workflow tabs after reload', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Topbar'
|
||||
)
|
||||
const tabs = await comfyPage.menu.topbar.getTabNames()
|
||||
const activeWorkflowName = await comfyPage.menu.topbar.getActiveTabName()
|
||||
|
||||
expect(tabs).toEqual(expect.arrayContaining([workflowA, workflowB]))
|
||||
expect(tabs.indexOf(workflowA)).toBeLessThan(tabs.indexOf(workflowB))
|
||||
expect(activeWorkflowName).toEqual(workflowB)
|
||||
})
|
||||
|
||||
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting(
|
||||
'Comfy.Workflow.WorkflowTabsPosition',
|
||||
'Sidebar'
|
||||
)
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
const openWorkflows =
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
const activeWorkflowName =
|
||||
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
|
||||
const workflowPathA = `${workflowA}.json`
|
||||
const workflowPathB = `${workflowB}.json`
|
||||
|
||||
expect(openWorkflows).toEqual(
|
||||
expect.arrayContaining([workflowPathA, workflowPathB])
|
||||
)
|
||||
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
|
||||
openWorkflows.indexOf(workflowPathB)
|
||||
)
|
||||
expect(activeWorkflowName).toEqual(workflowPathB)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Load duplicate workflow', () => {
|
||||
|
||||
2
global.d.ts
vendored
2
global.d.ts
vendored
@@ -1 +1,3 @@
|
||||
declare const __COMFYUI_FRONTEND_VERSION__: string
|
||||
declare const __SENTRY_ENABLED__: boolean
|
||||
declare const __SENTRY_DSN__: string
|
||||
|
||||
95
package-lock.json
generated
95
package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.7.8",
|
||||
"version": "1.7.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.7.8",
|
||||
"version": "1.7.10",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.7",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-table": "^2.10.4",
|
||||
@@ -4462,6 +4463,96 @@
|
||||
"string-argv": "~0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.48.0.tgz",
|
||||
"integrity": "sha512-pLtu0Fa1Ou0v3M1OEO1MB1EONJVmXEGtoTwFRCO1RPQI2ulmkG6BikINClFG5IBpoYKZ33WkEXuM6U5xh+pdZg==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.48.0.tgz",
|
||||
"integrity": "sha512-6PwcJNHVPg0EfZxmN+XxVOClfQpv7MBAweV8t9i5l7VFr8sM/7wPNSeU/cG7iK19Ug9ZEkBpzMOe3G4GXJ5bpw==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.48.0.tgz",
|
||||
"integrity": "sha512-csILVupc5RkrsTrncuUTGmlB56FQSFjXPYWG8I8yBTGlXEJ+o8oTuF6+55R4vbw3EIzBveXWi4kEBbnQlXW/eg==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.48.0.tgz",
|
||||
"integrity": "sha512-LdivLfBXXB9us1aAc6XaL7/L2Ob4vi3C/fEOXElehg3qHjX6q6pewiv5wBvVXGX1NfZTRvu+X11k6TZoxKsezw==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.48.0.tgz",
|
||||
"integrity": "sha512-fuuVULB5/1vI8NoIwXwR3xwhJJqk+y4RdSdajExGF7nnUDBpwUJyXsmYJnOkBO+oLeEs58xaCpotCKiPUNnE3g==",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "8.48.0",
|
||||
"@sentry-internal/feedback": "8.48.0",
|
||||
"@sentry-internal/replay": "8.48.0",
|
||||
"@sentry-internal/replay-canvas": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.48.0.tgz",
|
||||
"integrity": "sha512-VGwYgTfLpvJ5LRO5A+qWo1gpo6SfqaGXL9TOzVgBucAdpzbrYHpZ87sEarDVq/4275uk1b0S293/mfsskFczyw==",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vue": {
|
||||
"version": "8.48.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vue/-/vue-8.48.0.tgz",
|
||||
"integrity": "sha512-hqm9X7hz1vMQQB1HBYezrDBQihZk6e/MxWIG1wMJoClcBnD1Sh7y+D36UwaQlR4Gr/Ftiz+Bb0DxuAYHoUS4ow==",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "8.48.0",
|
||||
"@sentry/core": "8.48.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pinia": "2.x",
|
||||
"vue": "2.x || 3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pinia": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.7.8",
|
||||
"version": "1.7.10",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -17,8 +17,8 @@
|
||||
"update-litegraph": "node scripts/update-litegraph.js",
|
||||
"zipdist": "node scripts/zipdist.js",
|
||||
"typecheck": "vue-tsc --noEmit && tsc --noEmit && tsc-strict",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --plugin @trivago/prettier-plugin-sort-imports",
|
||||
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
"test:jest": "jest --config jest.config.ts",
|
||||
"test:generate": "npx tsx tests-ui/setup",
|
||||
"test:browser": "npx playwright test",
|
||||
@@ -86,6 +86,7 @@
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.7",
|
||||
"@comfyorg/litegraph": "^0.8.60",
|
||||
"@primevue/themes": "^4.0.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-table": "^2.10.4",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@@ -45,9 +45,6 @@ onMounted(() => {
|
||||
|
||||
if (isElectron()) {
|
||||
document.addEventListener('contextmenu', showContextMenu)
|
||||
|
||||
// Enable CSS selectors
|
||||
document.documentElement.dataset['platform'] = 'electron'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -765,6 +765,7 @@ audio.comfy-audio.empty-audio-widget {
|
||||
padding: var(--comfy-tree-explorer-item-padding) !important;
|
||||
}
|
||||
|
||||
/* [Desktop] Electron window specific styles */
|
||||
.app-drag {
|
||||
app-region: drag;
|
||||
}
|
||||
@@ -772,3 +773,8 @@ audio.comfy-audio.empty-audio-widget {
|
||||
.no-drag {
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-actions-spacer {
|
||||
width: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
}
|
||||
/* End of [Desktop] Electron window specific styles */
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<template>
|
||||
<Button
|
||||
<div
|
||||
v-show="workspaceState.focusMode"
|
||||
class="comfy-menu-hamburger"
|
||||
class="comfy-menu-hamburger no-drag"
|
||||
:style="positionCSS"
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
size="large"
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeMenu"
|
||||
/>
|
||||
>
|
||||
<Button
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
size="large"
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeMenu"
|
||||
/>
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -46,15 +50,13 @@ const positionCSS = computed<CSSProperties>(() =>
|
||||
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
|
||||
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
|
||||
menuSetting.value === 'Bottom'
|
||||
? { bottom: '0', right: '0' }
|
||||
: { top: '0', right: 'calc(100% - env(titlebar-area-width, 100%))' }
|
||||
? { bottom: '0px', right: '0px' }
|
||||
: { top: '0px', right: '0px' }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comfy-menu-hamburger {
|
||||
pointer-events: auto;
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
@apply pointer-events-auto fixed z-[9999] flex flex-row;
|
||||
}
|
||||
</style>
|
||||
|
||||
40
src/components/common/CheckboxGroup.vue
Normal file
40
src/components/common/CheckboxGroup.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<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>
|
||||
@@ -5,12 +5,20 @@
|
||||
:message="props.error.exception_message"
|
||||
/>
|
||||
<div class="comfy-error-report">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
:label="$t('g.showReport')"
|
||||
@click="showReport"
|
||||
text
|
||||
/>
|
||||
<div class="flex gap-2 justify-center">
|
||||
<Button
|
||||
v-show="!reportOpen"
|
||||
text
|
||||
:label="$t('g.showReport')"
|
||||
@click="showReport"
|
||||
/>
|
||||
<Button
|
||||
v-show="!sendReportOpen"
|
||||
text
|
||||
:label="$t('issueReport.helpFix')"
|
||||
@click="showSendReport"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="reportOpen">
|
||||
<Divider />
|
||||
<ScrollPanel style="width: 100%; height: 400px; max-width: 80vw">
|
||||
@@ -18,9 +26,12 @@
|
||||
</ScrollPanel>
|
||||
<Divider />
|
||||
</template>
|
||||
|
||||
<ReportIssuePanel
|
||||
v-if="sendReportOpen"
|
||||
error-type="graphExecutionError"
|
||||
:extra-fields="[stackTraceField]"
|
||||
/>
|
||||
<div class="action-container">
|
||||
<ReportIssueButton v-if="showSendError" :error="props.error" />
|
||||
<FindIssueButton
|
||||
:errorMessage="props.error.exception_message"
|
||||
:repoOwner="repoOwner"
|
||||
@@ -41,16 +52,18 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
|
||||
import ReportIssueButton from '@/components/dialog/content/error/ReportIssueButton.vue'
|
||||
import { useCopyToClipboard } from '@/hooks/clipboardHooks'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ExecutionErrorWsMessage, SystemStats } from '@/types/apiTypes'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import type { ReportField } from '@/types/issueReportTypes'
|
||||
|
||||
import ReportIssuePanel from './error/ReportIssuePanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
@@ -63,9 +76,24 @@ const reportOpen = ref(false)
|
||||
const showReport = () => {
|
||||
reportOpen.value = true
|
||||
}
|
||||
const showSendError = isElectron()
|
||||
|
||||
const sendReportOpen = ref(false)
|
||||
const showSendReport = () => {
|
||||
sendReportOpen.value = true
|
||||
}
|
||||
const toast = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const stackTraceField = computed<ReportField>(() => {
|
||||
return {
|
||||
label: t('issueReport.stackTrace'),
|
||||
value: 'StackTrace',
|
||||
optIn: true,
|
||||
data: {
|
||||
nodeType: props.error.node_type,
|
||||
stackTrace: props.error.traceback?.join('\n')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
@click="reportIssue"
|
||||
:label="$t('g.reportIssue')"
|
||||
:severity="submitted ? 'success' : 'secondary'"
|
||||
:icon="icon"
|
||||
:disabled="submitted"
|
||||
v-tooltip="$t('g.reportIssueTooltip')"
|
||||
>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { ExecutionErrorWsMessage } from '@/types/apiTypes'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const { error } = defineProps<{
|
||||
error: ExecutionErrorWsMessage
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
const icon = computed(
|
||||
() => `pi ${submitting.value ? 'pi-spin pi-spinner' : 'pi-send'}`
|
||||
)
|
||||
|
||||
const reportIssue = async () => {
|
||||
if (submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await electronAPI().sendErrorToSentry(error.exception_message, {
|
||||
stackTrace: error.traceback?.join('\n'),
|
||||
nodeType: error.node_type
|
||||
})
|
||||
submitted.value = true
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('g.reportSent'),
|
||||
life: 3000
|
||||
})
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
197
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
197
src/components/dialog/content/error/ReportIssuePanel.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<Panel>
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ $t('issueReport.submitErrorReport') }}</span>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CaptureContext, User } from '@sentry/core'
|
||||
import { captureMessage } from '@sentry/core'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
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'
|
||||
|
||||
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[]
|
||||
}>()
|
||||
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' }
|
||||
]
|
||||
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 reportCheckboxes = computed(() => [
|
||||
...(props.extraFields?.map(({ label, value }) => ({ label, value })) ?? []),
|
||||
...defaultReportCheckboxes.filter(({ value }) =>
|
||||
defaultFields.includes(value as DefaultField)
|
||||
)
|
||||
])
|
||||
|
||||
const getUserInfo = (): User => ({ email: contactInfo.value })
|
||||
|
||||
const getLogs = async () =>
|
||||
selection.value.includes('Logs') ? api.getLogs() : null
|
||||
|
||||
const getSystemStats = async () =>
|
||||
selection.value.includes('SystemStats') ? api.getSystemStats() : null
|
||||
|
||||
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 }
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
return {
|
||||
details: details.value,
|
||||
contactPreferences: {
|
||||
followUp: followUp.value,
|
||||
notifyOnResolution: notifyResolve.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createCaptureContext = async (): Promise<CaptureContext> => {
|
||||
return {
|
||||
user: getUserInfo(),
|
||||
level: 'error',
|
||||
tags: {
|
||||
errorType: props.errorType
|
||||
},
|
||||
extra: {
|
||||
...createFeedback(),
|
||||
...(await createDefaultFields()),
|
||||
...createExtraFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
// @ts-strict-ignore
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from 'primevue/button'
|
||||
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 { 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 ReportIssuePanel from '../ReportIssuePanel.vue'
|
||||
|
||||
type ReportIssuePanelProps = {
|
||||
errorType: string
|
||||
defaultFields?: DefaultField[]
|
||||
extraFields?: ReportField[]
|
||||
}
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: enMesages
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('primevue/usetoast', () => ({
|
||||
useToast: vi.fn(() => ({
|
||||
add: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getLogs: vi.fn().mockResolvedValue('mock logs'),
|
||||
getSystemStats: vi.fn().mockResolvedValue('mock stats'),
|
||||
getSettings: vi.fn().mockResolvedValue('mock settings')
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
graph: {
|
||||
asSerialisable: vi.fn().mockReturnValue({})
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@sentry/core', () => ({
|
||||
captureMessage: vi.fn()
|
||||
}))
|
||||
|
||||
describe('ReportIssuePanel', () => {
|
||||
beforeAll(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props: ReportIssuePanelProps, options = {}): any => {
|
||||
return mount(ReportIssuePanel, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createTestingPinia(), i18n],
|
||||
directives: { tooltip: Tooltip },
|
||||
components: { InputText, Button, Panel, Textarea, CheckboxGroup }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
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.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'])
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
|
||||
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.')
|
||||
})
|
||||
|
||||
it('updates contactPrefs when preferences are selected', async () => {
|
||||
const wrapper = mountComponent({ errorType: 'Test Error' })
|
||||
const preferences = wrapper.findAllComponents(CheckboxGroup).at(1)
|
||||
await preferences?.setValue(['FollowUp'])
|
||||
expect(wrapper.vm.contactPrefs).toEqual(['FollowUp'])
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
it('renders with overridden default fields', () => {
|
||||
const wrapper = mountComponent({
|
||||
errorType: 'Test Error',
|
||||
defaultFields: ['Settings']
|
||||
})
|
||||
const checkboxes = wrapper.findAllComponents(CheckboxGroup).at(0)
|
||||
expect(checkboxes?.props('checkboxes')).toEqual([
|
||||
{ label: 'Settings', value: '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'
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
checkbox: 'Logs',
|
||||
apiMethod: 'getLogs',
|
||||
expectedKey: 'logs',
|
||||
mockValue: 'mock logs'
|
||||
},
|
||||
{
|
||||
checkbox: 'SystemStats',
|
||||
apiMethod: 'getSystemStats',
|
||||
expectedKey: 'systemStats',
|
||||
mockValue: 'mock stats'
|
||||
},
|
||||
{
|
||||
checkbox: 'Settings',
|
||||
apiMethod: 'getSettings',
|
||||
expectedKey: 'settings',
|
||||
mockValue: 'mock settings'
|
||||
}
|
||||
])(
|
||||
'submits (%s) when the (%s) 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 })
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('submits workflow when the Workflow checkbox is selected', async () => {
|
||||
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 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 })
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -57,7 +57,7 @@ import { usePragmaticDroppable } from '@/hooks/dndHooks'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { setStorageValue } from '@/scripts/utils'
|
||||
import { getStorageValue, setStorageValue } from '@/scripts/utils'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
@@ -95,6 +95,29 @@ const canvasMenuEnabled = computed(() =>
|
||||
)
|
||||
const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
|
||||
const storedWorkflows = JSON.parse(
|
||||
getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'
|
||||
)
|
||||
const storedActiveIndex = JSON.parse(
|
||||
getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'
|
||||
)
|
||||
const openWorkflows = computed(() => workspaceStore?.workflow?.openWorkflows)
|
||||
const activeWorkflow = computed(() => workspaceStore?.workflow?.activeWorkflow)
|
||||
const restoreState = computed<{ paths: string[]; activeIndex: number }>(() => {
|
||||
if (!openWorkflows.value || !activeWorkflow.value) {
|
||||
return { paths: [], activeIndex: -1 }
|
||||
}
|
||||
|
||||
const paths = openWorkflows.value
|
||||
.filter((workflow) => workflow?.isPersisted && !workflow.isModified)
|
||||
.map((workflow) => workflow.path)
|
||||
const activeIndex = openWorkflows.value.findIndex(
|
||||
(workflow) => workflow.path === activeWorkflow.value?.path
|
||||
)
|
||||
|
||||
return { paths, activeIndex }
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')
|
||||
if (canvasStore.canvas) {
|
||||
@@ -364,6 +387,18 @@ onMounted(async () => {
|
||||
'Comfy.CustomColorPalettes'
|
||||
)
|
||||
|
||||
const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0
|
||||
if (isRestorable)
|
||||
workflowStore.openWorkflowsInBackground({
|
||||
left: storedWorkflows.slice(0, storedActiveIndex),
|
||||
right: storedWorkflows.slice(storedActiveIndex)
|
||||
})
|
||||
|
||||
watch(restoreState, ({ paths, activeIndex }) => {
|
||||
setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))
|
||||
setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))
|
||||
})
|
||||
|
||||
// Start watching for locale change after the initial value is loaded.
|
||||
watch(
|
||||
() => settingStore.get('Comfy.Locale'),
|
||||
|
||||
@@ -59,10 +59,27 @@
|
||||
</h4>
|
||||
<ul class="list-disc pl-6 space-y-1">
|
||||
<li>
|
||||
{{ $t('install.settings.dataCollectionDialog.errorReports') }}
|
||||
{{
|
||||
$t('install.settings.dataCollectionDialog.collect.errorReports')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('install.settings.dataCollectionDialog.systemInfo') }}
|
||||
{{ $t('install.settings.dataCollectionDialog.collect.systemInfo') }}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.collect.userJourneyEvents'
|
||||
)
|
||||
}}
|
||||
<span
|
||||
class="pi pi-info-circle text-neutral-400"
|
||||
v-tooltip="
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.collect.userJourneyTooltip'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -72,21 +89,29 @@
|
||||
<ul class="list-disc pl-6 space-y-1">
|
||||
<li>
|
||||
{{
|
||||
$t('install.settings.dataCollectionDialog.personalInformation')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('install.settings.dataCollectionDialog.workflowContents') }}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t('install.settings.dataCollectionDialog.fileSystemInformation')
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.doNotCollect.personalInformation'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.customNodeConfigurations'
|
||||
'install.settings.dataCollectionDialog.doNotCollect.workflowContents'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.doNotCollect.fileSystemInformation'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$t(
|
||||
'install.settings.dataCollectionDialog.doNotCollect.customNodeConfigurations'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
|
||||
@@ -130,12 +130,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
import Tag from 'primevue/tag'
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { TorchDeviceType, electronAPI } from '@/utils/envUtil'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
@click="alphabeticalSort = !alphabeticalSort"
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.sortOrder')"
|
||||
/>
|
||||
<Button
|
||||
class="grouping-button"
|
||||
:icon="groupBySource ? 'pi pi-list' : 'pi pi-list-check'"
|
||||
text
|
||||
severity="secondary"
|
||||
@click="groupBySource = !groupBySource"
|
||||
v-tooltip.bottom="$t('sideToolbar.nodeLibraryTab.groupingType')"
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
<SearchBox
|
||||
@@ -66,7 +74,7 @@ import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import Popover from 'primevue/popover'
|
||||
import type { TreeNode } from 'primevue/treenode'
|
||||
import { Ref, computed, nextTick, ref } from 'vue'
|
||||
import { Ref, computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import { SearchFilter } from '@/components/common/SearchFilterChip.vue'
|
||||
@@ -101,6 +109,21 @@ const nodeBookmarkTreeExplorerRef = ref<InstanceType<
|
||||
> | null>(null)
|
||||
const searchFilter = ref(null)
|
||||
const alphabeticalSort = ref(false)
|
||||
const groupBySource = ref(false)
|
||||
|
||||
const createSourceKey = (nodeDef: ComfyNodeDefImpl) => {
|
||||
const sourcePath = nodeDef.python_module.split('.')
|
||||
const pathWithoutCategory = nodeDef.nodePath.split('/').slice(1)
|
||||
return [...sourcePath, ...pathWithoutCategory]
|
||||
}
|
||||
|
||||
watch(groupBySource, (newValue) => {
|
||||
if (newValue) {
|
||||
nodeDefStore.setKeyFunction(createSourceKey)
|
||||
} else {
|
||||
nodeDefStore.setKeyFunction(null)
|
||||
}
|
||||
})
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
@@ -194,4 +217,9 @@ const onRemoveFilter = (filterAndValue) => {
|
||||
}
|
||||
handleSearch(searchQuery.value)
|
||||
}
|
||||
|
||||
// This can be added if the persistent state is not desirable:
|
||||
// onBeforeUnmount(() => {
|
||||
// nodeDefStore.setKeyFunction(null)
|
||||
// })
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template #header>
|
||||
<div class="flex items-center justify-center">
|
||||
<div
|
||||
class="relative overflow-hidden rounded-lg cursor-pointer w-64 h-64"
|
||||
class="relative overflow-hidden rounded-t-lg cursor-pointer w-64 h-64"
|
||||
>
|
||||
<img
|
||||
v-if="!imageError"
|
||||
@@ -13,7 +13,7 @@
|
||||
: `api/workflow_templates/${props.moduleName}/${props.workflowName}.jpg`
|
||||
"
|
||||
@error="imageError = true"
|
||||
class="w-64 h-64 rounded-lg object-cover thumbnail"
|
||||
class="w-64 h-64 rounded-t-lg object-cover thumbnail"
|
||||
/>
|
||||
<div v-else class="w-64 h-64 content-center text-center">
|
||||
<i class="pi pi-file" style="font-size: 4rem"></i>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
:key="selectedTab.moduleName"
|
||||
>
|
||||
<template #item="slotProps">
|
||||
<div @click="loadWorkflow(slotProps.data)">
|
||||
<div @click="loadWorkflow(slotProps.data)" class="p-2">
|
||||
<TemplateWorkflowCard
|
||||
:moduleName="selectedTab.moduleName"
|
||||
:workflowName="slotProps.data"
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
<div
|
||||
ref="topMenuRef"
|
||||
class="comfyui-menu flex items-center"
|
||||
v-show="betaMenuEnabled && !workspaceState.focusMode"
|
||||
v-show="showTopMenu"
|
||||
:class="{ dropzone: isDropZone, 'dropzone-active': isDroppable }"
|
||||
>
|
||||
<h1 class="comfyui-logo mx-2">ComfyUI</h1>
|
||||
<h1 class="comfyui-logo mx-2 app-drag">ComfyUI</h1>
|
||||
<CommandMenubar />
|
||||
<Divider layout="vertical" class="mx-2" />
|
||||
<div class="flex-grow min-w-0">
|
||||
<div class="flex-grow min-w-0 app-drag h-full">
|
||||
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
||||
</div>
|
||||
<div class="comfyui-menu-right" ref="menuRight"></div>
|
||||
@@ -25,14 +24,23 @@
|
||||
@click="workspaceState.focusMode = true"
|
||||
@contextmenu="showNativeMenu"
|
||||
/>
|
||||
<div
|
||||
v-show="menuSetting !== 'Bottom'"
|
||||
class="window-actions-spacer flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow && !showTopMenu"
|
||||
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventBus, useResizeObserver } from '@vueuse/core'
|
||||
import { useEventBus } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Divider from 'primevue/divider'
|
||||
import { computed, onMounted, provide, ref } from 'vue'
|
||||
|
||||
import Actionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
@@ -49,14 +57,20 @@ const settingStore = useSettingStore()
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const betaMenuEnabled = computed(() => menuSetting.value !== 'Disabled')
|
||||
const teleportTarget = computed(() =>
|
||||
settingStore.get('Comfy.UseNewMenu') === 'Top'
|
||||
? '.comfyui-body-top'
|
||||
: '.comfyui-body-bottom'
|
||||
)
|
||||
const isNativeWindow = computed(
|
||||
() =>
|
||||
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
|
||||
)
|
||||
const showTopMenu = computed(
|
||||
() => betaMenuEnabled.value && !workspaceState.focusMode
|
||||
)
|
||||
|
||||
const menuRight = ref<HTMLDivElement | null>(null)
|
||||
// Menu-right holds legacy topbar elements attached by custom scripts
|
||||
@@ -78,20 +92,13 @@ eventBus.on((event: string, payload: any) => {
|
||||
}
|
||||
})
|
||||
|
||||
/** Height of titlebar on desktop. */
|
||||
if (isElectron()) {
|
||||
let desktopHeight = 0
|
||||
|
||||
useResizeObserver(topMenuRef, (entries) => {
|
||||
if (settingStore.get('Comfy.UseNewMenu') !== 'Top') return
|
||||
|
||||
const { height } = entries[0].contentRect
|
||||
if (desktopHeight === height) return
|
||||
|
||||
electronAPI().changeTheme({ height })
|
||||
desktopHeight = height
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
if (isElectron()) {
|
||||
electronAPI().changeTheme({
|
||||
height: topMenuRef.value.getBoundingClientRect().height
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -127,30 +134,3 @@ if (isElectron()) {
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="postcss">
|
||||
/* Desktop: Custom window styling */
|
||||
:root[data-platform='electron'] {
|
||||
.comfyui-logo {
|
||||
@apply flex items-center gap-2 my-1 mx-1.5;
|
||||
|
||||
&::before {
|
||||
@apply w-7 h-7 bg-[url('/assets/images/Comfy_Logo_x256.png')] bg-no-repeat bg-contain content-[''];
|
||||
}
|
||||
}
|
||||
|
||||
.comfyui-body-top {
|
||||
.comfyui-menu {
|
||||
app-region: drag;
|
||||
padding-right: calc(100% - env(titlebar-area-width, 0));
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
.p-menubar,
|
||||
.comfyui-menu-right > *,
|
||||
.actionbar {
|
||||
app-region: no-drag;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="workflow-tabs-container flex flex-row w-full">
|
||||
<div class="workflow-tabs-container flex flex-row max-w-full h-full">
|
||||
<ScrollPanel
|
||||
class="overflow-hidden"
|
||||
class="overflow-hidden no-drag"
|
||||
:pt:content="{
|
||||
class: 'p-0 w-full',
|
||||
onwheel: handleWheel
|
||||
@@ -28,7 +28,7 @@
|
||||
</ScrollPanel>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
|
||||
class="new-blank-workflow-button flex-shrink-0"
|
||||
class="new-blank-workflow-button flex-shrink-0 no-drag"
|
||||
icon="pi pi-plus"
|
||||
text
|
||||
severity="secondary"
|
||||
@@ -50,7 +50,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { ComfyWorkflow } from '@/stores/workflowStore'
|
||||
import { ComfyWorkflow, useWorkflowBookmarkStore } from '@/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -67,6 +67,7 @@ const { t } = useI18n()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||
const rightClickedTab = ref<WorkflowOption>(null)
|
||||
const menu = ref()
|
||||
|
||||
@@ -154,6 +155,13 @@ const contextMenuItems = computed(() => {
|
||||
...options.value.slice(0, index)
|
||||
]),
|
||||
disabled: options.value.length <= 1
|
||||
},
|
||||
{
|
||||
label: workflowBookmarkStore.isBookmarked(tab.workflow.path)
|
||||
? t('tabMenu.removeFromBookmarks')
|
||||
: t('tabMenu.addToBookmarks'),
|
||||
command: () => workflowBookmarkStore.toggleBookmarked(tab.workflow.path),
|
||||
disabled: tab.workflow.isTemporary
|
||||
}
|
||||
]
|
||||
})
|
||||
@@ -170,21 +178,31 @@ const handleWheel = (event: WheelEvent) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-togglebutton) {
|
||||
@apply p-0 bg-transparent rounded-none flex-shrink-0 relative border-0 border-r border-solid;
|
||||
border-right-color: var(--border-color);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton::before) {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton) {
|
||||
@apply p-0 bg-transparent rounded-none flex-shrink-0 relative;
|
||||
:deep(.p-togglebutton:first-child) {
|
||||
@apply border-l border-solid;
|
||||
border-left-color: var(--border-color);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:not(:first-child)) {
|
||||
@apply border-l-0;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton.p-togglebutton-checked) {
|
||||
border-bottom-width: 1px;
|
||||
@apply border-b border-solid h-full;
|
||||
border-bottom-color: var(--p-button-text-primary-color);
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton:not(.p-togglebutton-checked)) {
|
||||
opacity: 0.75;
|
||||
@apply opacity-75;
|
||||
}
|
||||
|
||||
:deep(.p-togglebutton-checked) .close-button,
|
||||
@@ -200,9 +218,17 @@ const handleWheel = (event: WheelEvent) => {
|
||||
@apply invisible;
|
||||
}
|
||||
|
||||
:deep(.p-scrollpanel-content) {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
/* Scrollbar half opacity to avoid blocking the active tab bottom border */
|
||||
:deep(.p-scrollpanel:hover .p-scrollpanel-bar),
|
||||
:deep(.p-scrollpanel:active .p-scrollpanel-bar) {
|
||||
opacity: 0.5;
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
:deep(.p-selectbutton) {
|
||||
@apply rounded-none h-full;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -386,8 +386,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
category: ['Comfy', 'Menu', 'UseNewMenu'],
|
||||
defaultValue: 'Top',
|
||||
name: 'Use new menu',
|
||||
tooltip:
|
||||
'(Desktop, Windows only): When using custom window style, only Top is supported',
|
||||
type: 'combo',
|
||||
options: ['Disabled', 'Top', 'Bottom'],
|
||||
migrateDeprecatedValue: (value: string) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from '@/i18n'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
;(async () => {
|
||||
@@ -31,7 +30,7 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
{
|
||||
id: 'Comfy-Desktop.SendStatistics',
|
||||
category: ['Comfy-Desktop', 'General', 'Send Statistics'],
|
||||
name: 'Send anonymous crash reports',
|
||||
name: 'Send anonymous usage metrics',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
onChange: onChangeRestartApp
|
||||
@@ -40,22 +39,18 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
id: 'Comfy-Desktop.WindowStyle',
|
||||
category: ['Comfy-Desktop', 'General', 'Window Style'],
|
||||
name: 'Window Style',
|
||||
tooltip: "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
tooltip: 'Choose custom option to hide the system title bar',
|
||||
type: 'combo',
|
||||
experimental: true,
|
||||
defaultValue: 'default',
|
||||
options: ['default', 'custom'],
|
||||
onChange: (
|
||||
newValue: 'default' | 'custom',
|
||||
oldValue?: 'default' | 'custom'
|
||||
oldValue: 'default' | 'custom'
|
||||
) => {
|
||||
if (!oldValue) return
|
||||
|
||||
// Custom window mode requires the Top menu.
|
||||
if (newValue === 'custom') {
|
||||
useSettingStore().set('Comfy.UseNewMenu', 'Top')
|
||||
}
|
||||
|
||||
electronAPI.Config.setWindowStyle(newValue)
|
||||
|
||||
onChangeRestartApp(newValue, oldValue)
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -194,21 +189,4 @@ import { electronAPI as getElectronAPI, isElectron } from '@/utils/envUtil'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// TODO: Replace monkey patch with API or replace UX.
|
||||
// If the user changes frontend menu type, ensure custom window style is disabled.
|
||||
const menuSetting = useSettingStore().settingsById['Comfy.UseNewMenu']
|
||||
if (menuSetting) {
|
||||
const { onChange } = menuSetting
|
||||
menuSetting.onChange = (
|
||||
newValue: 'Disabled' | 'Top' | 'Bottom',
|
||||
oldValue?: 'Disabled' | 'Top' | 'Bottom'
|
||||
) => {
|
||||
const style = useSettingStore().get('Comfy-Desktop.WindowStyle')
|
||||
if (oldValue === 'Top' && style === 'custom') {
|
||||
useSettingStore().set('Comfy-Desktop.WindowStyle', 'default')
|
||||
}
|
||||
return onChange?.(newValue, oldValue)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { IWidget } from '@comfyorg/litegraph'
|
||||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper'
|
||||
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader'
|
||||
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
|
||||
@@ -115,8 +116,7 @@ class Load3d {
|
||||
stlLoader: STLLoader
|
||||
currentModel: THREE.Object3D | null = null
|
||||
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
|
||||
node: any
|
||||
private animationFrameId: number | null = null
|
||||
animationFrameId: number | null = null
|
||||
gridHelper: THREE.GridHelper
|
||||
lights: THREE.Light[] = []
|
||||
clock: THREE.Clock
|
||||
@@ -131,6 +131,10 @@ class Load3d {
|
||||
currentUpDirection: 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z' =
|
||||
'original'
|
||||
originalRotation: THREE.Euler | null = null
|
||||
viewHelper: ViewHelper
|
||||
viewHelperContainer: HTMLDivElement
|
||||
cameraSwitcherContainer: HTMLDivElement
|
||||
gridSwitcherContainer: HTMLDivElement
|
||||
|
||||
constructor(container: Element | HTMLElement) {
|
||||
this.scene = new THREE.Scene()
|
||||
@@ -157,6 +161,7 @@ class Load3d {
|
||||
this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true })
|
||||
this.renderer.setSize(300, 300)
|
||||
this.renderer.setClearColor(0x282828)
|
||||
this.renderer.autoClear = false
|
||||
|
||||
const rendererDomElement: HTMLCanvasElement = this.renderer.domElement
|
||||
|
||||
@@ -203,13 +208,143 @@ class Load3d {
|
||||
|
||||
this.standardMaterial = this.createSTLMaterial()
|
||||
|
||||
this.animate()
|
||||
this.createViewHelper(container)
|
||||
|
||||
this.createGridSwitcher(container)
|
||||
|
||||
this.createCameraSwitcher(container)
|
||||
|
||||
this.handleResize()
|
||||
|
||||
this.startAnimation()
|
||||
}
|
||||
|
||||
createViewHelper(container: Element | HTMLElement) {
|
||||
this.viewHelperContainer = document.createElement('div')
|
||||
|
||||
this.viewHelperContainer.style.position = 'absolute'
|
||||
this.viewHelperContainer.style.bottom = '0'
|
||||
this.viewHelperContainer.style.left = '0'
|
||||
this.viewHelperContainer.style.width = '128px'
|
||||
this.viewHelperContainer.style.height = '128px'
|
||||
this.viewHelperContainer.addEventListener('pointerup', (event) => {
|
||||
event.stopPropagation()
|
||||
|
||||
this.viewHelper.handleClick(event)
|
||||
})
|
||||
|
||||
this.viewHelperContainer.addEventListener('pointerdown', (event) => {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
container.appendChild(this.viewHelperContainer)
|
||||
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
|
||||
this.viewHelper.center = this.controls.target
|
||||
}
|
||||
|
||||
createGridSwitcher(container: Element | HTMLElement) {
|
||||
this.gridSwitcherContainer = document.createElement('div')
|
||||
this.gridSwitcherContainer.style.position = 'absolute'
|
||||
this.gridSwitcherContainer.style.top = '28px' // 修改这里,让按钮在相机按钮下方
|
||||
this.gridSwitcherContainer.style.left = '3px' // 与相机按钮左对齐
|
||||
this.gridSwitcherContainer.style.width = '20px'
|
||||
this.gridSwitcherContainer.style.height = '20px'
|
||||
this.gridSwitcherContainer.style.cursor = 'pointer'
|
||||
this.gridSwitcherContainer.style.alignItems = 'center'
|
||||
this.gridSwitcherContainer.style.justifyContent = 'center'
|
||||
this.gridSwitcherContainer.style.transition = 'background-color 0.2s'
|
||||
|
||||
const gridIcon = document.createElement('div')
|
||||
gridIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M3 3h18v18H3z"/>
|
||||
<path d="M3 9h18"/>
|
||||
<path d="M3 15h18"/>
|
||||
<path d="M9 3v18"/>
|
||||
<path d="M15 3v18"/>
|
||||
</svg>
|
||||
`
|
||||
|
||||
const updateButtonState = () => {
|
||||
if (this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor =
|
||||
'rgba(255, 255, 255, 0.2)'
|
||||
} else {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
}
|
||||
|
||||
updateButtonState()
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
if (!this.gridHelper.visible) {
|
||||
this.gridSwitcherContainer.style.backgroundColor = 'transparent'
|
||||
}
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.title = 'Toggle Grid'
|
||||
|
||||
this.gridSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleGrid(!this.gridHelper.visible)
|
||||
updateButtonState()
|
||||
})
|
||||
|
||||
this.gridSwitcherContainer.appendChild(gridIcon)
|
||||
container.appendChild(this.gridSwitcherContainer)
|
||||
}
|
||||
|
||||
createCameraSwitcher(container: Element | HTMLElement) {
|
||||
this.cameraSwitcherContainer = document.createElement('div')
|
||||
this.cameraSwitcherContainer.style.position = 'absolute'
|
||||
this.cameraSwitcherContainer.style.top = '3px'
|
||||
this.cameraSwitcherContainer.style.left = '3px'
|
||||
this.cameraSwitcherContainer.style.width = '20px'
|
||||
this.cameraSwitcherContainer.style.height = '20px'
|
||||
this.cameraSwitcherContainer.style.cursor = 'pointer'
|
||||
this.cameraSwitcherContainer.style.alignItems = 'center'
|
||||
this.cameraSwitcherContainer.style.justifyContent = 'center'
|
||||
|
||||
const cameraIcon = document.createElement('div')
|
||||
cameraIcon.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
||||
<path d="M18 4H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Z"/>
|
||||
<path d="m12 12 4-2.4"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
`
|
||||
this.cameraSwitcherContainer.addEventListener('mouseenter', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('mouseleave', () => {
|
||||
this.cameraSwitcherContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.title =
|
||||
'Switch Camera (Perspective/Orthographic)'
|
||||
|
||||
this.cameraSwitcherContainer.addEventListener('click', (event) => {
|
||||
event.stopPropagation()
|
||||
this.toggleCamera()
|
||||
})
|
||||
|
||||
this.cameraSwitcherContainer.appendChild(cameraIcon)
|
||||
|
||||
container.appendChild(this.cameraSwitcherContainer)
|
||||
}
|
||||
|
||||
setFOV(fov: number) {
|
||||
if (this.activeCamera === this.perspectiveCamera) {
|
||||
this.perspectiveCamera.fov = fov
|
||||
@@ -465,6 +600,13 @@ class Load3d {
|
||||
this.controls.target.copy(target)
|
||||
this.controls.update()
|
||||
|
||||
this.viewHelper.dispose()
|
||||
this.viewHelper = new ViewHelper(
|
||||
this.activeCamera,
|
||||
this.viewHelperContainer
|
||||
)
|
||||
this.viewHelper.center = this.controls.target
|
||||
|
||||
this.handleResize()
|
||||
}
|
||||
|
||||
@@ -501,8 +643,16 @@ class Load3d {
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
@@ -588,6 +738,7 @@ class Load3d {
|
||||
}
|
||||
|
||||
this.controls.dispose()
|
||||
this.viewHelper.dispose()
|
||||
this.renderer.dispose()
|
||||
this.renderer.domElement.remove()
|
||||
this.scene.clear()
|
||||
@@ -818,10 +969,12 @@ class Load3d {
|
||||
this.orthographicCamera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const sceneData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.clear()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
const maskData = this.renderer.domElement.toDataURL('image/png')
|
||||
|
||||
@@ -846,44 +999,6 @@ class Load3d {
|
||||
})
|
||||
}
|
||||
|
||||
setViewPosition(position: 'front' | 'top' | 'right' | 'isometric') {
|
||||
if (!this.currentModel) {
|
||||
return
|
||||
}
|
||||
|
||||
const box = new THREE.Box3()
|
||||
let center = new THREE.Vector3()
|
||||
let size = new THREE.Vector3()
|
||||
|
||||
if (this.currentModel) {
|
||||
box.setFromObject(this.currentModel)
|
||||
box.getCenter(center)
|
||||
box.getSize(size)
|
||||
}
|
||||
|
||||
const maxDim = Math.max(size.x, size.y, size.z)
|
||||
const distance = maxDim * 2
|
||||
|
||||
switch (position) {
|
||||
case 'front':
|
||||
this.activeCamera.position.set(0, 0, distance)
|
||||
break
|
||||
case 'top':
|
||||
this.activeCamera.position.set(0, distance, 0)
|
||||
break
|
||||
case 'right':
|
||||
this.activeCamera.position.set(distance, 0, 0)
|
||||
break
|
||||
case 'isometric':
|
||||
this.activeCamera.position.set(distance, distance, distance)
|
||||
break
|
||||
}
|
||||
|
||||
this.activeCamera.lookAt(center)
|
||||
this.controls.target.copy(center)
|
||||
this.controls.update()
|
||||
}
|
||||
|
||||
setBackgroundColor(color: string) {
|
||||
this.renderer.setClearColor(new THREE.Color(color))
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
@@ -1020,16 +1135,28 @@ class Load3dAnimation extends Load3d {
|
||||
})
|
||||
}
|
||||
|
||||
animate = () => {
|
||||
requestAnimationFrame(this.animate)
|
||||
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
startAnimation() {
|
||||
const animate = () => {
|
||||
this.animationFrameId = requestAnimationFrame(animate)
|
||||
const delta = this.clock.getDelta()
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
if (this.currentAnimation && this.isAnimationPlaying) {
|
||||
this.currentAnimation.update(delta)
|
||||
}
|
||||
|
||||
this.controls.update()
|
||||
|
||||
this.renderer.clear()
|
||||
|
||||
this.renderer.render(this.scene, this.activeCamera)
|
||||
|
||||
if (this.viewHelper.animating) {
|
||||
this.viewHelper.update(delta)
|
||||
}
|
||||
|
||||
this.viewHelper.render(this.renderer)
|
||||
}
|
||||
animate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,9 +1203,6 @@ function configureLoad3D(
|
||||
load3d: Load3d,
|
||||
loadFolder: 'input' | 'output',
|
||||
modelWidget: IWidget,
|
||||
showGrid: IWidget,
|
||||
cameraType: IWidget,
|
||||
view: IWidget,
|
||||
material: IWidget,
|
||||
bgColor: IWidget,
|
||||
lightIntensity: IWidget,
|
||||
@@ -1138,22 +1262,6 @@ function configureLoad3D(
|
||||
|
||||
modelWidget.callback = onModelWidgetUpdate
|
||||
|
||||
load3d.toggleGrid(showGrid.value as boolean)
|
||||
|
||||
showGrid.callback = (value: boolean) => {
|
||||
load3d.toggleGrid(value)
|
||||
}
|
||||
|
||||
load3d.toggleCamera(cameraType.value as 'perspective' | 'orthographic')
|
||||
|
||||
cameraType.callback = (value: 'perspective' | 'orthographic') => {
|
||||
load3d.toggleCamera(value)
|
||||
}
|
||||
|
||||
view.callback = (value: 'front' | 'top' | 'right' | 'isometric') => {
|
||||
load3d.setViewPosition(value)
|
||||
}
|
||||
|
||||
material.callback = (value: 'original' | 'normal' | 'wireframe') => {
|
||||
load3d.setMaterialMode(value)
|
||||
}
|
||||
@@ -1312,14 +1420,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
|
||||
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
|
||||
|
||||
const cameraType = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'camera_type'
|
||||
)
|
||||
|
||||
const view = node.widgets.find((w: IWidget) => w.name === 'view')
|
||||
|
||||
const material = node.widgets.find((w: IWidget) => w.name === 'material')
|
||||
|
||||
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
|
||||
@@ -1353,9 +1453,6 @@ app.registerExtension({
|
||||
load3d,
|
||||
'input',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
@@ -1569,14 +1666,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
|
||||
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
|
||||
|
||||
const cameraType = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'camera_type'
|
||||
)
|
||||
|
||||
const view = node.widgets.find((w: IWidget) => w.name === 'view')
|
||||
|
||||
const material = node.widgets.find((w: IWidget) => w.name === 'material')
|
||||
|
||||
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
|
||||
@@ -1621,9 +1710,6 @@ app.registerExtension({
|
||||
load3d,
|
||||
'input',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
@@ -1652,6 +1738,8 @@ app.registerExtension({
|
||||
sceneWidget.serializeValue = async () => {
|
||||
node.properties['Camera Info'] = JSON.stringify(load3d.getCameraState())
|
||||
|
||||
load3d.toggleAnimation(false)
|
||||
|
||||
const { scene: imageData, mask: maskData } = await load3d.captureScene(
|
||||
w.value,
|
||||
h.value
|
||||
@@ -1758,14 +1846,6 @@ app.registerExtension({
|
||||
(w: IWidget) => w.name === 'model_file'
|
||||
)
|
||||
|
||||
const showGrid = node.widgets.find((w: IWidget) => w.name === 'show_grid')
|
||||
|
||||
const cameraType = node.widgets.find(
|
||||
(w: IWidget) => w.name === 'camera_type'
|
||||
)
|
||||
|
||||
const view = node.widgets.find((w: IWidget) => w.name === 'view')
|
||||
|
||||
const material = node.widgets.find((w: IWidget) => w.name === 'material')
|
||||
|
||||
const bgColor = node.widgets.find((w: IWidget) => w.name === 'bg_color')
|
||||
@@ -1801,9 +1881,6 @@ app.registerExtension({
|
||||
load3d,
|
||||
'output',
|
||||
modelWidget,
|
||||
showGrid,
|
||||
cameraType,
|
||||
view,
|
||||
material,
|
||||
bgColor,
|
||||
lightIntensity,
|
||||
|
||||
@@ -69,7 +69,18 @@
|
||||
"command": "Command",
|
||||
"keybinding": "Keybinding",
|
||||
"upload": "Upload",
|
||||
"export": "Export"
|
||||
"export": "Export",
|
||||
"workflow": "Workflow"
|
||||
},
|
||||
"issueReport": {
|
||||
"submitErrorReport": "Submit Error Report (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"
|
||||
},
|
||||
"color": {
|
||||
"default": "Default",
|
||||
@@ -170,21 +181,26 @@
|
||||
},
|
||||
"settings": {
|
||||
"autoUpdate": "Automatic Updates",
|
||||
"allowMetrics": "Crash Reports",
|
||||
"autoUpdateDescription": "Automatically download and install updates when they become available. You'll always be notified before updates are installed.",
|
||||
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous crash reports. No personal information or workflow content will be collected. This can be disabled at any time in the settings menu.",
|
||||
"allowMetrics": "Usage Metrics",
|
||||
"autoUpdateDescription": "Automatically download updates when they become available. You will be notified before updates are installed.",
|
||||
"allowMetricsDescription": "Help improve ComfyUI by sending anonymous usage metrics. No personal information or workflow content will be collected.",
|
||||
"learnMoreAboutData": "Learn more about data collection",
|
||||
"dataCollectionDialog": {
|
||||
"title": "About Data Collection",
|
||||
"whatWeCollect": "What we collect:",
|
||||
"whatWeDoNotCollect": "What we don't collect:",
|
||||
"errorReports": "Error message and stack trace",
|
||||
"systemInfo": "Hardware, OS type, and app version",
|
||||
"personalInformation": "Personal information",
|
||||
"workflowContent": "Workflow content",
|
||||
"fileSystemInformation": "File system information",
|
||||
"workflowContents": "Workflow contents",
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
"collect": {
|
||||
"errorReports": "Error message and stack trace",
|
||||
"systemInfo": "Hardware, OS type, and app version",
|
||||
"userJourneyEvents": "User journey events",
|
||||
"userJourneyTooltip": "User journey events are used to track the user's journey through the app installation process. The event collection ends on the first successful ComfyUI workflow run."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"personalInformation": "Personal information",
|
||||
"fileSystemInformation": "File system information",
|
||||
"workflowContents": "Workflow contents",
|
||||
"customNodeConfigurations": "Custom node configurations"
|
||||
}
|
||||
}
|
||||
},
|
||||
"customNodes": "Custom Nodes",
|
||||
@@ -218,7 +234,8 @@
|
||||
"openWorkflow": "Open workflow in local file system",
|
||||
"newBlankWorkflow": "Create a new blank workflow",
|
||||
"nodeLibraryTab": {
|
||||
"sortOrder": "Sort Order"
|
||||
"sortOrder": "Sort Order",
|
||||
"groupingType": "Grouping Type"
|
||||
},
|
||||
"modelLibrary": "Model Library",
|
||||
"downloads": "Downloads",
|
||||
@@ -279,7 +296,9 @@
|
||||
"closeTab": "Close Tab",
|
||||
"closeTabsToLeft": "Close Tabs to Left",
|
||||
"closeTabsToRight": "Close Tabs to Right",
|
||||
"closeOtherTabs": "Close Other Tabs"
|
||||
"closeOtherTabs": "Close Other Tabs",
|
||||
"addToBookmarks": "Add to Bookmarks",
|
||||
"removeFromBookmarks": "Remove from Bookmarks"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"title": "Get Started with a Template",
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"name": "Automatically check for updates"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Send anonymous crash reports"
|
||||
"name": "Send anonymous usage metrics"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Window Style",
|
||||
"tooltip": "Custom: Replace the system title bar with ComfyUI's Top menu",
|
||||
"tooltip": "Choose custom option to hide the system title bar",
|
||||
"options": {
|
||||
"default": "default",
|
||||
"custom": "custom"
|
||||
@@ -261,7 +261,6 @@
|
||||
},
|
||||
"Comfy_UseNewMenu": {
|
||||
"name": "Use new menu",
|
||||
"tooltip": "(Desktop, Windows only): When using custom window style, only Top is supported",
|
||||
"options": {
|
||||
"Disabled": "Disabled",
|
||||
"Top": "Top",
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"systemInfo": "Informations système",
|
||||
"terminal": "Terminal",
|
||||
"upload": "Téléverser",
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo"
|
||||
"videoFailedToLoad": "Échec du chargement de la vidéo",
|
||||
"workflow": "Flux de travail"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Adapter la vue",
|
||||
@@ -205,27 +206,42 @@
|
||||
"pathValidationFailed": "Échec de la validation du chemin",
|
||||
"selectItemsToMigrate": "Sélectionnez les éléments à migrer",
|
||||
"settings": {
|
||||
"allowMetrics": "Rapports de plantage",
|
||||
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des rapports de plantage anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté. Cela peut être désactivé à tout moment dans le menu des paramètres.",
|
||||
"allowMetrics": "Métriques d'utilisation",
|
||||
"allowMetricsDescription": "Aidez à améliorer ComfyUI en envoyant des métriques d'utilisation anonymes. Aucune information personnelle ou contenu de flux de travail ne sera collecté.",
|
||||
"autoUpdate": "Mises à jour automatiques",
|
||||
"autoUpdateDescription": "Téléchargez et installez automatiquement les mises à jour lorsqu'elles deviennent disponibles. Vous serez toujours informé avant l'installation des mises à jour.",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "Configurations de nœuds personnalisés",
|
||||
"errorReports": "Message d'erreur et trace de la pile",
|
||||
"fileSystemInformation": "Informations sur le système de fichiers",
|
||||
"personalInformation": "Informations personnelles",
|
||||
"systemInfo": "Matériel, type d'OS et version de l'application",
|
||||
"collect": {
|
||||
"errorReports": "Message d'erreur et trace de la pile",
|
||||
"systemInfo": "Matériel, type de système d'exploitation et version de l'application",
|
||||
"userJourneyEvents": "Événements du parcours utilisateur",
|
||||
"userJourneyTooltip": "Les événements du parcours utilisateur sont utilisés pour suivre le parcours de l'utilisateur lors du processus d'installation de l'application. La collecte d'événements se termine lors de la première exécution réussie du flux de travail ComfyUI."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "Configurations de nœud personnalisées",
|
||||
"fileSystemInformation": "Informations sur le système de fichiers",
|
||||
"personalInformation": "Informations personnelles",
|
||||
"workflowContents": "Contenus du flux de travail"
|
||||
},
|
||||
"title": "À propos de la collecte de données",
|
||||
"whatWeCollect": "Ce que nous collectons :",
|
||||
"whatWeDoNotCollect": "Ce que nous ne collectons pas :",
|
||||
"workflowContent": "Contenu du flux de travail",
|
||||
"workflowContents": "Contenus du flux de travail"
|
||||
"whatWeDoNotCollect": "Ce que nous ne collectons pas :"
|
||||
},
|
||||
"learnMoreAboutData": "En savoir plus sur la collecte de données"
|
||||
},
|
||||
"systemLocations": "Emplacements système",
|
||||
"unhandledError": "Erreur inconnue"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Contactez-moi pour un suivi",
|
||||
"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)",
|
||||
"stackTrace": "Trace de la pile",
|
||||
"submitErrorReport": "Soumettre un rapport d'erreur (Facultatif)",
|
||||
"systemStats": "Statistiques du système"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "File d'attente automatique",
|
||||
"batchCount": "Nombre de lots",
|
||||
@@ -623,11 +639,13 @@
|
||||
"workflows": "Flux de travail"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Ajouter aux Favoris",
|
||||
"closeOtherTabs": "Fermer les autres onglets",
|
||||
"closeTab": "Fermer l'onglet",
|
||||
"closeTabsToLeft": "Fermer les onglets à gauche",
|
||||
"closeTabsToRight": "Fermer les onglets à droite",
|
||||
"duplicateTab": "Dupliquer l'onglet"
|
||||
"duplicateTab": "Dupliquer l'onglet",
|
||||
"removeFromBookmarks": "Retirer des Favoris"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Vérifier automatiquement les mises à jour"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Envoyer des rapports de plantage anonymes"
|
||||
"name": "Envoyer des métriques d'utilisation anonymes"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Style de fenêtre",
|
||||
@@ -265,8 +265,7 @@
|
||||
"Bottom": "Bas",
|
||||
"Disabled": "Désactivé",
|
||||
"Top": "Haut"
|
||||
},
|
||||
"tooltip": "(Bureau, uniquement Windows): Lors de l'utilisation d'un style de fenêtre personnalisé, seul Top est pris en charge"
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "Valider les définitions de nœuds (lent)",
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"systemInfo": "システム情報",
|
||||
"terminal": "ターミナル",
|
||||
"upload": "アップロード",
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました"
|
||||
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
|
||||
"workflow": "ワークフロー"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "ビューに合わせる",
|
||||
@@ -205,27 +206,42 @@
|
||||
"pathValidationFailed": "パスの検証に失敗しました",
|
||||
"selectItemsToMigrate": "移行する項目を選択",
|
||||
"settings": {
|
||||
"allowMetrics": "クラッシュレポート",
|
||||
"allowMetricsDescription": "ComfyUIの改善に協力してください。匿名のクラッシュレポートを送信します。個人情報やワークフロー内容は収集されません。この設定はいつでも無効にできます。",
|
||||
"allowMetrics": "使用状況のメトリクス",
|
||||
"allowMetricsDescription": "匿名の使用状況メトリクスを送信してComfyUIを改善します。個人情報やワークフローの内容は収集されません。",
|
||||
"autoUpdate": "自動更新",
|
||||
"autoUpdateDescription": "更新が利用可能になると、自動的にダウンロードおよびインストールを行います。インストール前に通知が表示されます。",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "カスタムノード設定",
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
"fileSystemInformation": "ファイルシステム情報",
|
||||
"personalInformation": "個人情報",
|
||||
"systemInfo": "ハードウェア、OSの種類、アプリのバージョン",
|
||||
"collect": {
|
||||
"errorReports": "エラーメッセージとスタックトレース",
|
||||
"systemInfo": "ハードウェア、OSタイプ、アプリバージョン",
|
||||
"userJourneyEvents": "ユーザージャーニーイベント",
|
||||
"userJourneyTooltip": "ユーザージャーニーイベントは、アプリのインストールプロセスを通じてユーザーの旅を追跡するために使用されます。イベントの収集は、最初の成功したComfyUIワークフローの実行で終了します。"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "カスタムノードの設定",
|
||||
"fileSystemInformation": "ファイルシステム情報",
|
||||
"personalInformation": "個人情報",
|
||||
"workflowContents": "ワークフローの内容"
|
||||
},
|
||||
"title": "データ収集について",
|
||||
"whatWeCollect": "収集内容:",
|
||||
"whatWeDoNotCollect": "収集しない内容:",
|
||||
"workflowContent": "ワークフロー内容",
|
||||
"workflowContents": "ワークフロー内容"
|
||||
"whatWeDoNotCollect": "収集しない内容:"
|
||||
},
|
||||
"learnMoreAboutData": "データ収集の詳細を見る"
|
||||
},
|
||||
"systemLocations": "システムの場所",
|
||||
"unhandledError": "未知のエラー"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "フォローアップのために私に連絡する",
|
||||
"helpFix": "これを修正するのを助ける",
|
||||
"notifyResolve": "解決したときに通知する",
|
||||
"provideAdditionalDetails": "追加の詳細を提供する(オプション)",
|
||||
"provideEmail": "あなたのメールアドレスを教えてください(オプション)",
|
||||
"stackTrace": "スタックトレース",
|
||||
"submitErrorReport": "エラーレポートを提出する(オプション)",
|
||||
"systemStats": "システム統計"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自動キュー",
|
||||
"batchCount": "バッチ数",
|
||||
@@ -623,11 +639,13 @@
|
||||
"workflows": "ワークフロー"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "ブックマークに追加",
|
||||
"closeOtherTabs": "他のタブを閉じる",
|
||||
"closeTab": "タブを閉じる",
|
||||
"closeTabsToLeft": "左のタブを閉じる",
|
||||
"closeTabsToRight": "右のタブを閉じる",
|
||||
"duplicateTab": "タブを複製"
|
||||
"duplicateTab": "タブを複製",
|
||||
"removeFromBookmarks": "ブックマークから削除"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "自動的に更新を確認する"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "匿名のクラッシュレポートを送信する"
|
||||
"name": "匿名の使用統計を送信する"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "ウィンドウスタイル",
|
||||
@@ -265,8 +265,7 @@
|
||||
"Bottom": "下",
|
||||
"Disabled": "無効",
|
||||
"Top": "上"
|
||||
},
|
||||
"tooltip": "(デスクトップ、Windowsのみ): カスタムウィンドウスタイルを使用する場合、Topのみがサポートされます"
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "ノード定義を検証(遅い)",
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"systemInfo": "시스템 정보",
|
||||
"terminal": "터미널",
|
||||
"upload": "업로드",
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다."
|
||||
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
|
||||
"workflow": "워크플로우"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "보기 맞춤",
|
||||
@@ -205,27 +206,42 @@
|
||||
"pathValidationFailed": "경로 유효성 검사 실패",
|
||||
"selectItemsToMigrate": "마이그레이션 항목 선택",
|
||||
"settings": {
|
||||
"allowMetrics": "충돌 보고서",
|
||||
"allowMetricsDescription": "익명의 충돌 보고서를 보내 ComfyUI 개선에 도움을 줍니다. 개인 정보나 워크플로 내용은 수집되지 않습니다. 이는 설정 메뉴에서 언제든지 비활성화할 수 있습니다.",
|
||||
"allowMetrics": "사용 통계",
|
||||
"allowMetricsDescription": "익명의 사용 통계를 보내 ComfyUI를 개선하는 데 도움을 줍니다. 개인 정보나 워크플로우 내용은 수집되지 않습니다.",
|
||||
"autoUpdate": "자동 업데이트",
|
||||
"autoUpdateDescription": "업데이트가 가능해지면 자동으로 다운로드하고 설치합니다. 업데이트가 설치되기 전에 항상 알림을 받습니다.",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "사용자 정의 노드 설정",
|
||||
"errorReports": "오류 메시지 및 스택 추적",
|
||||
"fileSystemInformation": "파일 시스템 정보",
|
||||
"personalInformation": "개인 정보",
|
||||
"systemInfo": "하드웨어, OS 유형 및 앱 버전",
|
||||
"collect": {
|
||||
"errorReports": "오류 메시지 및 스택 추적",
|
||||
"systemInfo": "하드웨어, OS 유형, 앱 버전",
|
||||
"userJourneyEvents": "사용자 여정 이벤트",
|
||||
"userJourneyTooltip": "사용자 여정 이벤트는 앱 설치 과정을 통한 사용자의 여정을 추적하는 데 사용됩니다. 이벤트 수집은 첫 번째 성공적인 ComfyUI 워크플로우 실행에서 종료됩니다."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "사용자 정의 노드 구성",
|
||||
"fileSystemInformation": "파일 시스템 정보",
|
||||
"personalInformation": "개인 정보",
|
||||
"workflowContents": "워크플로우 내용"
|
||||
},
|
||||
"title": "데이터 수집 안내",
|
||||
"whatWeCollect": "수집하는 정보:",
|
||||
"whatWeDoNotCollect": "수집하지 않는 정보:",
|
||||
"workflowContent": "워크플로 내용",
|
||||
"workflowContents": "워크플로 내용"
|
||||
"whatWeDoNotCollect": "수집하지 않는 정보:"
|
||||
},
|
||||
"learnMoreAboutData": "데이터 수집에 대해 더 알아보기"
|
||||
},
|
||||
"systemLocations": "시스템 위치",
|
||||
"unhandledError": "알 수 없는 오류"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "추적 조사를 위해 연락해 주세요",
|
||||
"helpFix": "이 문제 해결에 도움을 주세요",
|
||||
"notifyResolve": "해결되었을 때 알려주세요",
|
||||
"provideAdditionalDetails": "추가 세부 사항 제공 (선택 사항)",
|
||||
"provideEmail": "이메일을 알려주세요 (선택 사항)",
|
||||
"stackTrace": "스택 추적",
|
||||
"submitErrorReport": "오류 보고서 제출 (선택 사항)",
|
||||
"systemStats": "시스템 통계"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "자동 실행 큐",
|
||||
"batchCount": "배치 수",
|
||||
@@ -623,11 +639,13 @@
|
||||
"workflows": "워크플로"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "북마크에 추가",
|
||||
"closeOtherTabs": "다른 탭 닫기",
|
||||
"closeTab": "탭 닫기",
|
||||
"closeTabsToLeft": "왼쪽 탭 닫기",
|
||||
"closeTabsToRight": "오른쪽 탭 닫기",
|
||||
"duplicateTab": "탭 복제"
|
||||
"duplicateTab": "탭 복제",
|
||||
"removeFromBookmarks": "북마크에서 제거"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "자동 업데이트 확인"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "익명으로 충돌 보고서 전송"
|
||||
"name": "익명 사용 통계 보내기"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "창 스타일",
|
||||
@@ -265,8 +265,7 @@
|
||||
"Bottom": "하단",
|
||||
"Disabled": "비활성화",
|
||||
"Top": "상단"
|
||||
},
|
||||
"tooltip": "(데스크톱, 윈도우 전용): 사용자 정의 창 스타일을 사용할 때는 Top만 지원됩니다"
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "노드 정의 유효성 검사 (느림)",
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"systemInfo": "Информация о системе",
|
||||
"terminal": "Терминал",
|
||||
"upload": "Загрузить",
|
||||
"videoFailedToLoad": "Не удалось загрузить видео"
|
||||
"videoFailedToLoad": "Не удалось загрузить видео",
|
||||
"workflow": "Рабочий процесс"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "Подгонять под выделенные",
|
||||
@@ -205,27 +206,42 @@
|
||||
"pathValidationFailed": "Не удалось проверить путь",
|
||||
"selectItemsToMigrate": "Выберите элементы для миграции",
|
||||
"settings": {
|
||||
"allowMetrics": "Отчеты о сбоях",
|
||||
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные отчеты о сбоях. Личная информация или содержимое рабочего процесса не будут собираться. Это можно отключить в любое время в меню настроек.",
|
||||
"allowMetrics": "Метрики использования",
|
||||
"allowMetricsDescription": "Помогите улучшить ComfyUI, отправляя анонимные метрики использования. Личная информация или содержание рабочего процесса не будут собираться.",
|
||||
"autoUpdate": "Автоматические обновления",
|
||||
"autoUpdateDescription": "Автоматически загружать и устанавливать обновления, когда они становятся доступными. Вы всегда будете уведомлены перед установкой обновлений.",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "Конфигурации пользовательских узлов",
|
||||
"errorReports": "Сообщения об ошибках и трассировка стека",
|
||||
"fileSystemInformation": "Информация о файловой системе",
|
||||
"personalInformation": "Личная информация",
|
||||
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
|
||||
"collect": {
|
||||
"errorReports": "Сообщение об ошибке и трассировка стека",
|
||||
"systemInfo": "Аппаратное обеспечение, тип ОС и версия приложения",
|
||||
"userJourneyEvents": "События пользовательского пути",
|
||||
"userJourneyTooltip": "События пользовательского пути используются для отслеживания пути пользователя в процессе установки приложения. Сбор событий заканчивается после первого успешного запуска рабочего процесса ComfyUI."
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "Пользовательские конфигурации узлов",
|
||||
"fileSystemInformation": "Информация о файловой системе",
|
||||
"personalInformation": "Личная информация",
|
||||
"workflowContents": "Содержание рабочего процесса"
|
||||
},
|
||||
"title": "О сборе данных",
|
||||
"whatWeCollect": "Что мы собираем:",
|
||||
"whatWeDoNotCollect": "Что мы не собираем:",
|
||||
"workflowContent": "Содержимое рабочего процесса",
|
||||
"workflowContents": "Содержимое рабочего процесса"
|
||||
"whatWeDoNotCollect": "Что мы не собираем:"
|
||||
},
|
||||
"learnMoreAboutData": "Узнать больше о сборе данных"
|
||||
},
|
||||
"systemLocations": "Системные места",
|
||||
"unhandledError": "Неизвестная ошибка"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "Свяжитесь со мной для уточнения",
|
||||
"helpFix": "Помочь исправить это",
|
||||
"notifyResolve": "Уведомить меня, когда проблема будет решена",
|
||||
"provideAdditionalDetails": "Предоставьте дополнительные сведения (необязательно)",
|
||||
"provideEmail": "Укажите вашу электронную почту (необязательно)",
|
||||
"stackTrace": "Трассировка стека",
|
||||
"submitErrorReport": "Отправить отчет об ошибке (необязательно)",
|
||||
"systemStats": "Статистика системы"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "Автоочередь",
|
||||
"batchCount": "Количество пакетов",
|
||||
@@ -623,11 +639,13 @@
|
||||
"workflows": "Рабочие процессы"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "Добавить в закладки",
|
||||
"closeOtherTabs": "Закрыть другие вкладки",
|
||||
"closeTab": "Закрыть вкладку",
|
||||
"closeTabsToLeft": "Закрыть вкладки слева",
|
||||
"closeTabsToRight": "Закрыть вкладки справа",
|
||||
"duplicateTab": "Дублировать вкладку"
|
||||
"duplicateTab": "Дублировать вкладку",
|
||||
"removeFromBookmarks": "Удалить из закладок"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Автоматически проверять обновления"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "Отправлять анонимные отчеты о сбоях"
|
||||
"name": "Отправлять анонимную статистику использования"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "Стиль окна",
|
||||
@@ -265,8 +265,7 @@
|
||||
"Bottom": "Внизу",
|
||||
"Disabled": "Отключено",
|
||||
"Top": "Вверху"
|
||||
},
|
||||
"tooltip": "(Рабочий стол, только для Windows): При использовании пользовательского стиля окна поддерживается только верхняя часть"
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "Проверка определений узлов (медленно)",
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"systemInfo": "系统信息",
|
||||
"terminal": "终端",
|
||||
"upload": "上传",
|
||||
"videoFailedToLoad": "视频加载失败"
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
"graphCanvasMenu": {
|
||||
"fitView": "适应视图",
|
||||
@@ -205,27 +206,42 @@
|
||||
"pathValidationFailed": "路径验证失败",
|
||||
"selectItemsToMigrate": "选择要迁移的项目",
|
||||
"settings": {
|
||||
"allowMetrics": "崩溃报告",
|
||||
"allowMetricsDescription": "发送匿名崩溃报告帮助改善 ComfyUI。报告不会收集任何个人信息或工作流内容。您可以随时在设置菜单中禁用此功能。",
|
||||
"allowMetrics": "使用情况指标",
|
||||
"allowMetricsDescription": "通过发送匿名使用情况指标来帮助改进ComfyUI。不会收集任何个人信息或工作流内容。",
|
||||
"autoUpdate": "自动更新",
|
||||
"autoUpdateDescription": "更新可用时自动更新。您将在安装更新之前收到通知。",
|
||||
"dataCollectionDialog": {
|
||||
"customNodeConfigurations": "自定义节点配置",
|
||||
"errorReports": "错误信息和堆栈跟踪",
|
||||
"fileSystemInformation": "文件系统信息",
|
||||
"personalInformation": "个人信息",
|
||||
"systemInfo": "硬件、操作系统类型和应用版本",
|
||||
"collect": {
|
||||
"errorReports": "错误报告和堆栈跟踪",
|
||||
"systemInfo": "硬件,操作系统类型和应用版本",
|
||||
"userJourneyEvents": "用户旅程事件",
|
||||
"userJourneyTooltip": "用户旅程事件用于跟踪用户通过应用安装过程的旅程。事件收集在第一次成功运行ComfyUI工作流后结束。"
|
||||
},
|
||||
"doNotCollect": {
|
||||
"customNodeConfigurations": "自定义节点配置",
|
||||
"fileSystemInformation": "文件系统信息",
|
||||
"personalInformation": "个人信息",
|
||||
"workflowContents": "工作流内容"
|
||||
},
|
||||
"title": "关于数据收集",
|
||||
"whatWeCollect": "我们收集的内容:",
|
||||
"whatWeDoNotCollect": "我们不收集的内容:",
|
||||
"workflowContent": "工作流内容",
|
||||
"workflowContents": "工作流内容"
|
||||
"whatWeDoNotCollect": "我们不收集的内容:"
|
||||
},
|
||||
"learnMoreAboutData": "了解更多关于数据收集的信息"
|
||||
},
|
||||
"systemLocations": "系统位置",
|
||||
"unhandledError": "未知错误"
|
||||
},
|
||||
"issueReport": {
|
||||
"contactFollowUp": "跟进联系我",
|
||||
"helpFix": "帮助修复这个",
|
||||
"notifyResolve": "解决时通知我",
|
||||
"provideAdditionalDetails": "提供额外的详细信息(可选)",
|
||||
"provideEmail": "提供您的电子邮件(可选)",
|
||||
"stackTrace": "堆栈跟踪",
|
||||
"submitErrorReport": "提交错误报告(可选)",
|
||||
"systemStats": "系统状态"
|
||||
},
|
||||
"menu": {
|
||||
"autoQueue": "自动执行",
|
||||
"batchCount": "批次数量",
|
||||
@@ -623,11 +639,13 @@
|
||||
"workflows": "工作流"
|
||||
},
|
||||
"tabMenu": {
|
||||
"addToBookmarks": "添加到书签",
|
||||
"closeOtherTabs": "关闭其他标签",
|
||||
"closeTab": "关闭标签",
|
||||
"closeTabsToLeft": "关闭左侧标签",
|
||||
"closeTabsToRight": "关闭右侧标签",
|
||||
"duplicateTab": "复制标签"
|
||||
"duplicateTab": "复制标签",
|
||||
"removeFromBookmarks": "从书签中移除"
|
||||
},
|
||||
"templateWorkflows": {
|
||||
"template": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "自动检查更新"
|
||||
},
|
||||
"Comfy-Desktop_SendStatistics": {
|
||||
"name": "发送匿名崩溃报告"
|
||||
"name": "发送匿名使用情况统计"
|
||||
},
|
||||
"Comfy-Desktop_WindowStyle": {
|
||||
"name": "窗口样式",
|
||||
@@ -265,8 +265,7 @@
|
||||
"Bottom": "底部",
|
||||
"Disabled": "禁用",
|
||||
"Top": "顶部"
|
||||
},
|
||||
"tooltip": "(仅限桌面,Windows): 使用自定义窗口样式时,只支持顶部"
|
||||
}
|
||||
},
|
||||
"Comfy_Validation_NodeDefs": {
|
||||
"name": "校验节点定义(慢)",
|
||||
|
||||
12
src/main.ts
12
src/main.ts
@@ -2,6 +2,7 @@
|
||||
import '@comfyorg/litegraph/style.css'
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import * as Sentry from '@sentry/vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -24,6 +25,17 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: __SENTRY_DSN__,
|
||||
enabled: __SENTRY_ENABLED__,
|
||||
release: __COMFYUI_FRONTEND_VERSION__,
|
||||
integrations: [],
|
||||
autoSessionTracking: false,
|
||||
defaultIntegrations: false,
|
||||
normalizeDepth: 8,
|
||||
tracesSampleRate: 0
|
||||
})
|
||||
app.directive('tooltip', Tooltip)
|
||||
app
|
||||
.use(router)
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
ExecutingWsMessage,
|
||||
ExecutionCachedWsMessage,
|
||||
ExecutionErrorWsMessage,
|
||||
ExecutionInterruptedWsMessage,
|
||||
ExecutionStartWsMessage,
|
||||
ExecutionSuccessWsMessage,
|
||||
ExtensionsResponse,
|
||||
@@ -59,6 +60,7 @@ interface BackendApiCalls {
|
||||
execution_start: ExecutionStartWsMessage
|
||||
execution_success: ExecutionSuccessWsMessage
|
||||
execution_error: ExecutionErrorWsMessage
|
||||
execution_interrupted: ExecutionInterruptedWsMessage
|
||||
execution_cached: ExecutionCachedWsMessage
|
||||
logs: LogsWsMessage
|
||||
/** Mr Blob Preview, I presume? */
|
||||
@@ -355,6 +357,7 @@ export class ComfyApi extends EventTarget {
|
||||
break
|
||||
case 'execution_start':
|
||||
case 'execution_error':
|
||||
case 'execution_interrupted':
|
||||
case 'execution_cached':
|
||||
case 'execution_success':
|
||||
case 'progress':
|
||||
|
||||
@@ -411,223 +411,231 @@ export class ComfyUI {
|
||||
}
|
||||
})
|
||||
|
||||
this.menuContainer = $el('div.comfy-menu', { parent: containerElement }, [
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el('span.comfy-menu-queue-size', { $: (q) => (this.queueSize = q) }),
|
||||
$el('div.comfy-menu-actions', [
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
}
|
||||
this.menuContainer = $el(
|
||||
'div.comfy-menu.no-drag',
|
||||
{ parent: containerElement },
|
||||
[
|
||||
$el(
|
||||
'div.drag-handle.comfy-menu-header',
|
||||
{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
cursor: 'default'
|
||||
}
|
||||
},
|
||||
[
|
||||
$el('span.drag-handle'),
|
||||
$el('span.comfy-menu-queue-size', {
|
||||
$: (q) => (this.queueSize = q)
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
useWorkspaceStore().focusMode = true
|
||||
$el('div.comfy-menu-actions', [
|
||||
$el('button.comfy-settings-btn', {
|
||||
textContent: '⚙️',
|
||||
onclick: () => {
|
||||
useDialogService().showSettingsDialog()
|
||||
}
|
||||
}),
|
||||
$el('button.comfy-close-menu-btn', {
|
||||
textContent: '\u00d7',
|
||||
onclick: () => {
|
||||
useWorkspaceStore().focusMode = true
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('button.comfy-queue-btn', {
|
||||
id: 'queue-button',
|
||||
textContent: 'Queue Prompt',
|
||||
onclick: () => app.queuePrompt(0, this.batchCount)
|
||||
}),
|
||||
$el('div', {}, [
|
||||
$el('label', { innerHTML: 'Extra options' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
onchange: (i) => {
|
||||
document.getElementById('extraOptions').style.display = i
|
||||
.srcElement.checked
|
||||
? 'block'
|
||||
: 'none'
|
||||
this.batchCount = i.srcElement.checked
|
||||
? Number.parseInt(
|
||||
(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value
|
||||
)
|
||||
: 1
|
||||
;(
|
||||
document.getElementById(
|
||||
'autoQueueCheckbox'
|
||||
) as HTMLInputElement
|
||||
).checked = false
|
||||
this.autoQueueEnabled = false
|
||||
}
|
||||
})
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('button.comfy-queue-btn', {
|
||||
id: 'queue-button',
|
||||
textContent: 'Queue Prompt',
|
||||
onclick: () => app.queuePrompt(0, this.batchCount)
|
||||
}),
|
||||
$el('div', {}, [
|
||||
$el('label', { innerHTML: 'Extra options' }, [
|
||||
$el('input', {
|
||||
type: 'checkbox',
|
||||
onchange: (i) => {
|
||||
document.getElementById('extraOptions').style.display = i
|
||||
.srcElement.checked
|
||||
? 'block'
|
||||
: 'none'
|
||||
this.batchCount = i.srcElement.checked
|
||||
? Number.parseInt(
|
||||
(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value
|
||||
)
|
||||
: 1
|
||||
;(
|
||||
document.getElementById('autoQueueCheckbox') as HTMLInputElement
|
||||
).checked = false
|
||||
this.autoQueueEnabled = false
|
||||
}
|
||||
})
|
||||
])
|
||||
]),
|
||||
$el(
|
||||
'div',
|
||||
{ id: 'extraOptions', style: { width: '100%', display: 'none' } },
|
||||
[
|
||||
$el('div', [
|
||||
$el('label', { innerHTML: 'Batch count' }),
|
||||
$el('input', {
|
||||
id: 'batchCountInputNumber',
|
||||
type: 'number',
|
||||
value: this.batchCount,
|
||||
min: '1',
|
||||
style: { width: '35%', marginLeft: '0.4em' },
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.target.value
|
||||
/* Even though an <input> element with a type of range logically represents a number (since
|
||||
]),
|
||||
$el(
|
||||
'div',
|
||||
{ id: 'extraOptions', style: { width: '100%', display: 'none' } },
|
||||
[
|
||||
$el('div', [
|
||||
$el('label', { innerHTML: 'Batch count' }),
|
||||
$el('input', {
|
||||
id: 'batchCountInputNumber',
|
||||
type: 'number',
|
||||
value: this.batchCount,
|
||||
min: '1',
|
||||
style: { width: '35%', marginLeft: '0.4em' },
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.target.value
|
||||
/* Even though an <input> element with a type of range logically represents a number (since
|
||||
it's used for numeric input), the value it holds is still treated as a string in HTML and
|
||||
JavaScript. This behavior is consistent across all <input> elements regardless of their type
|
||||
(like text, number, or range), where the .value property is always a string. */
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value = this.batchCount.toString()
|
||||
}
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'batchCountInputRange',
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: this.batchCount,
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.srcElement.value
|
||||
// Note
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputNumber'
|
||||
) as HTMLInputElement
|
||||
).value = i.srcElement.value
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el('div', [
|
||||
$el('label', {
|
||||
for: 'autoQueueCheckbox',
|
||||
innerHTML: 'Auto Queue'
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'autoQueueCheckbox',
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
title: 'Automatically queue prompt when the queue size hits 0',
|
||||
onchange: (e) => {
|
||||
this.autoQueueEnabled = e.target.checked
|
||||
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||
? ''
|
||||
: 'none'
|
||||
}
|
||||
}),
|
||||
autoQueueModeEl
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('div.comfy-menu-btns', [
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputRange'
|
||||
) as HTMLInputElement
|
||||
).value = this.batchCount.toString()
|
||||
}
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'batchCountInputRange',
|
||||
type: 'range',
|
||||
min: '1',
|
||||
max: '100',
|
||||
value: this.batchCount,
|
||||
oninput: (i) => {
|
||||
this.batchCount = i.srcElement.value
|
||||
// Note
|
||||
;(
|
||||
document.getElementById(
|
||||
'batchCountInputNumber'
|
||||
) as HTMLInputElement
|
||||
).value = i.srcElement.value
|
||||
}
|
||||
})
|
||||
]),
|
||||
$el('div', [
|
||||
$el('label', {
|
||||
for: 'autoQueueCheckbox',
|
||||
innerHTML: 'Auto Queue'
|
||||
}),
|
||||
$el('input', {
|
||||
id: 'autoQueueCheckbox',
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
title: 'Automatically queue prompt when the queue size hits 0',
|
||||
onchange: (e) => {
|
||||
this.autoQueueEnabled = e.target.checked
|
||||
autoQueueModeEl.style.display = this.autoQueueEnabled
|
||||
? ''
|
||||
: 'none'
|
||||
}
|
||||
}),
|
||||
autoQueueModeEl
|
||||
])
|
||||
]
|
||||
),
|
||||
$el('div.comfy-menu-btns', [
|
||||
$el('button', {
|
||||
id: 'queue-front-button',
|
||||
textContent: 'Queue Front',
|
||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-queue-button',
|
||||
textContent: 'View Queue',
|
||||
onclick: () => {
|
||||
this.history.hide()
|
||||
this.queue.toggle()
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.history.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-history-button',
|
||||
textContent: 'View History',
|
||||
onclick: () => {
|
||||
this.queue.hide()
|
||||
this.history.toggle()
|
||||
}
|
||||
})
|
||||
]),
|
||||
this.queue.element,
|
||||
this.history.element,
|
||||
$el('button', {
|
||||
id: 'queue-front-button',
|
||||
textContent: 'Queue Front',
|
||||
onclick: () => app.queuePrompt(-1, this.batchCount)
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.queue.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-queue-button',
|
||||
textContent: 'View Queue',
|
||||
id: 'comfy-save-button',
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
this.history.hide()
|
||||
this.queue.toggle()
|
||||
useCommandStore().execute('Comfy.ExportWorkflow')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
$: (b) => (this.history.button = b as HTMLButtonElement),
|
||||
id: 'comfy-view-history-button',
|
||||
textContent: 'View History',
|
||||
id: 'comfy-dev-save-api-button',
|
||||
textContent: 'Save (API Format)',
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
this.queue.hide()
|
||||
this.history.toggle()
|
||||
useCommandStore().execute('Comfy.ExportWorkflowAPI')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-button',
|
||||
textContent: 'Load',
|
||||
onclick: () => fileInput.click()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-reset-view-button',
|
||||
textContent: 'Reset View',
|
||||
onclick: async () => {
|
||||
app.resetView()
|
||||
}
|
||||
})
|
||||
]),
|
||||
this.queue.element,
|
||||
this.history.element,
|
||||
$el('button', {
|
||||
id: 'comfy-save-button',
|
||||
textContent: 'Save',
|
||||
onclick: () => {
|
||||
useCommandStore().execute('Comfy.ExportWorkflow')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-dev-save-api-button',
|
||||
textContent: 'Save (API Format)',
|
||||
style: { width: '100%', display: 'none' },
|
||||
onclick: () => {
|
||||
useCommandStore().execute('Comfy.ExportWorkflowAPI')
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-button',
|
||||
textContent: 'Load',
|
||||
onclick: () => fileInput.click()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-refresh-button',
|
||||
textContent: 'Refresh',
|
||||
onclick: () => app.refreshComboInNodes()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clipspace-button',
|
||||
textContent: 'Clipspace',
|
||||
onclick: () => app.openClipspace()
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-clear-button',
|
||||
textContent: 'Clear',
|
||||
onclick: () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
app.resetView()
|
||||
api.dispatchCustomEvent('graphCleared')
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-load-default-button',
|
||||
textContent: 'Load Default',
|
||||
onclick: async () => {
|
||||
if (
|
||||
!useSettingStore().get('Comfy.ConfirmClear') ||
|
||||
confirm('Load default workflow?')
|
||||
) {
|
||||
app.resetView()
|
||||
await app.loadGraphData()
|
||||
}
|
||||
}
|
||||
}),
|
||||
$el('button', {
|
||||
id: 'comfy-reset-view-button',
|
||||
textContent: 'Reset View',
|
||||
onclick: async () => {
|
||||
app.resetView()
|
||||
}
|
||||
})
|
||||
]) as HTMLDivElement
|
||||
]
|
||||
) as HTMLDivElement
|
||||
// Hide by default on construction so it does not interfere with other views.
|
||||
this.menuContainer.style.display = 'none'
|
||||
|
||||
|
||||
@@ -176,10 +176,6 @@ export const useWorkflowService = () => {
|
||||
workflow: ComfyWorkflow,
|
||||
options: { warnIfUnsaved: boolean } = { warnIfUnsaved: true }
|
||||
): Promise<boolean> => {
|
||||
if (!workflow.isLoaded) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (workflow.isModified && options.warnIfUnsaved) {
|
||||
const confirmed = await dialogService.confirm({
|
||||
title: t('sideToolbar.workflowTab.dirtyCloseTitle'),
|
||||
|
||||
@@ -305,9 +305,13 @@ export const SYSTEM_NODE_DEFS: Record<string, ComfyNodeDef> = {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildNodeDefTree(nodeDefs: ComfyNodeDefImpl[]): TreeNode {
|
||||
return buildTree(nodeDefs, (nodeDef: ComfyNodeDefImpl) =>
|
||||
export function buildNodeDefTree(
|
||||
nodeDefs: ComfyNodeDefImpl[],
|
||||
keyFunction: (nodeDef: ComfyNodeDefImpl) => string[] = (nodeDef) =>
|
||||
nodeDef.nodePath.split('/')
|
||||
): TreeNode {
|
||||
return buildTree(nodeDefs, (nodeDef: ComfyNodeDefImpl) =>
|
||||
keyFunction(nodeDef)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -326,12 +330,16 @@ export function createDummyFolderNodeDef(folderPath: string): ComfyNodeDefImpl {
|
||||
} as ComfyNodeDef)
|
||||
}
|
||||
|
||||
const getCategoryKeys = (nodeDef: ComfyNodeDefImpl) =>
|
||||
nodeDef.nodePath.split('/')
|
||||
|
||||
export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeDefsByName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||
const nodeDefsByDisplayName = ref<Record<string, ComfyNodeDefImpl>>({})
|
||||
const showDeprecated = ref(false)
|
||||
const showExperimental = ref(false)
|
||||
|
||||
const keyFunction = ref(getCategoryKeys)
|
||||
const nodeDefs = computed(() => Object.values(nodeDefsByName.value))
|
||||
const nodeDataTypes = computed(() => {
|
||||
const types = new Set<string>()
|
||||
@@ -355,7 +363,13 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const nodeSearchService = computed(
|
||||
() => new NodeSearchService(visibleNodeDefs.value)
|
||||
)
|
||||
const nodeTree = computed(() => buildNodeDefTree(visibleNodeDefs.value))
|
||||
const nodeTree = computed(() =>
|
||||
buildNodeDefTree(visibleNodeDefs.value, keyFunction.value)
|
||||
)
|
||||
|
||||
function setKeyFunction(callback: (nodeDef: ComfyNodeDefImpl) => string[]) {
|
||||
keyFunction.value = callback ?? getCategoryKeys
|
||||
}
|
||||
|
||||
function updateNodeDefs(nodeDefs: ComfyNodeDef[]) {
|
||||
const newNodeDefsByName: Record<string, ComfyNodeDefImpl> = {}
|
||||
@@ -398,7 +412,8 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
|
||||
updateNodeDefs,
|
||||
addNodeDef,
|
||||
fromLGraphNode
|
||||
fromLGraphNode,
|
||||
setKeyFunction
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, markRaw, ref } from 'vue'
|
||||
|
||||
@@ -130,6 +131,10 @@ export interface WorkflowStore {
|
||||
openWorkflows: LoadedComfyWorkflow[]
|
||||
openedWorkflowIndexShift: (shift: number) => LoadedComfyWorkflow | null
|
||||
openWorkflow: (workflow: ComfyWorkflow) => Promise<LoadedComfyWorkflow>
|
||||
openWorkflowsInBackground: (paths: {
|
||||
left?: string[]
|
||||
right?: string[]
|
||||
}) => void
|
||||
isOpen: (workflow: ComfyWorkflow) => boolean
|
||||
isBusy: boolean
|
||||
closeWorkflow: (workflow: ComfyWorkflow) => Promise<void>
|
||||
@@ -213,6 +218,36 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
const isOpen = (workflow: ComfyWorkflow) =>
|
||||
openWorkflowPathSet.value.has(workflow.path)
|
||||
|
||||
/**
|
||||
* Add paths to the list of open workflow paths without loading the files
|
||||
* or changing the active workflow.
|
||||
*
|
||||
* @param paths - The workflows to open, specified as:
|
||||
* - `left`: Workflows to be added to the left.
|
||||
* - `right`: Workflows to be added to the right.
|
||||
*
|
||||
* Invalid paths (non-strings or paths not found in `workflowLookup.value`)
|
||||
* will be ignored. Duplicate paths are automatically removed.
|
||||
*/
|
||||
const openWorkflowsInBackground = (paths: {
|
||||
left?: string[]
|
||||
right?: string[]
|
||||
}) => {
|
||||
const { left = [], right = [] } = paths
|
||||
if (!left.length && !right.length) return
|
||||
|
||||
const isValidPath = (
|
||||
path: unknown
|
||||
): path is keyof typeof workflowLookup.value =>
|
||||
typeof path === 'string' && path in workflowLookup.value
|
||||
|
||||
openWorkflowPaths.value = _.union(
|
||||
left,
|
||||
openWorkflowPaths.value,
|
||||
right
|
||||
).filter(isValidPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the workflow as the active workflow.
|
||||
* @param workflow The workflow to open.
|
||||
@@ -389,6 +424,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
openWorkflows,
|
||||
openedWorkflowIndexShift,
|
||||
openWorkflow,
|
||||
openWorkflowsInBackground,
|
||||
isOpen,
|
||||
isBusy,
|
||||
closeWorkflow,
|
||||
|
||||
24
src/types/issueReportTypes.ts
Normal file
24
src/types/issueReportTypes.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type DefaultField = 'Workflow' | 'Logs' | 'SystemStats' | 'Settings'
|
||||
|
||||
export interface ReportField {
|
||||
/**
|
||||
* The label of the field, shown next to the checkbox if the field is opt-in.
|
||||
*/
|
||||
label: string
|
||||
|
||||
/**
|
||||
* A unique identifier for the field, used internally as the key for this field's value.
|
||||
*/
|
||||
value: string
|
||||
|
||||
/**
|
||||
* The data associated with this field, sent as part of the report.
|
||||
*/
|
||||
data: Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Indicates whether the field requires explicit opt-in from the user
|
||||
* before its data is included in the report.
|
||||
*/
|
||||
optIn: boolean
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export type InstallOptions = Parameters<ElectronAPI['installComfyUI']>[0]
|
||||
export type TorchDeviceType = InstallOptions['device']
|
||||
import {
|
||||
ElectronAPI,
|
||||
ElectronContextMenuOptions
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
export function isElectron() {
|
||||
return 'electronAPI' in window && window['electronAPI'] !== undefined
|
||||
return 'electronAPI' in window && window.electronAPI !== undefined
|
||||
}
|
||||
|
||||
export function electronAPI() {
|
||||
return (window as any)['electronAPI'] as ElectronAPI
|
||||
return (window as any).electronAPI as ElectronAPI
|
||||
}
|
||||
|
||||
type NativeContextOptions = Parameters<ElectronAPI['showContextMenu']>[0]
|
||||
export function showNativeMenu(options?: NativeContextOptions) {
|
||||
export function showNativeMenu(options?: ElectronContextMenuOptions) {
|
||||
electronAPI()?.showContextMenu(options)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<BaseViewTemplate class="app-drag">
|
||||
<BaseViewTemplate>
|
||||
<div
|
||||
class="no-drag max-w-screen-sm flex flex-col gap-8 p-8 bg-[url('/assets/images/Git-Logo-White.svg')] bg-no-repeat bg-right-top bg-origin-padding"
|
||||
class="max-w-screen-sm flex flex-col gap-8 p-8 bg-[url('/assets/images/Git-Logo-White.svg')] bg-no-repeat bg-right-top bg-origin-padding"
|
||||
>
|
||||
<!-- Header -->
|
||||
<h1 class="mt-24 text-4xl font-bold text-red-500">
|
||||
|
||||
@@ -66,23 +66,11 @@ watch(
|
||||
document.body.classList.add(DARK_THEME_CLASS)
|
||||
}
|
||||
|
||||
// Native window control theme
|
||||
if (isElectron()) {
|
||||
const cssVars = newTheme.colors.comfy_base
|
||||
// Allow OS to set matching hover colour
|
||||
const color = setZeroAlpha(cssVars['comfy-menu-bg'])
|
||||
const symbolColor = cssVars['input-text']
|
||||
electronAPI().changeTheme({ color, symbolColor })
|
||||
}
|
||||
|
||||
function setZeroAlpha(color: string) {
|
||||
if (!color.startsWith('#')) return color
|
||||
|
||||
if (color.length === 4) {
|
||||
const [_, r, g, b] = color
|
||||
return `#${r}${r}${g}${g}${b}${b}00`
|
||||
}
|
||||
return `#${color.substring(1, 7)}00`
|
||||
electronAPI().changeTheme({
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: newTheme.colors.comfy_base['input-text']
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="app-drag">
|
||||
<BaseViewTemplate dark>
|
||||
<!-- h-full to make sure the stepper does not layout shift between steps
|
||||
as for each step the stepper height is different. Inherit the center element
|
||||
placement from BaseViewTemplate would cause layout shift. -->
|
||||
<Stepper
|
||||
class="no-drag h-full p-8 2xl:p-16"
|
||||
class="h-full p-8 2xl:p-16"
|
||||
value="0"
|
||||
@update:value="setHighestStep"
|
||||
>
|
||||
@@ -103,6 +103,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
InstallOptions,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import Button from 'primevue/button'
|
||||
import Step from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
@@ -116,11 +120,7 @@ import DesktopSettingsConfiguration from '@/components/install/DesktopSettingsCo
|
||||
import GpuPicker from '@/components/install/GpuPicker.vue'
|
||||
import InstallLocationPicker from '@/components/install/InstallLocationPicker.vue'
|
||||
import MigrationPicker from '@/components/install/MigrationPicker.vue'
|
||||
import {
|
||||
type InstallOptions,
|
||||
type TorchDeviceType,
|
||||
electronAPI
|
||||
} from '@/utils/envUtil'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BaseViewTemplate from '@/views/templates/BaseViewTemplate.vue'
|
||||
|
||||
const device = ref<TorchDeviceType>(null)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="app-drag">
|
||||
<BaseViewTemplate dark>
|
||||
<!-- Installation Path Section -->
|
||||
<div
|
||||
class="no-drag comfy-installer grow flex flex-col gap-4 text-neutral-300 max-w-110"
|
||||
class="comfy-installer grow flex flex-col gap-4 text-neutral-300 max-w-110"
|
||||
>
|
||||
<h2 class="text-2xl font-semibold text-neutral-100">
|
||||
{{ $t('install.manualConfiguration.title') }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseViewTemplate class="app-drag">
|
||||
<div class="no-drag sad-container">
|
||||
<BaseViewTemplate>
|
||||
<div class="sad-container">
|
||||
<!-- Right side image -->
|
||||
<img
|
||||
class="sad-girl"
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="app-drag flex-col">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
class="no-drag flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex items-center my-4 gap-2">
|
||||
<BaseViewTemplate dark class="flex-col">
|
||||
<div class="flex flex-col w-full h-full items-center">
|
||||
<h2 class="text-2xl font-bold">
|
||||
{{ t(`serverStart.process.${status}`) }}
|
||||
<span v-if="status === ProgressStatus.ERROR">
|
||||
v{{ electronVersion }}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
v-if="status === ProgressStatus.ERROR"
|
||||
class="flex flex-col items-center gap-4"
|
||||
>
|
||||
<div class="flex items-center my-4 gap-2">
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:label="t('serverStart.reinstall')"
|
||||
@click="reinstall"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-flag"
|
||||
v-if="!terminalVisible"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.reportIssue')"
|
||||
@click="reportIssue"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-file"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.openLogs')"
|
||||
@click="openLogs"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
:label="t('serverStart.reinstall')"
|
||||
@click="reinstall"
|
||||
:label="t('serverStart.showTerminal')"
|
||||
@click="terminalVisible = true"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
v-if="!terminalVisible"
|
||||
icon="pi pi-search"
|
||||
severity="secondary"
|
||||
:label="t('serverStart.showTerminal')"
|
||||
@click="terminalVisible = true"
|
||||
/>
|
||||
<BaseTerminal v-show="terminalVisible" @created="terminalCreated" />
|
||||
</div>
|
||||
<BaseTerminal
|
||||
v-show="terminalVisible"
|
||||
class="no-drag"
|
||||
@created="terminalCreated"
|
||||
/>
|
||||
</BaseViewTemplate>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="app-drag">
|
||||
<BaseViewTemplate dark>
|
||||
<main
|
||||
id="comfy-user-selection"
|
||||
class="no-drag min-w-84 relative rounded-lg bg-[var(--comfy-menu-bg)] p-5 px-10 shadow-lg"
|
||||
class="min-w-84 relative rounded-lg bg-[var(--comfy-menu-bg)] p-5 px-10 shadow-lg"
|
||||
>
|
||||
<h1 class="my-2.5 mb-7 font-normal">ComfyUI</h1>
|
||||
<div class="flex w-full flex-col items-center">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BaseViewTemplate dark class="app-drag">
|
||||
<div class="no-drag flex flex-col items-center justify-center gap-8 p-8">
|
||||
<BaseViewTemplate dark>
|
||||
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<h1 class="animated-gradient-text text-glow select-none">
|
||||
{{ $t('welcome.title') }}
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
<template>
|
||||
<div
|
||||
class="font-sans w-screen h-screen flex items-center justify-center pointer-events-auto overflow-auto"
|
||||
class="font-sans w-screen h-screen flex flex-col pointer-events-auto"
|
||||
:class="[
|
||||
props.dark
|
||||
? 'text-neutral-300 bg-neutral-900 dark-theme'
|
||||
: 'text-neutral-900 bg-neutral-300'
|
||||
]"
|
||||
>
|
||||
<slot></slot>
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
<div
|
||||
v-show="isNativeWindow"
|
||||
ref="topMenuRef"
|
||||
class="app-drag w-full h-[var(--comfy-topbar-height)]"
|
||||
/>
|
||||
<div
|
||||
class="flex-grow w-full flex items-center justify-center overflow-auto"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
|
||||
import { electronAPI, isElectron } from '@/utils/envUtil'
|
||||
|
||||
@@ -26,18 +36,28 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const darkTheme = {
|
||||
color: 'rgba(23, 23, 23, 0)',
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: '#d4d4d4'
|
||||
}
|
||||
|
||||
const lightTheme = {
|
||||
color: 'rgba(212, 212, 212, 0)',
|
||||
color: 'rgba(0, 0, 0, 0)',
|
||||
symbolColor: '#171717'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const topMenuRef = ref<HTMLDivElement | null>(null)
|
||||
const isNativeWindow = ref(false)
|
||||
onMounted(async () => {
|
||||
if (isElectron()) {
|
||||
electronAPI().changeTheme(props.dark ? darkTheme : lightTheme)
|
||||
const windowStyle = await electronAPI().Config.getWindowStyle()
|
||||
isNativeWindow.value = windowStyle === 'custom'
|
||||
|
||||
await nextTick()
|
||||
|
||||
electronAPI().changeTheme({
|
||||
...(props.dark ? darkTheme : lightTheme),
|
||||
height: topMenuRef.value.getBoundingClientRect().height
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { api } from '@/scripts/api'
|
||||
import { defaultGraph, defaultGraphJSON } from '@/scripts/defaultGraph'
|
||||
import {
|
||||
ComfyWorkflow,
|
||||
LoadedComfyWorkflow,
|
||||
useWorkflowBookmarkStore,
|
||||
useWorkflowStore
|
||||
@@ -161,6 +162,97 @@ describe('useWorkflowStore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('openWorkflowsInBackground', () => {
|
||||
let workflowA: ComfyWorkflow
|
||||
let workflowB: ComfyWorkflow
|
||||
let workflowC: ComfyWorkflow
|
||||
|
||||
const openWorkflowPaths = () =>
|
||||
store.openWorkflows.filter((w) => store.isOpen(w)).map((w) => w.path)
|
||||
|
||||
beforeEach(async () => {
|
||||
await syncRemoteWorkflows(['a.json', 'b.json', 'c.json'])
|
||||
workflowA = store.getWorkflowByPath('workflows/a.json')!
|
||||
workflowB = store.getWorkflowByPath('workflows/b.json')!
|
||||
workflowC = store.getWorkflowByPath('workflows/c.json')!
|
||||
;(api.getUserData as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
text: () => Promise.resolve(defaultGraphJSON)
|
||||
})
|
||||
})
|
||||
|
||||
it('should open workflows adjacent to the active workflow', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowB.path],
|
||||
right: [workflowC.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([
|
||||
workflowB.path,
|
||||
workflowA.path,
|
||||
workflowC.path
|
||||
])
|
||||
})
|
||||
|
||||
it('should not change the active workflow', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowC.path],
|
||||
right: [workflowB.path]
|
||||
})
|
||||
expect(store.activeWorkflow).not.toBeUndefined()
|
||||
expect(store.activeWorkflow!.path).toBe(workflowA.path)
|
||||
})
|
||||
|
||||
it('should open workflows when none are active', async () => {
|
||||
expect(store.openWorkflows.length).toBe(0)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowA.path],
|
||||
right: [workflowB.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path, workflowB.path])
|
||||
})
|
||||
|
||||
it('should not open duplicate workflows', async () => {
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowA.path, workflowB.path, workflowA.path],
|
||||
right: [workflowB.path, workflowA.path, workflowB.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path, workflowB.path])
|
||||
})
|
||||
|
||||
it('should not open workflow that is already open', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: [workflowA.path]
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
expect(store.activeWorkflow?.path).toBe(workflowA.path)
|
||||
})
|
||||
|
||||
it('should ignore invalid or deleted workflow paths', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
store.openWorkflowsInBackground({
|
||||
left: ['workflows/invalid::$-path.json'],
|
||||
right: ['workflows/deleted-since-last-session.json']
|
||||
})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
expect(store.activeWorkflow?.path).toBe(workflowA.path)
|
||||
})
|
||||
|
||||
it('should do nothing when given an empty argument', async () => {
|
||||
await store.openWorkflow(workflowA)
|
||||
|
||||
store.openWorkflowsInBackground({})
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
|
||||
store.openWorkflowsInBackground({ left: [], right: [] })
|
||||
expect(openWorkflowPaths()).toEqual([workflowA.path])
|
||||
|
||||
expect(store.activeWorkflow?.path).toBe(workflowA.path)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameWorkflow', () => {
|
||||
it('should rename workflow and update bookmarks', async () => {
|
||||
const workflow = store.createTemporary('dir/test.json')
|
||||
|
||||
@@ -178,7 +178,11 @@ export default defineConfig({
|
||||
define: {
|
||||
__COMFYUI_FRONTEND_VERSION__: JSON.stringify(
|
||||
process.env.npm_package_version
|
||||
)
|
||||
),
|
||||
__SENTRY_ENABLED__: JSON.stringify(
|
||||
!(process.env.NODE_ENV === 'development' || !process.env.SENTRY_DSN)
|
||||
),
|
||||
__SENTRY_DSN__: JSON.stringify(process.env.SENTRY_DSN || '')
|
||||
},
|
||||
|
||||
resolve: {
|
||||
|
||||
Reference in New Issue
Block a user