mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
Floating menu option (#726)
* Add floating menu * Fix * Updates * Add auto-queue change test * Fix
This commit is contained in:
committed by
Chenlei Hu
parent
73a7f7dae0
commit
2d1ff64951
@@ -1,6 +1,7 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import { ComfyAppMenu } from './helpers/appMenu'
|
||||
import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import * as fs from 'fs'
|
||||
@@ -237,6 +238,7 @@ export class ComfyPage {
|
||||
// Components
|
||||
public readonly searchBox: ComfyNodeSearchBox
|
||||
public readonly menu: ComfyMenu
|
||||
public readonly appMenu: ComfyAppMenu
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
|
||||
@@ -247,6 +249,7 @@ export class ComfyPage {
|
||||
this.workflowUploadInput = page.locator('#comfy-file-input')
|
||||
this.searchBox = new ComfyNodeSearchBox(page)
|
||||
this.menu = new ComfyMenu(page)
|
||||
this.appMenu = new ComfyAppMenu(page)
|
||||
}
|
||||
|
||||
async getGraphNodesCount(): Promise<number> {
|
||||
|
||||
120
browser_tests/appMenu.spec.ts
Normal file
120
browser_tests/appMenu.spec.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { Response } from '@playwright/test'
|
||||
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
|
||||
import { expect, mergeTests } from '@playwright/test'
|
||||
import { comfyPageFixture } from './ComfyPage'
|
||||
import { webSocketFixture } from './fixtures/ws.ts'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
test.describe('AppMenu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
const currentThemeId = await comfyPage.menu.getThemeId()
|
||||
if (currentThemeId !== 'dark') {
|
||||
await comfyPage.menu.toggleTheme()
|
||||
}
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
/**
|
||||
* This test ensures that the autoqueue change mode can only queue one change at a time
|
||||
*/
|
||||
test('Does not auto-queue multiple changes at a time', async ({
|
||||
comfyPage,
|
||||
ws
|
||||
}) => {
|
||||
// Enable change auto-queue mode
|
||||
let queueOpts = await comfyPage.appMenu.queueButton.toggleOptions()
|
||||
expect(await queueOpts.getMode()).toBe('disabled')
|
||||
await queueOpts.setMode('change')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await queueOpts.getMode()).toBe('change')
|
||||
await comfyPage.appMenu.queueButton.toggleOptions()
|
||||
|
||||
// Intercept the prompt queue endpoint
|
||||
let promptNumber = 0
|
||||
comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
prompt_id: promptNumber,
|
||||
number: ++promptNumber,
|
||||
node_errors: {},
|
||||
// Include the request data to validate which prompt was queued so we can validate the width
|
||||
__request: req.postDataJSON()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Start watching for a message to prompt
|
||||
const requestPromise = comfyPage.page.waitForResponse('**/api/prompt')
|
||||
|
||||
// Find and set the width on the latent node
|
||||
const triggerChange = async (value: number) => {
|
||||
return await comfyPage.page.evaluate((value) => {
|
||||
const node = window['app'].graph._nodes.find(
|
||||
(n) => n.type === 'EmptyLatentImage'
|
||||
)
|
||||
node.widgets[0].value = value
|
||||
window['app'].workflowManager.activeWorkflow.changeTracker.checkState()
|
||||
}, value)
|
||||
}
|
||||
|
||||
// Trigger a status websocket message
|
||||
const triggerStatus = async (queueSize: number) => {
|
||||
await ws.trigger({
|
||||
type: 'status',
|
||||
data: {
|
||||
status: {
|
||||
exec_info: {
|
||||
queue_remaining: queueSize
|
||||
}
|
||||
}
|
||||
}
|
||||
} as StatusWsMessage)
|
||||
}
|
||||
|
||||
// Extract the width from the queue response
|
||||
const getQueuedWidth = async (resp: Promise<Response>) => {
|
||||
const obj = await (await resp).json()
|
||||
return obj['__request']['prompt']['5']['inputs']['width']
|
||||
}
|
||||
|
||||
// Trigger a bunch of changes
|
||||
const START = 32
|
||||
const END = 64
|
||||
for (let i = START; i <= END; i += 8) {
|
||||
await triggerChange(i)
|
||||
}
|
||||
|
||||
// Ensure the queued width is the first value
|
||||
expect(
|
||||
await getQueuedWidth(requestPromise),
|
||||
'the first queued prompt should be the first change width'
|
||||
).toBe(START)
|
||||
|
||||
// Ensure that no other changes are queued
|
||||
await expect(
|
||||
comfyPage.page.waitForResponse('**/api/prompt', { timeout: 250 })
|
||||
).rejects.toThrow()
|
||||
expect(
|
||||
promptNumber,
|
||||
'only 1 prompt should have been queued even though there were multiple changes'
|
||||
).toBe(1)
|
||||
|
||||
// Trigger a status update so auto-queue re-runs
|
||||
await triggerStatus(1)
|
||||
await triggerStatus(0)
|
||||
|
||||
// Ensure the queued width is the last queued value
|
||||
expect(
|
||||
await getQueuedWidth(comfyPage.page.waitForResponse('**/api/prompt')),
|
||||
'last queued prompt width should be the last change'
|
||||
).toBe(END)
|
||||
expect(promptNumber, 'queued prompt count should be 2').toBe(2)
|
||||
})
|
||||
})
|
||||
51
browser_tests/fixtures/ws.ts
Normal file
51
browser_tests/fixtures/ws.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export const webSocketFixture = base.extend<{
|
||||
ws: { trigger(data: any, url?: string): Promise<void> }
|
||||
}>({
|
||||
ws: [
|
||||
async ({ page }, use) => {
|
||||
// Each time a page loads, to catch navigations
|
||||
page.on('load', async () => {
|
||||
await page.evaluate(function () {
|
||||
// Create a wrapper for WebSocket that stores them globally
|
||||
// so we can look it up to trigger messages
|
||||
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
|
||||
window.WebSocket = class extends window.WebSocket {
|
||||
constructor() {
|
||||
// @ts-expect-error
|
||||
super(...arguments)
|
||||
store[this.url] = this
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await use({
|
||||
async trigger(data, url) {
|
||||
// Trigger a websocket event on the page
|
||||
await page.evaluate(
|
||||
function ([data, url]) {
|
||||
if (!url) {
|
||||
// If no URL specified, use page URL
|
||||
const u = new URL(window.location.toString())
|
||||
u.protocol = 'ws:'
|
||||
u.pathname = '/'
|
||||
url = u.toString() + 'ws'
|
||||
}
|
||||
const ws: WebSocket = (window as any).__ws__[url]
|
||||
ws.dispatchEvent(
|
||||
new MessageEvent('message', {
|
||||
data
|
||||
})
|
||||
)
|
||||
},
|
||||
[JSON.stringify(data), url]
|
||||
)
|
||||
}
|
||||
})
|
||||
},
|
||||
// We need this to run automatically as the first thing so it adds handlers as soon as the page loads
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
65
browser_tests/helpers/appMenu.ts
Normal file
65
browser_tests/helpers/appMenu.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { Page, Locator } from '@playwright/test'
|
||||
|
||||
export class ComfyAppMenu {
|
||||
public readonly root: Locator
|
||||
public readonly queueButton: ComfyQueueButton
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.root = page.locator('.app-menu')
|
||||
this.queueButton = new ComfyQueueButton(this)
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyQueueButton {
|
||||
public readonly root: Locator
|
||||
public readonly primaryButton: Locator
|
||||
public readonly dropdownButton: Locator
|
||||
constructor(public readonly appMenu: ComfyAppMenu) {
|
||||
this.root = appMenu.root.getByTestId('queue-button')
|
||||
this.primaryButton = this.root.locator('.p-splitbutton-button')
|
||||
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
|
||||
}
|
||||
|
||||
public async toggleOptions() {
|
||||
await this.dropdownButton.click()
|
||||
return new ComfyQueueButtonOptions(this.appMenu.page)
|
||||
}
|
||||
}
|
||||
|
||||
class ComfyQueueButtonOptions {
|
||||
public readonly popup: Locator
|
||||
public readonly modes: {
|
||||
disabled: { input: Locator; wrapper: Locator }
|
||||
instant: { input: Locator; wrapper: Locator }
|
||||
change: { input: Locator; wrapper: Locator }
|
||||
}
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.popup = page.getByTestId('queue-options')
|
||||
this.modes = (['disabled', 'instant', 'change'] as const).reduce(
|
||||
(modes, mode) => {
|
||||
modes[mode] = {
|
||||
input: page.locator(`#autoqueue-${mode}`),
|
||||
wrapper: page.getByTestId(`autoqueue-${mode}`)
|
||||
}
|
||||
return modes
|
||||
},
|
||||
{} as ComfyQueueButtonOptions['modes']
|
||||
)
|
||||
}
|
||||
|
||||
public async setMode(mode: keyof ComfyQueueButtonOptions['modes']) {
|
||||
await this.modes[mode].input.click()
|
||||
}
|
||||
|
||||
public async getMode() {
|
||||
return (
|
||||
await Promise.all(
|
||||
Object.entries(this.modes).map(async ([mode, opt]) => [
|
||||
mode,
|
||||
await opt.wrapper.getAttribute('data-p-checked')
|
||||
])
|
||||
)
|
||||
).find(([, checked]) => checked === 'true')?.[0]
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
<GlobalToast />
|
||||
<UnloadWindowConfirmDialog />
|
||||
<BrowserTabTitle />
|
||||
<AppMenu />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -41,6 +42,7 @@ import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
import GlobalToast from '@/components/toast/GlobalToast.vue'
|
||||
import UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'
|
||||
import BrowserTabTitle from '@/components/BrowserTabTitle.vue'
|
||||
import AppMenu from '@/components/appMenu/AppMenu.vue'
|
||||
|
||||
const isLoading = computed<boolean>(() => useWorkspaceStore().spinner)
|
||||
|
||||
@@ -67,6 +69,8 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
setupAutoQueueHandler()
|
||||
|
||||
watchEffect(() => {
|
||||
const fontSize = settingStore.get('Comfy.TextareaWidget.FontSize')
|
||||
document.documentElement.style.setProperty(
|
||||
|
||||
@@ -75,4 +75,9 @@ const gutterClass = computed(() => {
|
||||
z-index: 999;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.comfyui-floating-menu .splitter-overlay {
|
||||
top: var(--comfy-floating-menu-height);
|
||||
height: calc(100% - var(--comfy-floating-menu-height));
|
||||
}
|
||||
</style>
|
||||
|
||||
247
src/components/appMenu/AppMenu.vue
Normal file
247
src/components/appMenu/AppMenu.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<Panel v-if="visible" class="app-menu">
|
||||
<div class="app-menu-content">
|
||||
<Popover ref="queuePopover" data-testid="queue-options">
|
||||
<div class="queue-options">
|
||||
<p class="batch-count">
|
||||
<FloatLabel v-tooltip="$t('menu.batchCountTooltip')">
|
||||
<InputNumber id="batchCount" v-model="batchCount" :min="1" />
|
||||
<label for="batchCount">{{ $t('menu.batchCount') }}</label>
|
||||
</FloatLabel>
|
||||
|
||||
<Slider
|
||||
v-model="batchCount"
|
||||
:min="1"
|
||||
:max="100"
|
||||
v-tooltip="$t('menu.batchCountTooltip')"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Divider layout="vertical" />
|
||||
|
||||
<p class="auto-queue">
|
||||
<span class="label">{{ $t('menu.autoQueue') }}</span>
|
||||
<template v-for="mode in queueModes" :key="mode">
|
||||
<div
|
||||
v-tooltip="$t(`menu.${mode}Tooltip`)"
|
||||
class="auto-queue-mode"
|
||||
>
|
||||
<RadioButton
|
||||
v-model="queueMode"
|
||||
:inputId="`autoqueue-${mode}`"
|
||||
name="dynamic"
|
||||
:value="mode"
|
||||
:data-testid="`autoqueue-${mode}`"
|
||||
/>
|
||||
<label :for="`autoqueue-${mode}`">{{
|
||||
$t(`menu.${mode}`)
|
||||
}}</label>
|
||||
</div>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
</Popover>
|
||||
<SplitButton
|
||||
v-tooltip.bottom="$t('menu.queueWorkflow')"
|
||||
:label="$t('menu.generate')"
|
||||
:icon="`pi pi-${icon}`"
|
||||
severity="secondary"
|
||||
@click="queuePrompt"
|
||||
:model="[]"
|
||||
:pt="{
|
||||
pcDropdown: ({ instance }) => {
|
||||
instance.onDropdownButtonClick = function (e: Event) {
|
||||
e.preventDefault()
|
||||
queuePopover.toggle(e)
|
||||
}
|
||||
}
|
||||
}"
|
||||
data-testid="queue-button"
|
||||
>
|
||||
</SplitButton>
|
||||
<div class="separator"></div>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.interrupt')"
|
||||
icon="pi pi-times"
|
||||
severity="secondary"
|
||||
:disabled="!executingPrompt"
|
||||
@click="actions.interrupt"
|
||||
></Button>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.refresh')"
|
||||
icon="pi pi-refresh"
|
||||
severity="secondary"
|
||||
@click="actions.refresh"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.clipspace')"
|
||||
icon="pi pi-clipboard"
|
||||
severity="secondary"
|
||||
@click="actions.openClipspace"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.resetView')"
|
||||
icon="pi pi-expand"
|
||||
severity="secondary"
|
||||
@click="actions.resetView"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.bottom="$t('menu.clear')"
|
||||
icon="pi pi-ban"
|
||||
severity="secondary"
|
||||
@click="actions.clearWorkflow"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import Panel from 'primevue/panel'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import Button from 'primevue/button'
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import Popover from 'primevue/popover'
|
||||
import Divider from 'primevue/divider'
|
||||
import Slider from 'primevue/slider'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
useQueueSettingsStore
|
||||
} from '@/stores/queueStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { batchCount, mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const visible = computed(
|
||||
() => settingsStore.get('Comfy.UseNewMenu') === 'Floating'
|
||||
)
|
||||
|
||||
const queuePopover = ref(null)
|
||||
const queueModes = ['disabled', 'instant', 'change']
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (queueMode.value) {
|
||||
case 'instant':
|
||||
return 'forward'
|
||||
case 'change':
|
||||
return 'step-forward-alt'
|
||||
default:
|
||||
return 'play'
|
||||
}
|
||||
})
|
||||
|
||||
const executingPrompt = computed(() => !!queueCountStore.count.value)
|
||||
|
||||
const queuePrompt = (e: MouseEvent) => {
|
||||
app.queuePrompt(e.shiftKey ? -1 : 0, batchCount.value)
|
||||
}
|
||||
|
||||
const actions = {
|
||||
interrupt: async () => {
|
||||
await api.interrupt()
|
||||
useToastStore().add({
|
||||
severity: 'info',
|
||||
summary: 'Interrupted',
|
||||
detail: 'Execution has been interrupted',
|
||||
life: 1000
|
||||
})
|
||||
},
|
||||
clearWorkflow: () => {
|
||||
if (
|
||||
!(settingsStore.get('Comfy.ComfirmClear') ?? true) ||
|
||||
confirm('Clear workflow?')
|
||||
) {
|
||||
app.clean()
|
||||
app.graph.clear()
|
||||
api.dispatchEvent(new CustomEvent('graphCleared'))
|
||||
}
|
||||
},
|
||||
resetView: () => app.resetView(),
|
||||
openClipspace: () => app['openClipspace'](),
|
||||
refresh: () => app.refreshComboInNodes()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-menu {
|
||||
pointer-events: all;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.app-menu-content {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
:deep(.p-panel-header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.separator {
|
||||
background-color: var(--p-content-border-color);
|
||||
border-radius: 10px;
|
||||
opacity: 0.75;
|
||||
width: 5px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.queue-options {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.batch-count {
|
||||
padding-top: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.p-slider {
|
||||
--p-slider-border-radius: 5px;
|
||||
margin: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.p-floatlabel label {
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: var(--p-floatlabel-focus-color);
|
||||
}
|
||||
|
||||
.auto-queue {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.auto-queue-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
</style>
|
||||
20
src/i18n.ts
20
src/i18n.ts
@@ -57,6 +57,26 @@ const messages = {
|
||||
coverImagePreview: 'Fit Image Preview',
|
||||
clearPendingTasks: 'Clear Pending Tasks'
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
batchCount: 'Batch Count',
|
||||
batchCountTooltip:
|
||||
'The number of times the workflow generation should be queued',
|
||||
autoQueue: 'Auto Queue',
|
||||
disabled: 'Disabled',
|
||||
disabledTooltip: 'The workflow will not be automatically queued',
|
||||
instant: 'Instant',
|
||||
instantTooltip:
|
||||
'The workflow will be queued instantly after a generation finishes',
|
||||
change: 'On Change',
|
||||
changeTooltip: 'The workflow will be queued once a change is made',
|
||||
queueWorkflow: 'Queue workflow',
|
||||
generate: 'Generate',
|
||||
interrupt: 'Cancel current run',
|
||||
refresh: 'Refresh node definitions',
|
||||
clipspace: 'Open Clipspace',
|
||||
resetView: 'Reset canvas view',
|
||||
clear: 'Clear workflow'
|
||||
}
|
||||
},
|
||||
zh: {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getInterruptButton } from './interruptButton'
|
||||
import './menu.css'
|
||||
import type { ComfySettingsDialog } from '../settings'
|
||||
|
||||
type MenuPosition = 'Disabled' | 'Top' | 'Bottom'
|
||||
type MenuPosition = 'Disabled' | 'Top' | 'Bottom' | 'Floating'
|
||||
|
||||
const collapseOnMobile = (t) => {
|
||||
;(t.element ?? t).classList.add('comfyui-menu-mobile-collapse')
|
||||
@@ -89,7 +89,7 @@ export class ComfyAppMenu {
|
||||
})
|
||||
)
|
||||
|
||||
this.actionsGroup = new ComfyButtonGroup(
|
||||
const actionButtons = [
|
||||
new ComfyButton({
|
||||
icon: 'refresh',
|
||||
content: 'Refresh',
|
||||
@@ -123,13 +123,14 @@ export class ComfyAppMenu {
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
]
|
||||
this.actionsGroup = new ComfyButtonGroup(...actionButtons)
|
||||
|
||||
// Keep the settings group as there are custom scripts attaching extra
|
||||
// elements to it.
|
||||
this.settingsGroup = new ComfyButtonGroup()
|
||||
this.viewGroup = new ComfyButtonGroup(
|
||||
getInterruptButton('nlg-hide').element
|
||||
)
|
||||
const interruptButton = getInterruptButton('nlg-hide').element
|
||||
this.viewGroup = new ComfyButtonGroup(interruptButton)
|
||||
this.mobileMenuButton = new ComfyButton({
|
||||
icon: 'menu',
|
||||
action: (_, btn) => {
|
||||
@@ -165,15 +166,36 @@ export class ComfyAppMenu {
|
||||
experimental: true,
|
||||
tooltip: 'On small screens the menu will always be at the top.',
|
||||
type: 'combo',
|
||||
options: ['Disabled', 'Top', 'Bottom'],
|
||||
options: ['Disabled', 'Floating', 'Top', 'Bottom'],
|
||||
onChange: async (v: MenuPosition) => {
|
||||
if (v && v !== 'Disabled') {
|
||||
if (!resizeHandler) {
|
||||
resizeHandler = () => {
|
||||
this.calculateSizeBreak()
|
||||
const floating = v === 'Floating'
|
||||
if (floating) {
|
||||
if (resizeHandler) {
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
resizeHandler = null
|
||||
}
|
||||
this.element.classList.add('floating')
|
||||
document.body.classList.add('comfyui-floating-menu')
|
||||
} else {
|
||||
this.element.classList.remove('floating')
|
||||
document.body.classList.remove('comfyui-floating-menu')
|
||||
if (!resizeHandler) {
|
||||
resizeHandler = () => {
|
||||
this.calculateSizeBreak()
|
||||
}
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
}
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
}
|
||||
|
||||
for (const b of [
|
||||
...actionButtons.map((b) => b.element),
|
||||
interruptButton,
|
||||
this.queueButton.element
|
||||
]) {
|
||||
b.style.display = floating ? 'none' : null
|
||||
}
|
||||
|
||||
this.updatePosition(v)
|
||||
} else {
|
||||
if (resizeHandler) {
|
||||
@@ -204,7 +226,11 @@ export class ComfyAppMenu {
|
||||
} else {
|
||||
this.app.bodyTop.prepend(this.element)
|
||||
}
|
||||
this.calculateSizeBreak()
|
||||
if (v === 'Floating') {
|
||||
this.updateSizeBreak(0, this.#sizeBreaks.indexOf(this.#sizeBreak), -999)
|
||||
} else {
|
||||
this.calculateSizeBreak()
|
||||
}
|
||||
}
|
||||
|
||||
updateSizeBreak(idx: number, prevIdx: number, direction: number) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
:root {
|
||||
--comfy-floating-menu-height: 45px;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
@@ -118,6 +121,9 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comfyui-button-group:empty {
|
||||
display: none;
|
||||
}
|
||||
.comfyui-button-group > .comfyui-button,
|
||||
.comfyui-button-group > .comfyui-button-wrapper > .comfyui-button {
|
||||
padding: 4px 10px;
|
||||
@@ -142,6 +148,22 @@
|
||||
overflow: auto;
|
||||
max-height: 90vh;
|
||||
}
|
||||
.comfyui-menu.floating {
|
||||
width: max-content;
|
||||
padding: 8px 0 8px 12px;
|
||||
overflow: hidden;
|
||||
border-bottom-right-radius: 12px;
|
||||
height: var(--comfy-floating-menu-height);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.comfyui-menu.floating .comfyui-logo {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.comfyui-floating-menu .comfyui-body-left {
|
||||
margin-top: var(--comfy-floating-menu-height);
|
||||
}
|
||||
|
||||
.comfyui-menu>* {
|
||||
flex-shrink: 0;
|
||||
@@ -708,4 +730,5 @@
|
||||
}
|
||||
.comfyui-body-bottom .lt-sm.comfyui-menu > .comfyui-menu-button{
|
||||
bottom: 41px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ export class ComfyWorkflowsMenu {
|
||||
const r = getExtraMenuOptions?.apply?.(this, arguments)
|
||||
const setting = app.ui.settings.getSettingValue(
|
||||
'Comfy.UseNewMenu',
|
||||
false
|
||||
'Disabled'
|
||||
)
|
||||
if (setting && setting != 'Disabled') {
|
||||
const t = this
|
||||
|
||||
41
src/services/autoQueueService.ts
Normal file
41
src/services/autoQueueService.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
useQueueSettingsStore,
|
||||
useQueuePendingTaskCountStore
|
||||
} from '@/stores/queueStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
export function setupAutoQueueHandler() {
|
||||
const queueCountStore = useQueuePendingTaskCountStore()
|
||||
const queueSettingsStore = useQueueSettingsStore()
|
||||
|
||||
let graphHasChanged = false
|
||||
let internalCount = 0 // Use an internal counter here so it is instantly updated when re-queuing
|
||||
api.addEventListener('graphChanged', () => {
|
||||
if (queueSettingsStore.mode === 'change') {
|
||||
if (internalCount) {
|
||||
graphHasChanged = true
|
||||
} else {
|
||||
graphHasChanged = false
|
||||
app.queuePrompt(0, queueSettingsStore.batchCount)
|
||||
internalCount++
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
queueCountStore.$subscribe(
|
||||
() => {
|
||||
internalCount = queueCountStore.count
|
||||
if (!internalCount && !app.lastExecutionError) {
|
||||
if (
|
||||
queueSettingsStore.mode === 'instant' ||
|
||||
(queueSettingsStore.mode === 'change' && graphHasChanged)
|
||||
) {
|
||||
graphHasChanged = false
|
||||
app.queuePrompt(0, queueSettingsStore.batchCount)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ detached: true }
|
||||
)
|
||||
}
|
||||
@@ -355,3 +355,12 @@ export const useQueuePendingTaskCountStore = defineStore(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type AutoQueueMode = 'disabled' | 'instant' | 'change'
|
||||
|
||||
export const useQueueSettingsStore = defineStore('queueSettingsStore', {
|
||||
state: () => ({
|
||||
mode: 'disabled' as AutoQueueMode,
|
||||
batchCount: 1
|
||||
})
|
||||
})
|
||||
|
||||
@@ -493,8 +493,8 @@ const zSettings = z.record(z.any()).and(
|
||||
'Comfy.SnapToGrid.GridSize': z.number(),
|
||||
'Comfy.TextareaWidget.FontSize': z.number(),
|
||||
'Comfy.TextareaWidget.Spellcheck': z.boolean(),
|
||||
'Comfy.UseNewMenu': z.enum(['Disabled', 'Floating', 'Top', 'Bottom']),
|
||||
'Comfy.TreeExplorer.ItemPadding': z.number(),
|
||||
'Comfy.UseNewMenu': z.any(),
|
||||
'Comfy.Validation.Workflows': z.boolean(),
|
||||
'Comfy.Workflow.SortNodeIdOnSave': z.boolean(),
|
||||
'Comfy.Queue.ImageFit': z.enum(['contain', 'cover']),
|
||||
|
||||
Reference in New Issue
Block a user