mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-21 21:07:33 +00:00
Compare commits
16 Commits
fix/load-a
...
austin/dra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c79c2684a | ||
|
|
21d47a5fb5 | ||
|
|
3ebda6cb65 | ||
|
|
149b01db03 | ||
|
|
fbf7a4c7dc | ||
|
|
c8a9ac63cd | ||
|
|
42dd2c29fb | ||
|
|
70318e1613 | ||
|
|
53a3e23938 | ||
|
|
5dd1613c2c | ||
|
|
72ccec85ae | ||
|
|
ac82555d31 | ||
|
|
1cd6c8b739 | ||
|
|
b2bc20a665 | ||
|
|
e0ebc5168b | ||
|
|
d6c6ad6da2 |
59
src/components/common/DraggableList.vue
Normal file
59
src/components/common/DraggableList.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import { onBeforeUnmount, ref, useTemplateRef, watchEffect } from 'vue'
|
||||
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
|
||||
const modelValue = defineModel<T[]>({ required: true })
|
||||
const draggableList = ref<DraggableList>()
|
||||
const draggableItems = useTemplateRef('draggableItems')
|
||||
|
||||
watchEffect(() => {
|
||||
void modelValue.value.length
|
||||
draggableList.value?.dispose()
|
||||
if (!draggableItems.value?.children?.length) return
|
||||
draggableList.value = new DraggableList(
|
||||
draggableItems.value,
|
||||
'.draggable-item'
|
||||
)
|
||||
draggableList.value.applyNewItemsOrder = function () {
|
||||
const reorderedItems = []
|
||||
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem
|
||||
}
|
||||
}
|
||||
const newPosition = reorderedItems.indexOf(this.draggableItem)
|
||||
const itemList = modelValue.value
|
||||
const [item] = itemList.splice(oldPosition, 1)
|
||||
itemList.splice(newPosition, 0, item)
|
||||
modelValue.value = [...itemList]
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
draggableList.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div ref="draggableItems" class="pb-2 px-2 space-y-0.5 mt-0.5">
|
||||
<slot
|
||||
drag-class="draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -20,6 +20,7 @@ import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import TabApp from './TabApp.vue'
|
||||
import TabError from './TabError.vue'
|
||||
import TabInfo from './info/TabInfo.vue'
|
||||
import TabGlobalParameters from './parameters/TabGlobalParameters.vue'
|
||||
@@ -109,6 +110,10 @@ const tabs = computed<RightSidePanelTabList>(() => {
|
||||
icon: 'icon-[lucide--octagon-alert] bg-node-stroke-error ml-1'
|
||||
})
|
||||
}
|
||||
list.push({
|
||||
label: () => 'app',
|
||||
value: 'app'
|
||||
})
|
||||
|
||||
list.push({
|
||||
label: () =>
|
||||
@@ -298,6 +303,7 @@ function handleProxyWidgetsUpdate(value: ProxyWidgetsProperty) {
|
||||
<!-- Panel Content -->
|
||||
<div class="scrollbar-thin flex-1 overflow-y-auto">
|
||||
<template v-if="!hasSelection">
|
||||
<TabApp v-if="activeTab === 'app'" />
|
||||
<TabGlobalParameters v-if="activeTab === 'parameters'" />
|
||||
<TabNodes v-else-if="activeTab === 'nodes'" />
|
||||
<TabGlobalSettings v-else-if="activeTab === 'settings'" />
|
||||
|
||||
311
src/components/rightSidePanel/TabApp.vue
Normal file
311
src/components/rightSidePanel/TabApp.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import { remove } from 'es-toolkit'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { computed, reactive, toValue } from 'vue'
|
||||
import type { MaybeRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
import IoItem from '@/components/rightSidePanel/app/IoItem.vue'
|
||||
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import { useHoveredStore } from '@/stores/hoveredStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
type BoundStyle = { top: string; left: string; width: string; height: string }
|
||||
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
const canvasStore = useCanvasStore()
|
||||
const hoveredStore = useHoveredStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
const canvas: LGraphCanvas = canvasStore.getCanvas()
|
||||
|
||||
const selectedInputs = reactive<
|
||||
[string, string, string, (() => void) | undefined, MaybeRef<BoundStyle>][]
|
||||
>([])
|
||||
const selectedOutputs = reactive<[NodeId, string][]>([])
|
||||
|
||||
async function renameWidget(
|
||||
widget: IBaseWidget,
|
||||
input: INodeInputSlot,
|
||||
key: string
|
||||
) {
|
||||
const newLabel = await useDialogService().prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('g.enterNewNamePrompt'),
|
||||
defaultValue: widget.label,
|
||||
placeholder: widget.name
|
||||
})
|
||||
if (newLabel === null) return
|
||||
widget.label = newLabel || undefined
|
||||
input.label = newLabel || undefined
|
||||
widget.callback?.(widget.value)
|
||||
useCanvasStore().canvas?.setDirty(true)
|
||||
const inputTuple = selectedInputs.find(([k]) => k === key)
|
||||
if (!inputTuple) return
|
||||
|
||||
inputTuple[1] = newLabel
|
||||
}
|
||||
|
||||
function getHovered(
|
||||
e?: MouseEvent
|
||||
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
|
||||
const { graph } = canvas
|
||||
if (!canvas || !graph) return
|
||||
|
||||
if (settingStore.get('Comfy.VueNodes.Enabled')) {
|
||||
const node = graph.getNodeById(hoveredStore.hoveredNodeId)
|
||||
if (!node) return
|
||||
|
||||
const widget = node.widgets?.find(
|
||||
(w) => w.name === hoveredStore.hoveredWidgetName
|
||||
)
|
||||
return [node, widget]
|
||||
}
|
||||
if (!e) return
|
||||
|
||||
canvas.adjustMouseEvent(e)
|
||||
const node = graph.getNodeOnPos(e.canvasX, e.canvasY)
|
||||
if (!node) return
|
||||
|
||||
return [node, node.getWidgetOnPos(e.canvasX, e.canvasY, false)]
|
||||
}
|
||||
|
||||
function elementPosition(e: HTMLElement) {
|
||||
const bounding = useElementBounding(e)
|
||||
return computed(() => ({
|
||||
width: `${bounding.width.value / canvas.ds.scale}px`,
|
||||
height: `${bounding.height.value / canvas.ds.scale}px`,
|
||||
left: `${bounding.left.value / canvas.ds.scale - canvas.ds.offset[0]}px`,
|
||||
top: `${bounding.top.value / canvas.ds.scale - canvas.ds.offset[1]}px`
|
||||
}))
|
||||
}
|
||||
function getBounding(nodeId: NodeId, widgetName?: string) {
|
||||
if (settingStore.get('Comfy.VueNodes.Enabled')) {
|
||||
const element = document.querySelector(
|
||||
widgetName
|
||||
? `[data-node-id="${nodeId}"] [data-widget-name="${widgetName}"`
|
||||
: `[data-node-id="${nodeId}"]`
|
||||
)
|
||||
return element instanceof HTMLElement ? elementPosition(element) : undefined
|
||||
}
|
||||
const node = canvas.graph?.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
|
||||
if (!widgetName)
|
||||
return {
|
||||
width: `${node.size[0]}px`,
|
||||
height: `${node.size[1] + 30}px`,
|
||||
left: `${node.pos[0]}px`,
|
||||
top: `${node.pos[1] - 30}px`
|
||||
}
|
||||
const widget = node.widgets?.find((w) => w.name === widgetName)
|
||||
if (!widget) return
|
||||
|
||||
return {
|
||||
width: `${node.size[0] - 30}px`,
|
||||
height: `${(widget.computedHeight ?? 24) - 4}px`,
|
||||
left: `${node.pos[0] + 15}px`,
|
||||
top: `${node.pos[1] + widget.y}px`
|
||||
}
|
||||
}
|
||||
|
||||
function handleDown(e: MouseEvent) {
|
||||
const [node] = getHovered(e) ?? []
|
||||
if (!node || e.button > 0) canvasInteractions.forwardEventToCanvas(e)
|
||||
}
|
||||
function handleClick(e: MouseEvent) {
|
||||
const [node, widget] = getHovered(e) ?? []
|
||||
if (!node) return canvasInteractions.forwardEventToCanvas(e)
|
||||
|
||||
if (!widget) {
|
||||
if (!node.constructor.nodeData?.output_node)
|
||||
return canvasInteractions.forwardEventToCanvas(e)
|
||||
const index = selectedOutputs.findIndex(([id]) => id === node.id)
|
||||
if (index === -1) selectedOutputs.push([node.id, node.title])
|
||||
else selectedOutputs.splice(index, 1)
|
||||
return
|
||||
}
|
||||
|
||||
const key = `${node.id}: ${widget.name}`
|
||||
const bounding = getBounding(node.id, widget.name)
|
||||
const keyIndex = selectedInputs.findIndex(([k]) => k === key)
|
||||
const input = node.inputs.find((i) => i.widget?.name === widget.name)
|
||||
const rename = input && (() => renameWidget(widget, input, key))
|
||||
if (keyIndex === -1 && bounding)
|
||||
selectedInputs.push([
|
||||
key,
|
||||
widget.label ?? widget.name,
|
||||
node.title,
|
||||
rename,
|
||||
bounding
|
||||
])
|
||||
else selectedInputs.splice(keyIndex, 1)
|
||||
}
|
||||
|
||||
function nodeToDisplayTuple(
|
||||
n: LGraphNode
|
||||
): [NodeId, MaybeRef<BoundStyle> | undefined, boolean] {
|
||||
return [n.id, getBounding(n.id), selectedOutputs.some(([id]) => n.id === id)]
|
||||
}
|
||||
|
||||
const outputNodes = computed(() =>
|
||||
canvas
|
||||
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
|
||||
.map(nodeToDisplayTuple)
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex font-bold p-2 border-border-subtle border-b">
|
||||
{{ t('[ph]App builder mode') }}
|
||||
<Button class="ml-auto">
|
||||
{{ t('[ph]Exit builder') }}
|
||||
</Button>
|
||||
</div>
|
||||
<PropertiesAccordionItem
|
||||
:label="t('nodeHelpPage.inputs')"
|
||||
enable-empty-state
|
||||
:disabled="!selectedInputs.length"
|
||||
class="border-border-subtle border-b"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="w-full p-4 text-muted-foreground gap-2 flex flex-col">
|
||||
<div
|
||||
v-text="t('[ph]Click on node parameters to add them here as inputs')"
|
||||
/>
|
||||
<div
|
||||
class="text-base-foreground"
|
||||
v-text="t('[ph]No inputs added yet')"
|
||||
/>
|
||||
<div
|
||||
v-text="
|
||||
t(
|
||||
'[ph]Users will interact and adjust these to generate their outputs.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div v-text="t('[ph]Examples: “Load image”, “Text prompt”, “Steps”')" />
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('[ph]Click on node parameters to add them here as inputs')"
|
||||
/>
|
||||
<DraggableList v-slot="{ dragClass }" v-model="selectedInputs">
|
||||
<IoItem
|
||||
v-for="[key, title, subTitle, rename] in selectedInputs"
|
||||
:key
|
||||
:class="cn(dragClass, 'bg-primary-background/30 p-2 my-2 rounded-lg')"
|
||||
:title
|
||||
:sub-title
|
||||
:rename
|
||||
:remove="() => remove(selectedInputs, ([k]) => k === key)"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
<PropertiesAccordionItem
|
||||
:label="t('nodeHelpPage.outputs')"
|
||||
enable-empty-state
|
||||
:disabled="!selectedOutputs.length"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="w-full p-4 text-muted-foreground gap-2 flex flex-col">
|
||||
<div
|
||||
v-text="
|
||||
t(
|
||||
'[ph]Click on output nodes to add them here. These will be the generated results.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
class="text-base-foreground"
|
||||
v-text="t('[ph]No outputs nodes added yet')"
|
||||
/>
|
||||
<div
|
||||
v-text="
|
||||
t(
|
||||
'[ph]Connect at least one output node so users can see results after running.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div v-text="t('[ph]Examples: “Save Image” or “Save Video”')" />
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
class="w-full p-4 pt-2 text-muted-foreground"
|
||||
v-text="t('[ph]Click on node parameters to add them here as inputs')"
|
||||
/>
|
||||
<DraggableList v-slot="{ dragClass }" v-model="selectedOutputs">
|
||||
<IoItem
|
||||
v-for="([key, title], index) in selectedOutputs"
|
||||
:key
|
||||
:class="
|
||||
cn(
|
||||
dragClass,
|
||||
'bg-warning-background/40 p-2 my-2 rounded-lg',
|
||||
index === 0 && 'ring-warning-background ring-2'
|
||||
)
|
||||
"
|
||||
:title
|
||||
:sub-title="String(key)"
|
||||
:remove="() => remove(selectedOutputs, ([k]) => k === key)"
|
||||
/>
|
||||
</DraggableList>
|
||||
</PropertiesAccordionItem>
|
||||
|
||||
<Teleport to="body">
|
||||
<TransformPane
|
||||
:class="
|
||||
cn(
|
||||
'absolute w-full h-full pointer-events-auto!',
|
||||
getHovered() ? 'cursor-pointer' : 'cursor-grab'
|
||||
)
|
||||
"
|
||||
:canvas="canvasStore.getCanvas()"
|
||||
@pointerdown="handleDown"
|
||||
@click="handleClick"
|
||||
@wheel="canvasInteractions.forwardEventToCanvas"
|
||||
>
|
||||
<div
|
||||
v-for="[key, , , , style] in selectedInputs"
|
||||
:key
|
||||
:style="toValue(style)"
|
||||
class="fixed bg-primary-background/30 rounded-lg"
|
||||
/>
|
||||
<div
|
||||
v-for="[key, style, isSelected] in outputNodes"
|
||||
:key
|
||||
:style="toValue(style)"
|
||||
:class="
|
||||
cn(
|
||||
'fixed ring-warning-background ring-5 rounded-2xl',
|
||||
!isSelected && 'ring-warning-background/50'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="absolute top-0 right-0 size-8">
|
||||
<div
|
||||
v-if="isSelected"
|
||||
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg"
|
||||
>
|
||||
<i class="icon-[lucide--check] bg-text-foreground size-full" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TransformPane>
|
||||
</Teleport>
|
||||
</template>
|
||||
46
src/components/rightSidePanel/app/IoItem.vue
Normal file
46
src/components/rightSidePanel/app/IoItem.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { rename, remove } = defineProps<{
|
||||
title: string
|
||||
subTitle?: string
|
||||
rename?: () => void
|
||||
remove?: () => void
|
||||
}>()
|
||||
|
||||
const entries = computed(() => {
|
||||
const items = []
|
||||
if (rename)
|
||||
items.push({
|
||||
label: t('g.rename'),
|
||||
action: rename,
|
||||
icon: 'icon-[lucide--pencil]'
|
||||
})
|
||||
if (remove)
|
||||
items.push({
|
||||
label: t('g.delete'),
|
||||
action: remove,
|
||||
icon: 'icon-[lucide--trash-2]'
|
||||
})
|
||||
return [items]
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="p-2 my-2 rounded-lg flex items-center-safe">
|
||||
<span class="mr-auto" v-text="title" />
|
||||
<span class="text-muted-foreground mr-2" v-text="subTitle" />
|
||||
<Popover :entries>
|
||||
<template #button>
|
||||
<Button variant="muted-textonly">
|
||||
<i class="icon-[lucide--ellipsis]" />
|
||||
</Button>
|
||||
</template>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useMounted, watchDebounced } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef
|
||||
} from 'vue'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import type { ValidFavoritedWidget } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
@@ -25,8 +17,6 @@ const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
const { t } = useI18n()
|
||||
|
||||
const draggableList = ref<DraggableList | undefined>(undefined)
|
||||
const sectionWidgetsRef = ref<{ widgetsContainer: HTMLElement }>()
|
||||
const isSearching = ref(false)
|
||||
|
||||
const favoritedWidgets = computed(
|
||||
@@ -47,67 +37,6 @@ async function searcher(query: string) {
|
||||
isSearching.value = query.trim().length > 0
|
||||
searchedFavoritedWidgets.value = searchWidgets(favoritedWidgets.value, query)
|
||||
}
|
||||
|
||||
const isMounted = useMounted()
|
||||
|
||||
function setDraggableState() {
|
||||
if (!isMounted.value) return
|
||||
draggableList.value?.dispose()
|
||||
const container = sectionWidgetsRef.value?.widgetsContainer
|
||||
if (isSearching.value || !container?.children?.length) return
|
||||
|
||||
draggableList.value = new DraggableList(container, '.draggable-item')
|
||||
|
||||
draggableList.value.applyNewItemsOrder = function () {
|
||||
const reorderedItems: HTMLElement[] = []
|
||||
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem as HTMLElement
|
||||
}
|
||||
}
|
||||
|
||||
const newPosition = reorderedItems.indexOf(
|
||||
this.draggableItem as HTMLElement
|
||||
)
|
||||
const widgets = [...searchedFavoritedWidgets.value]
|
||||
const [widget] = widgets.splice(oldPosition, 1)
|
||||
widgets.splice(newPosition, 0, widget)
|
||||
searchedFavoritedWidgets.value = widgets
|
||||
favoritedWidgetsStore.reorderFavorites(widgets)
|
||||
}
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
searchedFavoritedWidgets,
|
||||
() => {
|
||||
setDraggableState()
|
||||
},
|
||||
{ debounce: 100 }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
setDraggableState()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
draggableList.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -118,42 +47,45 @@ onBeforeUnmount(() => {
|
||||
:update-key="favoritedWidgets"
|
||||
/>
|
||||
</div>
|
||||
<SectionWidgets
|
||||
ref="sectionWidgetsRef"
|
||||
:label
|
||||
:widgets="searchedFavoritedWidgets"
|
||||
:is-draggable="!isSearching"
|
||||
hidden-favorite-indicator
|
||||
show-node-name
|
||||
enable-empty-state
|
||||
class="border-b border-interface-stroke"
|
||||
@update:collapse="nextTick(setDraggableState)"
|
||||
<DraggableList
|
||||
v-model="searchedFavoritedWidgets"
|
||||
@update:model-value="favoritedWidgetsStore.reorderFavorites"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="text-sm text-muted-foreground px-4 text-center py-10">
|
||||
<p>
|
||||
{{
|
||||
isSearching
|
||||
? t('rightSidePanel.noneSearchDesc')
|
||||
: t('rightSidePanel.favoritesNoneDesc')
|
||||
}}
|
||||
</p>
|
||||
<i18n-t
|
||||
v-if="!isSearching"
|
||||
keypath="rightSidePanel.favoritesNoneHint"
|
||||
tag="p"
|
||||
class="mt-2 text-xs"
|
||||
>
|
||||
<template #moreIcon>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="inline-flex size-5 items-center justify-center rounded-md bg-secondary-background-hover text-secondary-foreground align-middle"
|
||||
>
|
||||
<i class="icon-[lucide--more-vertical] text-sm" />
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
</SectionWidgets>
|
||||
<SectionWidgets
|
||||
:label
|
||||
:widgets="searchedFavoritedWidgets"
|
||||
:is-draggable="!isSearching"
|
||||
hidden-favorite-indicator
|
||||
show-node-name
|
||||
enable-empty-state
|
||||
class="border-b border-interface-stroke"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="text-sm text-muted-foreground px-4 text-center py-10">
|
||||
<p>
|
||||
{{
|
||||
isSearching
|
||||
? t('rightSidePanel.noneSearchDesc')
|
||||
: t('rightSidePanel.favoritesNoneDesc')
|
||||
}}
|
||||
</p>
|
||||
<i18n-t
|
||||
v-if="!isSearching"
|
||||
keypath="rightSidePanel.favoritesNoneHint"
|
||||
tag="p"
|
||||
class="mt-2 text-xs"
|
||||
>
|
||||
<template #moreIcon>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="inline-flex size-5 items-center justify-center rounded-md bg-secondary-background-hover text-secondary-foreground align-middle"
|
||||
>
|
||||
<i class="icon-[lucide--more-vertical] text-sm" />
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</template>
|
||||
</SectionWidgets>
|
||||
</DraggableList>
|
||||
</template>
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useMounted, watchDebounced } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
computed,
|
||||
customRef,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
triggerRef,
|
||||
useTemplateRef,
|
||||
watch
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
import { searchWidgets } from '../shared'
|
||||
@@ -37,13 +32,10 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const advancedInputsCollapsed = ref(true)
|
||||
const draggableList = ref<DraggableList | undefined>(undefined)
|
||||
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
|
||||
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
|
||||
|
||||
// Use customRef to track proxyWidgets changes
|
||||
@@ -137,66 +129,6 @@ async function searcher(query: string) {
|
||||
searchedWidgetsList.value = searchWidgets(widgetsList.value, query)
|
||||
}
|
||||
|
||||
const isMounted = useMounted()
|
||||
|
||||
function setDraggableState() {
|
||||
if (!isMounted.value) return
|
||||
|
||||
draggableList.value?.dispose()
|
||||
const container = sectionWidgetsRef.value?.widgetsContainer
|
||||
if (isSearching.value || !container?.children?.length) return
|
||||
|
||||
draggableList.value = new DraggableList(container, '.draggable-item')
|
||||
|
||||
draggableList.value.applyNewItemsOrder = function () {
|
||||
const reorderedItems: HTMLElement[] = []
|
||||
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
if (oldPosition === -1) {
|
||||
console.error('[TabSubgraphInputs] draggableItem not found in items')
|
||||
return
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem as HTMLElement
|
||||
}
|
||||
}
|
||||
|
||||
const newPosition = reorderedItems.indexOf(
|
||||
this.draggableItem as HTMLElement
|
||||
)
|
||||
|
||||
// Update proxyWidgets order
|
||||
const pw = proxyWidgets.value
|
||||
const [w] = pw.splice(oldPosition, 1)
|
||||
pw.splice(newPosition, 0, w)
|
||||
proxyWidgets.value = pw
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
triggerRef(proxyWidgets)
|
||||
}
|
||||
}
|
||||
|
||||
watchDebounced(searchedWidgetsList, () => setDraggableState(), {
|
||||
debounce: 100
|
||||
})
|
||||
onMounted(() => setDraggableState())
|
||||
onBeforeUnmount(() => draggableList.value?.dispose())
|
||||
|
||||
const label = computed(() => {
|
||||
return searchedWidgetsList.value.length !== 0
|
||||
? t('rightSidePanel.inputs')
|
||||
@@ -212,28 +144,28 @@ const label = computed(() => {
|
||||
:update-key="widgetsList"
|
||||
/>
|
||||
</div>
|
||||
<SectionWidgets
|
||||
ref="sectionWidgetsRef"
|
||||
:node
|
||||
:label
|
||||
:parents
|
||||
:widgets="searchedWidgetsList"
|
||||
:is-draggable="!isSearching"
|
||||
:enable-empty-state="isSearching"
|
||||
:tooltip="
|
||||
isSearching || searchedWidgetsList.length
|
||||
? ''
|
||||
: t('rightSidePanel.inputsNoneTooltip')
|
||||
"
|
||||
class="border-b border-interface-stroke"
|
||||
@update:collapse="nextTick(setDraggableState)"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15">
|
||||
{{ t('rightSidePanel.noneSearchDesc') }}
|
||||
</div>
|
||||
</template>
|
||||
</SectionWidgets>
|
||||
<DraggableList v-model="proxyWidgets">
|
||||
<SectionWidgets
|
||||
:node
|
||||
:label
|
||||
:parents
|
||||
:widgets="searchedWidgetsList"
|
||||
:is-draggable="!isSearching"
|
||||
:enable-empty-state="isSearching"
|
||||
:tooltip="
|
||||
isSearching || searchedWidgetsList.length
|
||||
? ''
|
||||
: t('rightSidePanel.inputsNoneTooltip')
|
||||
"
|
||||
class="border-b border-interface-stroke"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="text-sm text-muted-foreground px-4 text-center pt-5 pb-15">
|
||||
{{ t('rightSidePanel.noneSearchDesc') }}
|
||||
</div>
|
||||
</template>
|
||||
</SectionWidgets>
|
||||
</DraggableList>
|
||||
<SectionWidgets
|
||||
v-if="advancedInputsWidgets.length > 0"
|
||||
ref="advancedInputsSectionRef"
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
computed,
|
||||
customRef,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
triggerRef,
|
||||
watch
|
||||
} from 'vue'
|
||||
import { computed, customRef, onMounted, triggerRef } from 'vue'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import {
|
||||
demoteWidget,
|
||||
@@ -28,9 +21,9 @@ import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
|
||||
|
||||
@@ -38,8 +31,6 @@ const canvasStore = useCanvasStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const draggableList = ref<DraggableList | undefined>(undefined)
|
||||
const draggableItems = ref()
|
||||
const proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({
|
||||
get() {
|
||||
track()
|
||||
@@ -188,54 +179,9 @@ function showRecommended() {
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
|
||||
function setDraggableState() {
|
||||
draggableList.value?.dispose()
|
||||
if (searchQuery.value || !draggableItems.value?.children?.length) return
|
||||
draggableList.value = new DraggableList(
|
||||
draggableItems.value,
|
||||
'.draggable-item'
|
||||
)
|
||||
draggableList.value.applyNewItemsOrder = function () {
|
||||
const reorderedItems = []
|
||||
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem
|
||||
}
|
||||
}
|
||||
const newPosition = reorderedItems.indexOf(this.draggableItem)
|
||||
const aw = activeWidgets.value
|
||||
const [w] = aw.splice(oldPosition, 1)
|
||||
aw.splice(newPosition, 0, w)
|
||||
activeWidgets.value = aw
|
||||
}
|
||||
}
|
||||
watch(filteredActive, () => {
|
||||
setDraggableState()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setDraggableState()
|
||||
if (activeNode.value) pruneDisconnected(activeNode.value)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
draggableList.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -273,19 +219,18 @@ onBeforeUnmount(() => {
|
||||
{{ $t('subgraphStore.hideAll') }}</a
|
||||
>
|
||||
</div>
|
||||
<div ref="draggableItems" class="pb-2 px-2 space-y-0.5 mt-0.5">
|
||||
<DraggableList v-slot="{ dragClass }" v-model="activeWidgets">
|
||||
<SubgraphNodeWidget
|
||||
v-for="[node, widget] in filteredActive"
|
||||
:key="toKey([node, widget])"
|
||||
class="bg-comfy-menu-bg"
|
||||
:class="cn(!searchQuery && dragClass, 'bg-comfy-menu-bg')"
|
||||
:node-title="node.title"
|
||||
:widget-name="widget.name"
|
||||
:is-shown="true"
|
||||
:is-draggable="!searchQuery"
|
||||
:is-physical="node.id === -1"
|
||||
:is-draggable="!searchQuery"
|
||||
@toggle-visibility="demote([node, widget])"
|
||||
/>
|
||||
</div>
|
||||
</DraggableList>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -29,8 +29,7 @@ function getIcon() {
|
||||
cn(
|
||||
'flex py-1 px-2 break-all rounded items-center gap-1',
|
||||
'bg-node-component-surface',
|
||||
props.isDraggable &&
|
||||
'draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing hover:ring-1 ring-accent-background',
|
||||
props.isDraggable && 'hover:ring-1 ring-accent-background',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
|
||||
25
src/stores/hoveredStore.ts
Normal file
25
src/stores/hoveredStore.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useElementByPoint, useMouse } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export const useHoveredStore = defineStore('hovered', () => {
|
||||
const { x, y } = useMouse({ type: 'client' })
|
||||
const { element } = useElementByPoint({ x, y, multiple: true })
|
||||
|
||||
const hoveredWidgetName = computed(() => {
|
||||
const widgetEl = element.value?.find((e) => e.matches('.lg-node-widget'))
|
||||
if (!(widgetEl instanceof HTMLElement)) return
|
||||
|
||||
return widgetEl.dataset.widgetName
|
||||
})
|
||||
const hoveredNodeId = computed(() => {
|
||||
const nodeEl = element.value?.find((e) => e.matches('.lg-node'))
|
||||
if (!(nodeEl instanceof HTMLElement)) return
|
||||
|
||||
return nodeEl.dataset.nodeId
|
||||
})
|
||||
return {
|
||||
hoveredNodeId,
|
||||
hoveredWidgetName
|
||||
}
|
||||
})
|
||||
@@ -10,6 +10,7 @@ export type RightSidePanelTab =
|
||||
| 'settings'
|
||||
| 'info'
|
||||
| 'subgraph'
|
||||
| 'app'
|
||||
|
||||
type RightSidePanelSection = 'advanced-inputs' | string
|
||||
|
||||
|
||||
Reference in New Issue
Block a user