mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
[Electron] Add basic welcome screen (#1491)
* WIP * Add LogTerminal * Modify server startup view * Add installView * Add basic welcome screen and dev server setup * nit * nit * nit * nit * nit
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.3.38",
|
"version": "1.3.38",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||||
|
"@comfyorg/comfyui-electron-types": "^0.2.10",
|
||||||
"@comfyorg/litegraph": "^0.8.24",
|
"@comfyorg/litegraph": "^0.8.24",
|
||||||
"@primevue/themes": "^4.0.5",
|
"@primevue/themes": "^4.0.5",
|
||||||
"@vueuse/core": "^11.0.0",
|
"@vueuse/core": "^11.0.0",
|
||||||
@@ -32,7 +33,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/core": "^7.24.7",
|
||||||
"@babel/preset-env": "^7.22.20",
|
"@babel/preset-env": "^7.22.20",
|
||||||
"@comfyorg/comfyui-electron-types": "^0.2.9",
|
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@iconify/json": "^2.2.245",
|
"@iconify/json": "^2.2.245",
|
||||||
"@pinia/testing": "^0.1.5",
|
"@pinia/testing": "^0.1.5",
|
||||||
@@ -1916,10 +1916,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@comfyorg/comfyui-electron-types": {
|
"node_modules/@comfyorg/comfyui-electron-types": {
|
||||||
"version": "0.2.9",
|
"version": "0.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@comfyorg/comfyui-electron-types/-/comfyui-electron-types-0.2.10.tgz",
|
||||||
"integrity": "sha512-wXmeGrQnuiEXlUkWDAnfkPgb8YTICX8EsNKSIWDb5uMSx46fW8gJQjTc1ehawV/fw1MmuqjhOpw+rV1f2+DAtw==",
|
"integrity": "sha512-JwqFeqmJBp6n276Ki+VEkMkO43rFHobdt93AzJYpWC+BXGUuvTyquon/MvblWtJDnTdO0mGWGXztDFe0sXie6A==",
|
||||||
"dev": true,
|
|
||||||
"license": "GPL-3.0-only"
|
"license": "GPL-3.0-only"
|
||||||
},
|
},
|
||||||
"node_modules/@comfyorg/litegraph": {
|
"node_modules/@comfyorg/litegraph": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"dev:electron": "vite --config vite.electron.config.mts",
|
||||||
"build": "npm run typecheck && vite build",
|
"build": "npm run typecheck && vite build",
|
||||||
"deploy": "npm run build && node scripts/deploy.js",
|
"deploy": "npm run build && node scripts/deploy.js",
|
||||||
"release": "node scripts/release.js",
|
"release": "node scripts/release.js",
|
||||||
@@ -26,7 +27,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.24.7",
|
"@babel/core": "^7.24.7",
|
||||||
"@babel/preset-env": "^7.22.20",
|
"@babel/preset-env": "^7.22.20",
|
||||||
"@comfyorg/comfyui-electron-types": "^0.2.9",
|
|
||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@iconify/json": "^2.2.245",
|
"@iconify/json": "^2.2.245",
|
||||||
"@pinia/testing": "^0.1.5",
|
"@pinia/testing": "^0.1.5",
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||||
|
"@comfyorg/comfyui-electron-types": "^0.2.10",
|
||||||
"@comfyorg/litegraph": "^0.8.24",
|
"@comfyorg/litegraph": "^0.8.24",
|
||||||
"@primevue/themes": "^4.0.5",
|
"@primevue/themes": "^4.0.5",
|
||||||
"@vueuse/core": "^11.0.0",
|
"@vueuse/core": "^11.0.0",
|
||||||
|
|||||||
66
src/components/common/LogTerminal.vue
Normal file
66
src/components/common/LogTerminal.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<!-- A simple read-only terminal component that displays logs. -->
|
||||||
|
<template>
|
||||||
|
<div class="p-terminal rounded-none h-full w-full">
|
||||||
|
<ScrollPanel class="h-full w-full" ref="scrollPanelRef">
|
||||||
|
<pre class="px-4 whitespace-pre-wrap">{{ log }}</pre>
|
||||||
|
</ScrollPanel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ScrollPanel from 'primevue/scrollpanel'
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
fetchLogs: () => Promise<string>
|
||||||
|
fetchInterval: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const log = ref<string>('')
|
||||||
|
const scrollPanelRef = ref<InstanceType<typeof ScrollPanel> | null>(null)
|
||||||
|
/**
|
||||||
|
* Whether the user has scrolled to the bottom of the terminal.
|
||||||
|
* This is used to prevent the terminal from scrolling to the bottom
|
||||||
|
* when new logs are fetched.
|
||||||
|
*/
|
||||||
|
const scrolledToBottom = ref(false)
|
||||||
|
|
||||||
|
let intervalId: number = 0
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const element = scrollPanelRef.value?.$el
|
||||||
|
const scrollContainer = element?.querySelector('.p-scrollpanel-content')
|
||||||
|
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.addEventListener('scroll', () => {
|
||||||
|
scrolledToBottom.value =
|
||||||
|
scrollContainer.scrollTop + scrollContainer.clientHeight ===
|
||||||
|
scrollContainer.scrollHeight
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (scrollContainer) {
|
||||||
|
scrollContainer.scrollTop = scrollContainer.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(log, () => {
|
||||||
|
if (scrolledToBottom.value) {
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
log.value = await props.fetchLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchLogs()
|
||||||
|
scrollToBottom()
|
||||||
|
intervalId = window.setInterval(fetchLogs, props.fetchInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.clearInterval(intervalId)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
47
src/components/install/WelcomeScreen.vue
Normal file
47
src/components/install/WelcomeScreen.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center gap-8 p-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<h1 class="text-4xl font-bold text-neutral-100">Welcome to ComfyUI</h1>
|
||||||
|
|
||||||
|
<!-- Get Started Button -->
|
||||||
|
<Button
|
||||||
|
label="Get Started"
|
||||||
|
icon="pi pi-arrow-right"
|
||||||
|
iconPos="right"
|
||||||
|
size="large"
|
||||||
|
@click="$emit('start')"
|
||||||
|
class="p-4 text-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Installation Steps -->
|
||||||
|
<div class="w-[600px]">
|
||||||
|
<Steps :model="installSteps" :readonly="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Steps from 'primevue/steps'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
|
||||||
|
const installSteps = [
|
||||||
|
{
|
||||||
|
label: 'Install Location',
|
||||||
|
icon: 'pi pi-folder'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Migration',
|
||||||
|
icon: 'pi pi-download'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Desktop Settings',
|
||||||
|
icon: 'pi pi-desktop'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Review',
|
||||||
|
icon: 'pi pi-check'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
defineEmits(['start'])
|
||||||
|
</script>
|
||||||
@@ -32,6 +32,19 @@ const router = createRouter({
|
|||||||
next('/')
|
next('/')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'install',
|
||||||
|
name: 'InstallView',
|
||||||
|
component: () => import('@/views/InstallView.vue'),
|
||||||
|
beforeEnter: async (to, from, next) => {
|
||||||
|
// Only allow access to this page in electron environment
|
||||||
|
if (isElectron()) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1784,6 +1784,8 @@ export class ComfyApp {
|
|||||||
*/
|
*/
|
||||||
async setup(canvasEl: HTMLCanvasElement) {
|
async setup(canvasEl: HTMLCanvasElement) {
|
||||||
this.canvasEl = canvasEl
|
this.canvasEl = canvasEl
|
||||||
|
// Show menu container for GraphView.
|
||||||
|
this.ui.menuContainer.style.display = 'block'
|
||||||
await this.#setUser()
|
await this.#setUser()
|
||||||
|
|
||||||
this.resizeCanvas()
|
this.resizeCanvas()
|
||||||
|
|||||||
@@ -636,6 +636,8 @@ export class ComfyUI {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
]) as HTMLDivElement
|
]) as HTMLDivElement
|
||||||
|
// Hide by default on construction so it does not interfere with other views.
|
||||||
|
this.menuContainer.style.display = 'none'
|
||||||
|
|
||||||
this.restoreMenuPosition = dragElement(this.menuContainer, this.settings)
|
this.restoreMenuPosition = dragElement(this.menuContainer, this.settings)
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ export function isElectron() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function electronAPI() {
|
export function electronAPI() {
|
||||||
return window['electronAPI'] as ElectronAPI
|
return (window as any)['electronAPI'] as ElectronAPI
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/views/InstallView.vue
Normal file
11
src/views/InstallView.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="font-sans flex flex-col justify-center items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||||
|
>
|
||||||
|
<WelcomeScreen />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import WelcomeScreen from '@/components/install/WelcomeScreen.vue'
|
||||||
|
</script>
|
||||||
@@ -1,17 +1,44 @@
|
|||||||
<!-- This is a dummy page built for electron app only. -->
|
|
||||||
<!-- Replace this page with the electron side logic on installation & log streaming. -->
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 flex flex-col items-center justify-center h-screen bg-surface-0"
|
class="font-sans flex flex-col justify-center items-center h-screen m-0 text-neutral-300 bg-neutral-900 dark-theme pointer-events-auto"
|
||||||
>
|
>
|
||||||
<ProgressSpinner class="w-16 h-16 mb-4" />
|
<h2 class="text-2xl font-bold">{{ ProgressMessages[status] }}</h2>
|
||||||
<div class="text-xl">{{ $t('loading') }}{{ counter }}</div>
|
<LogTerminal :fetch-logs="fetchLogs" :fetch-interval="500" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useInterval } from '@vueuse/core'
|
import LogTerminal from '@/components/common/LogTerminal.vue'
|
||||||
|
import {
|
||||||
|
ProgressStatus,
|
||||||
|
ProgressMessages
|
||||||
|
} from '@comfyorg/comfyui-electron-types'
|
||||||
|
import { electronAPI as getElectronAPI } from '@/utils/envUtil'
|
||||||
|
|
||||||
const counter = useInterval(1000)
|
const electronAPI = getElectronAPI()
|
||||||
|
|
||||||
|
const status = ref<ProgressStatus>(ProgressStatus.INITIAL_STATE)
|
||||||
|
const logs = ref<string[]>([])
|
||||||
|
|
||||||
|
const updateProgress = ({ status: newStatus }: { status: ProgressStatus }) => {
|
||||||
|
status.value = newStatus
|
||||||
|
logs.value = [] // Clear logs when status changes
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLogMessage = (message: string) => {
|
||||||
|
logs.value = [...logs.value, message]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
return logs.value.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
electronAPI.sendReady()
|
||||||
|
electronAPI.onProgressUpdate(updateProgress)
|
||||||
|
electronAPI.onLogMessage((message: string) => {
|
||||||
|
addLogMessage(message)
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
33
vite.electron.config.mts
Normal file
33
vite.electron.config.mts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { defineConfig, Plugin } from 'vite'
|
||||||
|
import { mergeConfig } from 'vite'
|
||||||
|
import type { UserConfig } from 'vitest/config'
|
||||||
|
import baseConfig from './vite.config.mts'
|
||||||
|
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
|
||||||
|
|
||||||
|
const electronAPIMock: Partial<ElectronAPI> = {
|
||||||
|
sendReady: () => {},
|
||||||
|
onShowSelectDirectory: () => {},
|
||||||
|
onFirstTimeSetupComplete: () => {},
|
||||||
|
onProgressUpdate: () => {},
|
||||||
|
onLogMessage: () => {},
|
||||||
|
isFirstTimeSetup: () => Promise.resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockElectronAPI: Plugin = {
|
||||||
|
name: 'mock-electron-api',
|
||||||
|
transformIndexHtml() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
children: `window.electronAPI = ${JSON.stringify(electronAPIMock)};`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
baseConfig as unknown as UserConfig,
|
||||||
|
defineConfig({
|
||||||
|
plugins: [mockElectronAPI]
|
||||||
|
})
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user