Compare commits

...

6 Commits

Author SHA1 Message Date
Chenlei Hu
08d2322817 1.8.4 (#2319) 2025-01-22 15:37:07 -05:00
filtered
8cfc1c4682 [Desktop] Remove beforeunload handler - unusable in electron (#2318) 2025-01-22 15:35:21 -05:00
filtered
c7bce87b8d [Desktop] Use Window Controls Overlay API (#2316)
Co-authored-by: huchenlei <huchenlei@proton.me>
2025-01-22 13:22:21 -05:00
Chenlei Hu
8f6b594a9f Fix console error on adding node via searchbox (#2317) 2025-01-22 13:13:26 -05:00
Chenlei Hu
1c9b300396 1.8.3 (#2312) 2025-01-21 20:36:54 -05:00
filtered
cd5283c4b7 [Refactor] Desktop maintenance task runner (#2311) 2025-01-21 20:18:01 -05:00
13 changed files with 130 additions and 96 deletions

12
global.d.ts vendored
View File

@@ -1,3 +1,15 @@
declare const __COMFYUI_FRONTEND_VERSION__: string
declare const __SENTRY_ENABLED__: boolean
declare const __SENTRY_DSN__: string
interface Navigator {
/**
* Used by the electron API. This is a WICG non-standard API, but is guaranteed to exist in Electron.
* It is `undefined` in Firefox and older browsers.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/windowControlsOverlay
*/
windowControlsOverlay?: {
/** When `true`, the window is using custom window style. */
visible: boolean
}
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.8.2",
"version": "1.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.8.2",
"version": "1.8.4",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.8.2",
"version": "1.8.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -5,12 +5,12 @@
>
<Card
class="max-w-48 relative h-full overflow-hidden"
:class="{ 'opacity-65': state.state !== 'error' }"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
<i
v-if="state.state === 'error'"
v-if="runner.state === 'error'"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
style="font-size: 10rem"
/>
@@ -38,7 +38,7 @@
</Card>
<i
v-if="!isLoading && state.state === 'OK'"
v-if="!isLoading && runner.state === 'OK'"
class="task-card-ok pi pi-check"
/>
</div>
@@ -54,7 +54,7 @@ import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
const taskStore = useMaintenanceTaskStore()
const state = computed(() => taskStore.getState(props.task))
const runner = computed(() => taskStore.getRunner(props.task))
// Properties
const props = defineProps<{
@@ -68,14 +68,14 @@ defineEmits<{
// Bindings
const description = computed(() =>
state.value.state === 'error'
runner.value.state === 'error'
? props.task.errorDescription ?? props.task.shortDescription
: props.task.shortDescription
)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveLoading = computed(() => state.value.refreshing)
const reactiveExecuting = computed(() => state.value.executing)
const reactiveLoading = computed(() => runner.value.refreshing)
const reactiveExecuting = computed(() => runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)

View File

@@ -2,12 +2,12 @@
<tr
class="border-neutral-700 border-solid border-y"
:class="{
'opacity-50': state.state === 'resolved',
'opacity-75': isLoading && state.state !== 'resolved'
'opacity-50': runner.resolved,
'opacity-75': isLoading && runner.resolved
}"
>
<td class="text-center w-16">
<TaskListStatusIcon :state="state.state" :loading="isLoading" />
<TaskListStatusIcon :state="runner.state" :loading="isLoading" />
</td>
<td>
<p class="inline-block">{{ task.name }}</p>
@@ -51,7 +51,7 @@ import { useMinLoadingDurationRef } from '@/utils/refUtil'
import TaskListStatusIcon from './TaskListStatusIcon.vue'
const taskStore = useMaintenanceTaskStore()
const state = computed(() => taskStore.getState(props.task))
const runner = computed(() => taskStore.getRunner(props.task))
// Properties
const props = defineProps<{
@@ -65,14 +65,14 @@ defineEmits<{
// Binding
const severity = computed<VueSeverity>(() =>
state.value.state === 'error' || state.value.state === 'warning'
runner.value.state === 'error' || runner.value.state === 'warning'
? 'primary'
: 'secondary'
)
// Use a minimum run time to ensure tasks "feel" like they have run
const reactiveLoading = computed(() => state.value.refreshing)
const reactiveExecuting = computed(() => state.value.executing)
const reactiveLoading = computed(() => runner.value.refreshing)
const reactiveExecuting = computed(() => runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)

View File

@@ -60,8 +60,9 @@
<!-- FilterAndValue -->
<template v-slot:chip="{ value }">
<SearchFilterChip
v-if="Array.isArray(value) && value.length === 2"
:key="`${value[0].id}-${value[1]}`"
@remove="onRemoveFilter($event, value)"
@remove="onRemoveFilter($event, value as FilterAndValue)"
:text="value[1]"
:badge="value[0].invokeSequence.toUpperCase()"
:badge-class="value[0].invokeSequence + '-badge'"

View File

@@ -33,7 +33,7 @@
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow && !showTopMenu"
v-show="isNativeWindow() && !showTopMenu"
class="fixed top-0 left-0 app-drag w-full h-[var(--comfy-topbar-height)]"
/>
</template>
@@ -50,7 +50,12 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { app } from '@/scripts/app'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI, isElectron, showNativeMenu } from '@/utils/envUtil'
import {
electronAPI,
isElectron,
isNativeWindow,
showNativeMenu
} from '@/utils/envUtil'
const workspaceState = useWorkspaceStore()
const settingStore = useSettingStore()
@@ -64,10 +69,6 @@ const teleportTarget = computed(() =>
? '.comfyui-body-top'
: '.comfyui-body-bottom'
)
const isNativeWindow = computed(
() =>
isElectron() && settingStore.get('Comfy-Desktop.WindowStyle') === 'custom'
)
const showTopMenu = computed(
() => betaMenuEnabled.value && !workspaceState.focusMode
)

View File

@@ -3,12 +3,77 @@ import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { DESKTOP_MAINTENANCE_TASKS } from '@/constants/desktopMaintenanceTasks'
import type {
MaintenanceTask,
MaintenanceTaskState
} from '@/types/desktop/maintenanceTypes'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
/** State of a maintenance task, managed by the maintenance task store. */
type MaintenanceTaskState = 'warning' | 'error' | 'OK' | 'skipped'
// Type not exported by API
type ValidationState = InstallValidation['basePath']
// Add index to API type
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
/** State of a maintenance task, managed by the maintenance task store. */
export class MaintenanceTaskRunner {
constructor(readonly task: MaintenanceTask) {}
private _state?: MaintenanceTaskState
/** The current state of the task. Setter also controls {@link resolved} as a side-effect. */
get state() {
return this._state
}
/** Updates the task state and {@link resolved} status. */
setState(value: MaintenanceTaskState) {
// Mark resolved
if (this._state === 'error' && value === 'OK') this.resolved = true
// Mark unresolved (if previously resolved)
if (value === 'error') this.resolved &&= false
this._state = value
}
/** `true` if the task has been resolved (was `error`, now `OK`). This is a side-effect of the {@link state} setter. */
resolved?: boolean
/** Whether the task state is currently being refreshed. */
refreshing?: boolean
/** Whether the task is currently running. */
executing?: boolean
/** The error message that occurred when the task failed. */
error?: string
update(update: IndexedUpdate) {
const state = update[this.task.id]
this.refreshing = state === undefined
if (state) this.setState(state)
}
finaliseUpdate(update: IndexedUpdate) {
this.refreshing = false
this.setState(update[this.task.id] ?? 'skipped')
}
/** Wraps the execution of a maintenance task, updating state and rethrowing errors. */
async execute(task: MaintenanceTask) {
try {
this.executing = true
const success = await task.execute()
if (!success) return false
this.error = undefined
return true
} catch (error) {
this.error = (error as Error)?.message
throw error
} finally {
this.executing = false
}
}
}
/**
* User-initiated maintenance tasks. Currently only used by the desktop app maintenance view.
*
@@ -24,78 +89,46 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
const isRunningTerminalCommand = computed(() =>
tasks.value
.filter((task) => task.usesTerminal)
.some((task) => getState(task)?.executing)
.some((task) => getRunner(task)?.executing)
)
const isRunningInstallationFix = computed(() =>
tasks.value
.filter((task) => task.isInstallationFix)
.some((task) => getState(task)?.executing)
.some((task) => getRunner(task)?.executing)
)
// Task list
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
const taskStates = ref(
new Map<MaintenanceTask['id'], MaintenanceTaskState>(
DESKTOP_MAINTENANCE_TASKS.map((x) => [x.id, {}])
new Map<MaintenanceTask['id'], MaintenanceTaskRunner>(
DESKTOP_MAINTENANCE_TASKS.map((x) => [x.id, new MaintenanceTaskRunner(x)])
)
)
/** True if any tasks are in an error state. */
const anyErrors = computed(() =>
tasks.value.some((task) => getState(task).state === 'error')
tasks.value.some((task) => getRunner(task).state === 'error')
)
/** Wraps the execution of a maintenance task, updating state and rethrowing errors. */
const execute = async (task: MaintenanceTask) => {
const state = getState(task)
try {
state.executing = true
const success = await task.execute()
if (!success) return false
state.error = undefined
return true
} catch (error) {
state.error = (error as Error)?.message
throw error
} finally {
state.executing = false
}
}
/**
* Returns the matching state object for a task.
* @param task Task to get the matching state object for
* @returns The state object for this task
*/
const getState = (task: MaintenanceTask) => taskStates.value.get(task.id)!
const getRunner = (task: MaintenanceTask) => taskStates.value.get(task.id)!
/**
* Updates the task list with the latest validation state.
* @param validationUpdate Update details passed in by electron
*/
const processUpdate = (validationUpdate: InstallValidation) => {
// Type not exported by API
type ValidationState = InstallValidation['basePath']
// Add index to API type
type IndexedUpdate = InstallValidation & Record<string, ValidationState>
const update = validationUpdate as IndexedUpdate
isRefreshing.value = true
// Update each task state
for (const task of tasks.value) {
const state = getState(task)
state.refreshing = update[task.id] === undefined
// Mark resolved
if (state.state === 'error' && update[task.id] === 'OK')
state.state = 'resolved'
if (update[task.id] === 'OK' && state.state === 'resolved') continue
if (update[task.id]) state.state = update[task.id]
getRunner(task).update(update)
}
// Final update
@@ -103,11 +136,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
isRefreshing.value = false
for (const task of tasks.value) {
const state = getState(task)
state.refreshing = false
if (state.state === 'resolved') continue
state.state = update[task.id] ?? 'skipped'
getRunner(task).finaliseUpdate(update)
}
}
}
@@ -115,8 +144,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
/** Clears the resolved status of tasks (when changing filters) */
const clearResolved = () => {
for (const task of tasks.value) {
const state = getState(task)
if (state?.state === 'resolved') state.state = 'OK'
getRunner(task).resolved &&= false
}
}
@@ -127,13 +155,17 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
await electron.Validation.validateInstallation(processUpdate)
}
const execute = async (task: MaintenanceTask) => {
return getRunner(task).execute(task)
}
return {
tasks,
isRefreshing,
isRunningTerminalCommand,
isRunningInstallationFix,
execute,
getState,
getRunner,
processUpdate,
clearResolved,
/** True if any tasks are in an error state. */

View File

@@ -39,18 +39,6 @@ export interface MaintenanceTask {
isInstallationFix?: boolean
}
/** State of a maintenance task, managed by the maintenance task store. */
export interface MaintenanceTaskState {
/** The current state of the task. */
state?: 'warning' | 'error' | 'resolved' | 'OK' | 'skipped'
/** Whether the task state is currently being refreshed. */
refreshing?: boolean
/** Whether the task is currently running. */
executing?: boolean
/** The error message that occurred when the task failed. */
error?: string
}
/** The filter options for the maintenance task list. */
export interface MaintenanceFilter {
/** CSS classes used for the filter button icon, e.g. 'pi pi-cross' */

View File

@@ -14,3 +14,7 @@ export function electronAPI() {
export function showNativeMenu(event: MouseEvent) {
electronAPI()?.showContextMenu(event as ElectronContextMenuOptions)
}
export function isNativeWindow() {
return isElectron() && !!window.navigator.windowControlsOverlay?.visible
}

View File

@@ -4,7 +4,7 @@
<TopMenubar />
<GraphCanvas @ready="onGraphReady" />
<GlobalToast />
<UnloadWindowConfirmDialog />
<UnloadWindowConfirmDialog v-if="!isElectron()" />
<BrowserTabTitle />
<MenuHamburger />
</template>

View File

@@ -125,8 +125,8 @@ const displayAsList = ref(PrimeIcons.TH_LARGE)
const errorFilter = computed(() =>
taskStore.tasks.filter((x) => {
const { state } = taskStore.getState(x)
return state === 'error' || state === 'resolved'
const { state, resolved } = taskStore.getRunner(x)
return state === 'error' || resolved
})
)

View File

@@ -9,7 +9,7 @@
>
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow"
v-show="isNativeWindow()"
ref="topMenuRef"
class="app-drag w-full h-[var(--comfy-topbar-height)]"
/>
@@ -24,7 +24,7 @@
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { electronAPI, isElectron, isNativeWindow } from '@/utils/envUtil'
const props = withDefaults(
defineProps<{
@@ -46,12 +46,8 @@ const lightTheme = {
}
const topMenuRef = ref<HTMLDivElement | null>(null)
const isNativeWindow = ref(false)
onMounted(async () => {
if (isElectron()) {
const windowStyle = await electronAPI().Config.getWindowStyle()
isNativeWindow.value = windowStyle === 'custom'
await nextTick()
electronAPI().changeTheme({