mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-02 13:17:48 +00:00
Compare commits
3 Commits
v1.12.5
...
webview-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b198aaaca2 | ||
|
|
7003a9e98b | ||
|
|
35fb6378d1 |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.3",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "5.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.12.5",
|
||||
"version": "1.12.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<!-- If load immediately, the top-level splitter stateKey won't be correctly
|
||||
synced with the stateStorage (localStorage). -->
|
||||
<LiteGraphCanvasSplitterOverlay
|
||||
v-if="comfyAppReady && betaMenuEnabled && !workspaceStore.focusMode"
|
||||
v-if="
|
||||
comfyAppReady &&
|
||||
betaMenuEnabled &&
|
||||
!workspaceStore.focusMode &&
|
||||
!webviewStore.hasActiveWebview
|
||||
"
|
||||
>
|
||||
<template #side-bar-panel>
|
||||
<SideToolbar />
|
||||
@@ -69,6 +74,7 @@ import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWebviewStore } from '@/stores/webviewStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -78,6 +84,7 @@ const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const webviewStore = useWebviewStore()
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<ButtonGroup
|
||||
v-if="!webviewStore.hasActiveWebview"
|
||||
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
|
||||
>
|
||||
<Button
|
||||
@@ -69,11 +70,13 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWebviewStore } from '@/stores/webviewStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const settingStore = useSettingStore()
|
||||
const webviewStore = useWebviewStore()
|
||||
|
||||
const linkHidden = computed(
|
||||
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
||||
|
||||
@@ -10,23 +10,25 @@
|
||||
<div class="flex-grow min-w-0 app-drag h-full">
|
||||
<WorkflowTabs v-if="workflowTabsPosition === 'Topbar'" />
|
||||
</div>
|
||||
<div class="comfyui-menu-right flex-shrink-0" ref="menuRight"></div>
|
||||
<Actionbar />
|
||||
<BottomPanelToggleButton class="flex-shrink-0" />
|
||||
<Button
|
||||
class="flex-shrink-0"
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.hideMenu')"
|
||||
@click="workspaceState.focusMode = true"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div
|
||||
v-show="menuSetting !== 'Bottom'"
|
||||
class="window-actions-spacer flex-shrink-0"
|
||||
/>
|
||||
<template v-if="!webviewStore.hasActiveWebview">
|
||||
<div class="comfyui-menu-right flex-shrink-0" ref="menuRight"></div>
|
||||
<Actionbar />
|
||||
<BottomPanelToggleButton class="flex-shrink-0" />
|
||||
<Button
|
||||
class="flex-shrink-0"
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
v-tooltip="{ value: $t('menu.hideMenu'), showDelay: 300 }"
|
||||
:aria-label="$t('menu.hideMenu')"
|
||||
@click="workspaceState.focusMode = true"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div
|
||||
v-show="menuSetting !== 'Bottom'"
|
||||
class="window-actions-spacer flex-shrink-0"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Virtual top menu for native window (drag handle) -->
|
||||
@@ -47,6 +49,7 @@ import CommandMenubar from '@/components/topbar/CommandMenubar.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWebviewStore } from '@/stores/webviewStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import {
|
||||
electronAPI,
|
||||
@@ -57,6 +60,7 @@ import {
|
||||
|
||||
const workspaceState = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
const webviewStore = useWebviewStore()
|
||||
const workflowTabsPosition = computed(() =>
|
||||
settingStore.get('Comfy.Workflow.WorkflowTabsPosition')
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
>
|
||||
<template #option="{ option }">
|
||||
<WorkflowTab
|
||||
@click="() => webviewStore.hideActiveWebview()"
|
||||
@contextmenu="showContextMenu($event, option)"
|
||||
@click.middle="onCloseWorkflow(option)"
|
||||
:workflow-option="option"
|
||||
@@ -50,6 +51,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import WorkflowTab from '@/components/topbar/WorkflowTab.vue'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWebviewStore } from '@/stores/webviewStore'
|
||||
import { ComfyWorkflow, useWorkflowBookmarkStore } from '@/stores/workflowStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
@@ -68,6 +70,7 @@ const workspaceStore = useWorkspaceStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||
const webviewStore = useWebviewStore()
|
||||
const rightClickedTab = ref<WorkflowOption>(null)
|
||||
const menu = ref()
|
||||
|
||||
|
||||
29
src/components/webviews/WebviewContainer.vue
Normal file
29
src/components/webviews/WebviewContainer.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="w-full h-full relative pointer-events-auto">
|
||||
<KeepAlive>
|
||||
<component
|
||||
v-if="
|
||||
webviewStore.hasActiveWebview && webviewStore.activeWebview.keepAlive
|
||||
"
|
||||
:is="webviewStore.activeWebview.component"
|
||||
:key="webviewStore.activeWebviewId"
|
||||
v-bind="webviewStore.activeWebview.props || {}"
|
||||
/>
|
||||
</KeepAlive>
|
||||
|
||||
<component
|
||||
v-if="
|
||||
webviewStore.hasActiveWebview && !webviewStore.activeWebview.keepAlive
|
||||
"
|
||||
:is="webviewStore.activeWebview.component"
|
||||
:key="webviewStore.activeWebviewId"
|
||||
v-bind="webviewStore.activeWebview.props || {}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWebviewStore } from '@/stores/webviewStore'
|
||||
|
||||
const webviewStore = useWebviewStore()
|
||||
</script>
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
type NodeId,
|
||||
validateComfyWorkflow
|
||||
} from '@/schemas/comfyWorkflowSchema'
|
||||
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import {
|
||||
type ComfyNodeDef as ComfyNodeDefV2,
|
||||
isComboInputSpec,
|
||||
@@ -888,7 +889,9 @@ export class ComfyApp {
|
||||
nodeDefStore.updateNodeDefs(nodeDefArray)
|
||||
}
|
||||
|
||||
async #getNodeDefs(): Promise<Record<string, ComfyNodeDefV1>> {
|
||||
async #getNodeDefs(): Promise<
|
||||
Record<string, ComfyNodeDefV1 & ComfyNodeDefV2>
|
||||
> {
|
||||
const translateNodeDef = (def: ComfyNodeDefV1): ComfyNodeDefV1 => ({
|
||||
...def,
|
||||
display_name: st(
|
||||
@@ -908,7 +911,7 @@ export class ComfyApp {
|
||||
await api.getNodeDefs({
|
||||
validate: useSettingStore().get('Comfy.Validation.NodeDefs')
|
||||
}),
|
||||
(def) => translateNodeDef(def)
|
||||
(def) => new ComfyNodeDefImpl(translateNodeDef(def))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -944,7 +947,15 @@ export class ComfyApp {
|
||||
}
|
||||
|
||||
async registerNodeDef(nodeId: string, nodeDef: ComfyNodeDefV1) {
|
||||
return await useLitegraphService().registerNodeDef(nodeId, nodeDef)
|
||||
return await useLitegraphService().registerNodeDef(
|
||||
nodeId,
|
||||
isComfyNodeDefV2(nodeDef)
|
||||
? nodeDef
|
||||
: {
|
||||
...(nodeDef as ComfyNodeDefV1),
|
||||
...transformNodeDefV1ToV2(nodeDef)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async registerNodesFromDefs(defs: Record<string, ComfyNodeDefV1>) {
|
||||
@@ -1595,26 +1606,31 @@ export class ComfyApp {
|
||||
useToastStore().add(requestToastMessage)
|
||||
}
|
||||
|
||||
const defs = await this.#getNodeDefs()
|
||||
for (const nodeId in defs) {
|
||||
this.registerNodeDef(nodeId, defs[nodeId])
|
||||
const defs: Record<string, ComfyNodeDefV1 & ComfyNodeDefV2> =
|
||||
await this.#getNodeDefs()
|
||||
|
||||
for (const [nodeId, nodeDef] of Object.entries(defs)) {
|
||||
this.registerNodeDef(nodeId, nodeDef)
|
||||
}
|
||||
|
||||
for (const node of this.graph.nodes) {
|
||||
const def = defs[node.type]
|
||||
// Allow primitive nodes to handle refresh
|
||||
node.refreshComboInNode?.(defs)
|
||||
|
||||
if (!def?.input) continue
|
||||
if (!def) continue
|
||||
|
||||
// Update combo options in combo widgets
|
||||
for (const widget of node.widgets) {
|
||||
if (widget.type === 'combo') {
|
||||
if (def['input'].required?.[widget.name] !== undefined) {
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
widget.options.values = def['input'].required[widget.name][0]
|
||||
} else if (def['input'].optional?.[widget.name] !== undefined) {
|
||||
// @ts-expect-error InputSpec is not typed correctly
|
||||
widget.options.values = def['input'].optional[widget.name][0]
|
||||
}
|
||||
const inputSpec = def.inputs[widget.name]
|
||||
if (
|
||||
inputSpec &&
|
||||
isComboInputSpec(inputSpec) &&
|
||||
widget.type === 'combo'
|
||||
) {
|
||||
widget.options.values = inputSpec.options.map((o) =>
|
||||
typeof o === 'string' ? o : o.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { $el } from '@/scripts/ui'
|
||||
import { calculateImageGrid, createImageHost } from '@/scripts/ui/imagePreview'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { is_all_same_aspect_ratio } from '@/utils/imageUtil'
|
||||
@@ -36,11 +35,14 @@ export const useLitegraphService = () => {
|
||||
const toastStore = useToastStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
async function registerNodeDef(nodeId: string, nodeDefV1: ComfyNodeDefV1) {
|
||||
async function registerNodeDef(
|
||||
nodeId: string,
|
||||
nodeDef: ComfyNodeDefV2 & ComfyNodeDefV1
|
||||
) {
|
||||
const node = class ComfyNode extends LGraphNode {
|
||||
static comfyClass?: string
|
||||
static title?: string
|
||||
static nodeData?: ComfyNodeDefV1 & ComfyNodeDefV2
|
||||
static comfyClass?: string = nodeDef.name
|
||||
static title?: string = nodeDef.display_name || nodeDef.name
|
||||
static nodeData?: ComfyNodeDefV1 & ComfyNodeDefV2 = nodeDef
|
||||
static category?: string
|
||||
|
||||
constructor(title?: string) {
|
||||
@@ -168,6 +170,7 @@ export const useLitegraphService = () => {
|
||||
super.configure(data)
|
||||
}
|
||||
}
|
||||
node.prototype.comfyClass = nodeDef.name
|
||||
|
||||
addNodeContextMenuHandler(node)
|
||||
addDrawBackgroundHandler(node)
|
||||
@@ -176,18 +179,12 @@ export const useLitegraphService = () => {
|
||||
await extensionService.invokeExtensionsAsync(
|
||||
'beforeRegisterNodeDef',
|
||||
node,
|
||||
nodeDefV1 // Receives V1 NodeDef, and potentially make modifications to it
|
||||
nodeDef // Receives V1 NodeDef
|
||||
)
|
||||
|
||||
const nodeDef = new ComfyNodeDefImpl(nodeDefV1)
|
||||
node.comfyClass = nodeDef.name
|
||||
node.prototype.comfyClass = nodeDef.name
|
||||
node.nodeData = nodeDef
|
||||
LiteGraph.registerNodeType(nodeId, node)
|
||||
// Note: Do not following assignments before `LiteGraph.registerNodeType`
|
||||
// because `registerNodeType` will overwrite the assignments.
|
||||
// Note: Do not move this to the class definition, it will be overwritten
|
||||
node.category = nodeDef.category
|
||||
node.title = nodeDef.display_name || nodeDef.name
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,12 +70,6 @@ export class ComfyNodeDefImpl implements ComfyNodeDefV1, ComfyNodeDefV2 {
|
||||
readonly nodeSource: NodeSource
|
||||
|
||||
constructor(obj: ComfyNodeDefV1) {
|
||||
/**
|
||||
* Assign extra fields to `this` for compatibility with group node feature.
|
||||
* TODO: Remove this once group node feature is removed.
|
||||
*/
|
||||
Object.assign(this, obj)
|
||||
|
||||
// Initialize V1 fields
|
||||
this.name = obj.name
|
||||
this.display_name = obj.display_name
|
||||
|
||||
96
src/stores/webviewStore.ts
Normal file
96
src/stores/webviewStore.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { merge } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
import { Component, computed, markRaw, ref, shallowRef } from 'vue'
|
||||
|
||||
export interface Webview {
|
||||
id: string
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
keepAlive?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Store used to manage webview canvases
|
||||
*/
|
||||
export const useWebviewStore = defineStore('webview', () => {
|
||||
const registeredWebviews = shallowRef<Record<string, Webview>>({})
|
||||
const activeWebviewId = ref<string | null>(null)
|
||||
|
||||
const activeWebview = computed(() =>
|
||||
activeWebviewId.value
|
||||
? registeredWebviews.value[activeWebviewId.value]
|
||||
: null
|
||||
)
|
||||
const hasActiveWebview = computed(() => activeWebviewId.value !== null)
|
||||
|
||||
/**
|
||||
* Register a new webview
|
||||
* @param webview The webview to register
|
||||
*/
|
||||
const registerWebview = (webview: Webview) => {
|
||||
registeredWebviews.value[webview.id] = {
|
||||
...webview,
|
||||
component: markRaw(webview.component)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a webview
|
||||
* @param id The ID of the webview to unregister
|
||||
*/
|
||||
const unregisterWebview = (id: string) => {
|
||||
const webview = registeredWebviews.value[id]
|
||||
if (!webview) return
|
||||
|
||||
if (activeWebviewId.value === id) {
|
||||
activeWebviewId.value = null
|
||||
}
|
||||
|
||||
delete registeredWebviews.value[id]
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a webview and make it active
|
||||
* @param id The ID of the webview to show
|
||||
* @param props Optional props to pass to the webview component
|
||||
*/
|
||||
const showWebview = (id: string, props?: Record<string, any>) => {
|
||||
const webview = registeredWebviews.value[id]
|
||||
if (!webview) return
|
||||
|
||||
if (props) {
|
||||
webview.props = merge(webview.props, props)
|
||||
}
|
||||
|
||||
activeWebviewId.value = id
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide a webview by ID
|
||||
* @param id The ID of the webview to hide
|
||||
*/
|
||||
const hideWebview = (id: string) => {
|
||||
if (activeWebviewId.value === id) {
|
||||
activeWebviewId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the active webview
|
||||
*/
|
||||
const hideActiveWebview = () => {
|
||||
activeWebviewId.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
activeWebviewId,
|
||||
activeWebview,
|
||||
hasActiveWebview,
|
||||
|
||||
registerWebview,
|
||||
unregisterWebview,
|
||||
showWebview,
|
||||
hideWebview,
|
||||
hideActiveWebview
|
||||
}
|
||||
})
|
||||
@@ -10,6 +10,12 @@
|
||||
<div class="comfyui-body-right" id="comfyui-body-right" />
|
||||
<div class="graph-canvas-container" id="graph-canvas-container">
|
||||
<GraphCanvas @ready="onGraphReady" />
|
||||
<div
|
||||
class="absolute inset-0 isolation z-50 pointer-events-none bg-[var(--p-dialog-background)]"
|
||||
:class="{ hidden: !webviewStore.hasActiveWebview }"
|
||||
>
|
||||
<WebviewContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +58,7 @@ import {
|
||||
} from '@/stores/queueStore'
|
||||
import { useServerConfigStore } from '@/stores/serverConfigStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWebviewStore } from '@/stores/webviewStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
@@ -66,6 +73,7 @@ const settingStore = useSettingStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const queueStore = useQueueStore()
|
||||
const webviewStore = useWebviewStore()
|
||||
|
||||
watch(
|
||||
() => colorPaletteStore.completedActivePalette,
|
||||
|
||||
Reference in New Issue
Block a user